diff --git a/.eslintrc b/.eslintrc index 3b6ab7498d9..46fb354c11c 100644 --- a/.eslintrc +++ b/.eslintrc @@ -147,10 +147,15 @@ "Chart": true, "Cypress": true, "cy": true, + "describe": true, + "expect": true, "it": true, "context": true, "before": true, "beforeEach": true, - "onScan": true + "onScan": true, + "html2canvas": true, + "extend_cscript": true, + "localforage": true } } diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index be425ec2d9d..6cc5e3547ba 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -10,3 +10,6 @@ # This commit just changes spaces to tabs for indentation in some files 5f473611bd6ed57703716244a054d3fb5ba9cd23 + +# Whitespace trimming throughout codebase +9bb69e711a5da43aaf8c8ecb5601aeffd89dbe5a diff --git a/.github/helper/documentation.py b/.github/helper/documentation.py index 9cc4663c394..b4a4ba1bbdd 100644 --- a/.github/helper/documentation.py +++ b/.github/helper/documentation.py @@ -32,11 +32,15 @@ if __name__ == "__main__": if response.ok: payload = response.json() - title = payload.get("title", "").lower() + title = payload.get("title", "").lower().strip() head_sha = payload.get("head", {}).get("sha") body = payload.get("body", "").lower() - if title.startswith("feat") and head_sha and "no-docs" not in body: + if (title.startswith("feat") + and head_sha + and "no-docs" not in body + and "backport" not in body + ): if docs_link_exists(body): print("Documentation Link Found. You're Awesome! 🎉") diff --git a/.github/helper/install.sh b/.github/helper/install.sh index 7b0f944c669..e7f46410e6c 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 @@ -42,5 +37,5 @@ sed -i 's/socketio:/# socketio:/g' Procfile sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile bench get-app erpnext "${GITHUB_WORKSPACE}" -bench start & +bench start &> bench_run_logs.txt & bench --site test_site reinstall --yes diff --git a/.github/helper/semgrep_rules/frappe_correctness.yml b/.github/helper/semgrep_rules/frappe_correctness.yml index faab3344a62..d9603e89aa4 100644 --- a/.github/helper/semgrep_rules/frappe_correctness.yml +++ b/.github/helper/semgrep_rules/frappe_correctness.yml @@ -98,8 +98,6 @@ rules: languages: [python] severity: WARNING paths: - exclude: - - test_*.py include: - "*/**/doctype/*" diff --git a/.github/helper/semgrep_rules/security.yml b/.github/helper/semgrep_rules/security.yml index 5a5098bf506..8b219792080 100644 --- a/.github/helper/semgrep_rules/security.yml +++ b/.github/helper/semgrep_rules/security.yml @@ -8,18 +8,3 @@ rules: dynamic content. Avoid it or use safe_eval(). languages: [python] severity: ERROR - -- id: frappe-sqli-format-strings - patterns: - - pattern-inside: | - @frappe.whitelist() - def $FUNC(...): - ... - - pattern-either: - - pattern: frappe.db.sql("..." % ...) - - pattern: frappe.db.sql(f"...", ...) - - pattern: frappe.db.sql("...".format(...), ...) - message: | - Detected use of raw string formatting for SQL queries. This can lead to sql injection vulnerabilities. Refer security guidelines - https://github.com/frappe/erpnext/wiki/Code-Security-Guidelines - languages: [python] - severity: WARNING diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index 7c6b8432b87..1d180f251e1 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -1,16 +1,25 @@ name: Backport on: - pull_request: + pull_request_target: types: - closed - labeled jobs: - backport: - runs-on: ubuntu-18.04 - name: Backport + main: + runs-on: ubuntu-latest steps: - - name: Backport - uses: tibdex/backport@v1 + - name: Checkout Actions + uses: actions/checkout@v2 with: - github_token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + repository: "frappe/backport" + path: ./actions + ref: develop + - name: Install Actions + run: npm install --production --prefix ./actions + - name: Run backport + uses: ./actions/backport + with: + token: ${{secrets.BACKPORT_BOT_TOKEN}} + labelsToAdd: "backport" + title: "{{originalTitle}}" diff --git a/.github/workflows/patch.yml b/.github/workflows/patch.yml index b96a3d6bbed..eaab24b9081 100644 --- a/.github/workflows/patch.yml +++ b/.github/workflows/patch.yml @@ -1,6 +1,12 @@ name: Patch -on: [pull_request, workflow_dispatch] +on: + pull_request: + paths-ignore: + - '**.js' + - '**.md' + workflow_dispatch: + jobs: test: @@ -26,6 +32,12 @@ jobs: with: python-version: 3.6 + - name: Setup Node + uses: actions/setup-node@v2 + with: + node-version: 12 + 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/semgrep.yml b/.github/workflows/semgrep.yml index 389524e9684..e27b406df05 100644 --- a/.github/workflows/semgrep.yml +++ b/.github/workflows/semgrep.yml @@ -1,34 +1,18 @@ name: Semgrep on: - pull_request: - branches: - - develop - - version-13-hotfix - - version-13-pre-release + pull_request: { } + jobs: semgrep: name: Frappe Linter runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: Setup python3 - uses: actions/setup-python@v2 - with: - python-version: 3.8 - - - name: Setup semgrep - run: | - python -m pip install -q semgrep - git fetch origin $GITHUB_BASE_REF:$GITHUB_BASE_REF -q - - - name: Semgrep errors - run: | - files=$(git diff --name-only --diff-filter=d $GITHUB_BASE_REF) - [[ -d .github/helper/semgrep_rules ]] && semgrep --severity ERROR --config=.github/helper/semgrep_rules --quiet --error $files - semgrep --config="r/python.lang.correctness" --quiet --error $files - - - name: Semgrep warnings - run: | - files=$(git diff --name-only --diff-filter=d $GITHUB_BASE_REF) - [[ -d .github/helper/semgrep_rules ]] && semgrep --severity WARNING --severity INFO --config=.github/helper/semgrep_rules --quiet $files + - uses: actions/checkout@v2 + - uses: returntocorp/semgrep-action@v1 + env: + SEMGREP_TIMEOUT: 120 + with: + config: >- + r/python.lang.correctness + .github/helper/semgrep_rules diff --git a/.github/workflows/server-tests.yml b/.github/workflows/server-tests.yml index 92685e2177d..a008b638c3f 100644 --- a/.github/workflows/server-tests.yml +++ b/.github/workflows/server-tests.yml @@ -1,6 +1,16 @@ name: Server -on: [pull_request, workflow_dispatch] +on: + pull_request: + paths-ignore: + - '**.js' + - '**.md' + workflow_dispatch: + push: + branches: [ develop ] + paths-ignore: + - '**.js' + - '**.md' jobs: test: @@ -32,6 +42,12 @@ jobs: with: python-version: 3.7 + - name: Setup Node + uses: actions/setup-node@v2 + with: + node-version: 12 + 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/ui-tests.yml b/.github/workflows/ui-tests.yml new file mode 100644 index 00000000000..cc3c5ceb7d1 --- /dev/null +++ b/.github/workflows/ui-tests.yml @@ -0,0 +1,110 @@ +name: UI + +on: + pull_request: + paths-ignore: + - '**.md' + workflow_dispatch: + +jobs: + test: + runs-on: ubuntu-18.04 + + strategy: + fail-fast: false + + name: UI Tests (Cypress) + + services: + mysql: + image: mariadb:10.3 + env: + MYSQL_ALLOW_EMPTY_PASSWORD: YES + ports: + - 3306:3306 + options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 + + steps: + - name: Clone + uses: actions/checkout@v2 + + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: 3.7 + + - 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 + + - name: Cache pip + uses: actions/cache@v2 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + ${{ runner.os }}- + + - name: Cache node modules + uses: actions/cache@v2 + env: + cache-name: cache-node-modules + with: + path: ~/.npm + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-build-${{ env.cache-name }}- + ${{ runner.os }}-build- + ${{ runner.os }}- + + - name: Get yarn cache directory path + id: yarn-cache-dir-path + run: echo "::set-output name=dir::$(yarn cache dir)" + + - uses: actions/cache@v2 + id: yarn-cache + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + + - name: Cache cypress binary + uses: actions/cache@v2 + with: + path: ~/.cache + key: ${{ runner.os }}-cypress- + restore-keys: | + ${{ runner.os }}-cypress- + ${{ runner.os }}- + + - name: Install + run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh + env: + DB: mariadb + TYPE: ui + + - name: Site Setup + 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 @testing-library/cypress@^8 --no-lockfile + + + - name: Build Assets + run: cd ~/frappe-bench/ && bench build + + - name: UI Tests + run: cd ~/frappe-bench/ && bench --site test_site run-ui-tests erpnext --headless + env: + CYPRESS_RECORD_KEY: 60a8e3bf-08f5-45b1-9269-2b207d7d30cd + + - name: Show bench console if tests failed + if: ${{ failure() }} + run: cat ~/frappe-bench/bench_run_logs.txt diff --git a/CODEOWNERS b/CODEOWNERS index 7cf65a7a732..a4a14de1b8e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -3,16 +3,33 @@ # These owners will be the default owners for everything in # the repo. Unless a later match takes precedence, -manufacturing/ @rohitwaghchaure @marination -accounts/ @deepeshgarg007 @nextchamp-saqib -loan_management/ @deepeshgarg007 @rohitwaghchaure -pos* @nextchamp-saqib @rohitwaghchaure -assets/ @nextchamp-saqib @deepeshgarg007 -stock/ @marination @rohitwaghchaure -buying/ @marination @deepeshgarg007 -hr/ @Anurag810 @rohitwaghchaure -projects/ @hrwX @nextchamp-saqib -support/ @hrwX @marination -healthcare/ @ruchamahabal @marination -erpnext_integrations/ @Mangesh-Khairnar @nextchamp-saqib -requirements.txt @gavindsouza +erpnext/accounts/ @nextchamp-saqib @deepeshgarg007 +erpnext/assets/ @nextchamp-saqib @deepeshgarg007 +erpnext/erpnext_integrations/ @nextchamp-saqib +erpnext/loan_management/ @nextchamp-saqib @deepeshgarg007 +erpnext/regional @nextchamp-saqib @deepeshgarg007 +erpnext/selling @nextchamp-saqib @deepeshgarg007 +erpnext/support/ @nextchamp-saqib @deepeshgarg007 +pos* @nextchamp-saqib + +erpnext/buying/ @marination @rohitwaghchaure @ankush +erpnext/e_commerce/ @marination +erpnext/maintenance/ @marination @rohitwaghchaure +erpnext/manufacturing/ @marination @rohitwaghchaure @ankush +erpnext/portal/ @marination +erpnext/quality_management/ @marination @rohitwaghchaure +erpnext/shopping_cart/ @marination +erpnext/stock/ @marination @rohitwaghchaure @ankush + +erpnext/crm/ @ruchamahabal @pateljannat +erpnext/education/ @ruchamahabal @pateljannat +erpnext/healthcare/ @ruchamahabal @pateljannat @chillaranand +erpnext/hr/ @ruchamahabal @pateljannat +erpnext/non_profit/ @ruchamahabal +erpnext/payroll @ruchamahabal @pateljannat +erpnext/projects/ @ruchamahabal @pateljannat + +erpnext/controllers @deepeshgarg007 @nextchamp-saqib @rohitwaghchaure @marination + +.github/ @surajshetty3416 @ankush +requirements.txt @gavindsouza diff --git a/cypress.json b/cypress.json new file mode 100644 index 00000000000..afcd657c53d --- /dev/null +++ b/cypress.json @@ -0,0 +1,11 @@ +{ + "baseUrl": "http://test_site:8000", + "projectId": "da59y9", + "adminPassword": "admin", + "defaultCommandTimeout": 20000, + "pageLoadTimeout": 15000, + "retries": { + "runMode": 2, + "openMode": 2 + } +} \ No newline at end of file diff --git a/cypress/fixtures/example.json b/cypress/fixtures/example.json new file mode 100644 index 00000000000..da18d9352a1 --- /dev/null +++ b/cypress/fixtures/example.json @@ -0,0 +1,5 @@ +{ + "name": "Using fixtures to represent data", + "email": "hello@cypress.io", + "body": "Fixtures are a great way to mock data for responses to routes" +} \ No newline at end of file diff --git a/cypress/integration/test_customer.js b/cypress/integration/test_customer.js new file mode 100644 index 00000000000..3d6ed5d0d89 --- /dev/null +++ b/cypress/integration/test_customer.js @@ -0,0 +1,13 @@ + +context('Customer', () => { + before(() => { + cy.login(); + }); + it('Check Customer Group', () => { + cy.visit(`app/customer/`); + cy.get('.primary-action').click(); + cy.wait(500); + cy.get('.custom-actions > .btn').click(); + cy.get_field('customer_group', 'Link').should('have.value', 'All Customer Groups'); + }); +}); diff --git a/cypress/integration/test_organizational_chart_desktop.js b/cypress/integration/test_organizational_chart_desktop.js new file mode 100644 index 00000000000..39b00d32635 --- /dev/null +++ b/cypress/integration/test_organizational_chart_desktop.js @@ -0,0 +1,116 @@ +context('Organizational Chart', () => { + before(() => { + cy.login(); + cy.visit('/app/website'); + }); + + it('navigates to org chart', () => { + 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({ + url: `/api/method/erpnext.tests.ui_test_helpers.create_employee_records`, + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + 'X-Frappe-CSRF-Token': csrf_token + }, + timeout: 60000 + }).then(res => { + expect(res.status).eq(200); + cy.get('.frappe-control[data-fieldname=company] input').focus().as('input'); + cy.get('@input') + .clear({ force: true }) + .type('Test Org Chart{enter}', { force: true }) + .blur({ force: true }); + }); + }); + }); + + it('renders root nodes and loads children for the first expandable node', () => { + // check rendered root nodes and the node name, title, connections + cy.get('.hierarchy').find('.root-level ul.node-children').children() + .should('have.length', 2) + .first() + .as('first-child'); + + cy.get('@first-child').get('.node-name').contains('Test Employee 1'); + cy.get('@first-child').get('.node-info').find('.node-title').contains('CEO'); + cy.get('@first-child').get('.node-info').find('.node-connections').contains('· 2 Connections'); + + cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => { + // children of 1st root visible + cy.get(`div[data-parent="${employee_records.message[0]}"]`).as('child-node'); + cy.get('@child-node') + .should('have.length', 1) + .should('be.visible'); + cy.get('@child-node').get('.node-name').contains('Test Employee 3'); + + // connectors between first root node and immediate child + cy.get(`path[data-parent="${employee_records.message[0]}"]`) + .should('be.visible') + .invoke('attr', 'data-child') + .should('equal', employee_records.message[2]); + }); + }); + + it('hides active nodes children and connectors on expanding sibling node', () => { + cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => { + // click sibling + cy.get(`#${employee_records.message[1]}`) + .click() + .should('have.class', 'active'); + + // child nodes and connectors hidden + cy.get(`[data-parent="${employee_records.message[0]}"]`).should('not.be.visible'); + cy.get(`path[data-parent="${employee_records.message[0]}"]`).should('not.be.visible'); + }); + }); + + it('collapses previous level nodes and refreshes connectors on expanding child node', () => { + cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => { + // click child node + cy.get(`#${employee_records.message[3]}`) + .click() + .should('have.class', 'active'); + + // previous level nodes: parent should be on active-path; other nodes should be collapsed + cy.get(`#${employee_records.message[0]}`).should('have.class', 'collapsed'); + cy.get(`#${employee_records.message[1]}`).should('have.class', 'active-path'); + + // previous level connectors refreshed + cy.get(`path[data-parent="${employee_records.message[1]}"]`) + .should('have.class', 'collapsed-connector'); + + // child node's children and connectors rendered + cy.get(`[data-parent="${employee_records.message[3]}"]`).should('be.visible'); + cy.get(`path[data-parent="${employee_records.message[3]}"]`).should('be.visible'); + }); + }); + + it('expands previous level nodes', () => { + cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => { + cy.get(`#${employee_records.message[0]}`) + .click() + .should('have.class', 'active'); + + cy.get(`[data-parent="${employee_records.message[0]}"]`) + .should('be.visible'); + + cy.get('ul.hierarchy').children().should('have.length', 2); + cy.get(`#connectors`).children().should('have.length', 1); + }); + }); + + it('edit node navigates to employee master', () => { + cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => { + cy.get(`#${employee_records.message[0]}`).find('.btn-edit-node') + .click(); + + cy.url().should('include', `/employee/${employee_records.message[0]}`); + }); + }); +}); diff --git a/cypress/integration/test_organizational_chart_mobile.js b/cypress/integration/test_organizational_chart_mobile.js new file mode 100644 index 00000000000..6e751513967 --- /dev/null +++ b/cypress/integration/test_organizational_chart_mobile.js @@ -0,0 +1,195 @@ +context('Organizational Chart Mobile', () => { + before(() => { + cy.login(); + 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({ + url: `/api/method/erpnext.tests.ui_test_helpers.create_employee_records`, + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + 'X-Frappe-CSRF-Token': csrf_token + }, + timeout: 60000 + }).then(res => { + expect(res.status).eq(200); + cy.get('.frappe-control[data-fieldname=company] input').focus().as('input'); + cy.get('@input') + .clear({ force: true }) + .type('Test Org Chart{enter}', { force: true }) + .blur({ force: true }); + }); + }); + }); + + it('renders root nodes', () => { + // check rendered root nodes and the node name, title, connections + cy.get('.hierarchy-mobile').find('.root-level').children() + .should('have.length', 2) + .first() + .as('first-child'); + + cy.get('@first-child').get('.node-name').contains('Test Employee 1'); + cy.get('@first-child').get('.node-info').find('.node-title').contains('CEO'); + cy.get('@first-child').get('.node-info').find('.node-connections').contains('· 2'); + }); + + it('expands root node', () => { + cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => { + cy.get(`#${employee_records.message[1]}`) + .click() + .should('have.class', 'active'); + + // other root node removed + cy.get(`#${employee_records.message[0]}`).should('not.exist'); + + // children of active root node + cy.get('.hierarchy-mobile').find('.level').first().find('ul.node-children').children() + .should('have.length', 2); + + cy.get(`div[data-parent="${employee_records.message[1]}"]`).first().as('child-node'); + cy.get('@child-node').should('be.visible'); + + cy.get('@child-node') + .get('.node-name') + .contains('Test Employee 4'); + + // connectors between root node and immediate children + cy.get(`path[data-parent="${employee_records.message[1]}"]`).as('connectors'); + cy.get('@connectors') + .should('have.length', 2) + .should('be.visible'); + + cy.get('@connectors') + .first() + .invoke('attr', 'data-child') + .should('eq', employee_records.message[3]); + }); + }); + + it('expands child node', () => { + cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => { + cy.get(`#${employee_records.message[3]}`) + .click() + .should('have.class', 'active') + .as('expanded_node'); + + // 2 levels on screen; 1 on active path; 1 collapsed + cy.get('.hierarchy-mobile').children().should('have.length', 2); + cy.get(`#${employee_records.message[1]}`).should('have.class', 'active-path'); + + // children of expanded node visible + cy.get('@expanded_node') + .next() + .should('have.class', 'node-children') + .as('node-children'); + + cy.get('@node-children').children().should('have.length', 1); + cy.get('@node-children') + .first() + .get('.node-card') + .should('have.class', 'active-child') + .contains('Test Employee 7'); + + // orphan connectors removed + cy.get(`#connectors`).children().should('have.length', 2); + }); + }); + + it('renders sibling group', () => { + cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => { + // sibling group visible for parent + cy.get(`#${employee_records.message[1]}`) + .next() + .as('sibling_group'); + + cy.get('@sibling_group') + .should('have.attr', 'data-parent', 'undefined') + .should('have.class', 'node-group') + .and('have.class', 'collapsed'); + + cy.get('@sibling_group').get('.avatar-group').children().as('siblings'); + cy.get('@siblings').should('have.length', 1); + cy.get('@siblings') + .first() + .should('have.attr', 'title', 'Test Employee 1'); + + }); + }); + + it('expands previous level nodes', () => { + cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => { + cy.get(`#${employee_records.message[6]}`) + .click() + .should('have.class', 'active'); + + // clicking on previous level node should remove all the nodes ahead + // and expand that node + cy.get(`#${employee_records.message[3]}`).click(); + cy.get(`#${employee_records.message[3]}`) + .should('have.class', 'active') + .should('not.have.class', 'active-path'); + + cy.get(`#${employee_records.message[6]}`).should('have.class', 'active-child'); + cy.get('.hierarchy-mobile').children().should('have.length', 2); + cy.get(`#connectors`).children().should('have.length', 2); + }); + }); + + it('expands sibling group', () => { + cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => { + // sibling group visible for parent + cy.get(`#${employee_records.message[6]}`).click(); + + cy.get(`#${employee_records.message[3]}`) + .next() + .click(); + + // siblings of parent should be visible + cy.get('.hierarchy-mobile').prev().as('sibling_group'); + cy.get('@sibling_group') + .should('exist') + .should('have.class', 'sibling-group') + .should('not.have.class', 'collapsed'); + + cy.get(`#${employee_records.message[1]}`) + .should('be.visible') + .should('have.class', 'active'); + + cy.get(`[data-parent="${employee_records.message[1]}"]`) + .should('be.visible') + .should('have.length', 2) + .should('have.class', 'active-child'); + }); + }); + + it('goes to the respective level after clicking on non-collapsed sibling group', () => { + cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(() => { + // click on non-collapsed sibling group + cy.get('.hierarchy-mobile') + .prev() + .click(); + + // should take you to that level + cy.get('.hierarchy-mobile').find('li.level .node-card').should('have.length', 2); + }); + }); + + it('edit node navigates to employee master', () => { + cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => { + cy.get(`#${employee_records.message[0]}`).find('.btn-edit-node') + .click(); + + cy.url().should('include', `/employee/${employee_records.message[0]}`); + }); + }); +}); diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js new file mode 100644 index 00000000000..07d9804a733 --- /dev/null +++ b/cypress/plugins/index.js @@ -0,0 +1,17 @@ +// *********************************************************** +// This example plugins/index.js can be used to load plugins +// +// You can change the location of this file or turn off loading +// the plugins file with the 'pluginsFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/plugins-guide +// *********************************************************** + +// This function is called when a project is opened or re-opened (e.g. due to +// the project's config changing) + +module.exports = () => { + // `on` is used to hook into various events Cypress emits + // `config` is the resolved Cypress config +}; diff --git a/cypress/support/commands.js b/cypress/support/commands.js new file mode 100644 index 00000000000..7ddc80ab8dd --- /dev/null +++ b/cypress/support/commands.js @@ -0,0 +1,31 @@ +// *********************************************** +// This example commands.js shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** +// +// +// -- This is a parent command -- +// Cypress.Commands.add("login", (email, password) => { ... }); +// +// +// -- This is a child command -- +// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }); +// +// +// -- This is a dual command -- +// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }); +// +// +// -- This is will overwrite an existing command -- +// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }); + +const slug = (name) => name.toLowerCase().replace(" ", "-"); + +Cypress.Commands.add("go_to_doc", (doctype, name) => { + cy.visit(`/app/${slug(doctype)}/${encodeURIComponent(name)}`); +}); diff --git a/cypress/support/index.js b/cypress/support/index.js new file mode 100644 index 00000000000..72070cc81c4 --- /dev/null +++ b/cypress/support/index.js @@ -0,0 +1,26 @@ +// *********************************************************** +// This example support/index.js is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import './commands'; +import '../../../frappe/cypress/support/commands' // eslint-disable-line + + +// Alternatively you can use CommonJS syntax: +// require('./commands') + +Cypress.Cookies.defaults({ + preserve: 'sid' +}); diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json new file mode 100644 index 00000000000..d90ebf6856d --- /dev/null +++ b/cypress/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "allowJs": true, + "baseUrl": "../node_modules", + "types": [ + "cypress" + ] + }, + "include": [ + "**/*.*" + ] +} \ No newline at end of file diff --git a/erpnext/.stylelintrc b/erpnext/.stylelintrc index 1e05d1fb41d..30075f13d04 100644 --- a/erpnext/.stylelintrc +++ b/erpnext/.stylelintrc @@ -6,4 +6,4 @@ "scss/at-rule-no-unknown": true, "no-descending-specificity": null } -} \ No newline at end of file +} diff --git a/erpnext/accounts/custom/address.py b/erpnext/accounts/custom/address.py index c417a493c69..834227bb586 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/dashboard_chart_source/account_balance_timeline/account_balance_timeline.js b/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.js index e12eae9c1c1..d8a83e53dc0 100644 --- a/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.js +++ b/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.js @@ -19,4 +19,4 @@ frappe.dashboards.chart_sources["Account Balance Timeline"] = { reqd: 1 }, ] -}; \ No newline at end of file +}; diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py index 335e8a15ab0..0c81d83ed8e 100644 --- a/erpnext/accounts/deferred_revenue.py +++ b/erpnext/accounts/deferred_revenue.py @@ -450,5 +450,3 @@ def get_deferred_booking_accounts(doctype, voucher_detail_no, dr_or_cr): return debit_account else: return credit_account - - diff --git a/erpnext/accounts/doctype/account/account.js b/erpnext/accounts/doctype/account/account.js index f7f1a5fb15a..7a1d7359488 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/account/account.py b/erpnext/accounts/doctype/account/account.py index 1be2fbf5c81..f763df0852b 100644 --- a/erpnext/accounts/doctype/account/account.py +++ b/erpnext/accounts/doctype/account/account.py @@ -230,7 +230,7 @@ class Account(NestedSet): if self.check_gle_exists(): throw(_("Account with existing transaction can not be converted to group.")) elif self.account_type and not self.flags.exclude_account_type_check: - throw(_("Cannot covert to Group because Account Type is selected.")) + throw(_("Cannot convert to Group because Account Type is selected.")) else: self.is_group = 1 self.save() diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.js b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.js index 65c5ff1ceaf..2fa1d53c60c 100644 --- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.js +++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.js @@ -60,4 +60,4 @@ frappe.ui.form.on('Accounting Dimension Detail', { let row = locals[cdt][cdn]; row.reference_document = frm.doc.document_type; } -}); \ No newline at end of file +}); diff --git a/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py index e657a9ae34b..4f3ee7643ab 100644 --- a/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py +++ b/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py @@ -113,5 +113,3 @@ def disable_dimension(): dimension2 = frappe.get_doc("Accounting Dimension", "Location") dimension2.disabled = 1 dimension2.save() - - diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js index 74b7b516763..9dd882a3119 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js +++ b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js @@ -79,4 +79,4 @@ frappe.ui.form.on('Allowed Dimension', { row.accounting_dimension = frm.doc.accounting_dimension; frm.refresh_field("dimensions"); } -}); \ No newline at end of file +}); diff --git a/erpnext/accounts/doctype/accounting_period/accounting_period.py b/erpnext/accounts/doctype/accounting_period/accounting_period.py index 63b5dbbd3e6..739d8f6bc63 100644 --- a/erpnext/accounts/doctype/accounting_period/accounting_period.py +++ b/erpnext/accounts/doctype/accounting_period/accounting_period.py @@ -56,4 +56,4 @@ class AccountingPeriod(Document): self.append('closed_documents', { "document_type": doctype_for_closing.document_type, "closed": doctype_for_closing.closed - }) \ No newline at end of file + }) 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 71ce5b8d04f..00000000000 --- 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 541901c9abf..e44af3a9167 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.js +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.js @@ -48,4 +48,4 @@ frappe.tour['Accounts Settings'] = [ title: "Unlink Advance Payment on Cancellation of Order", description: __("Similar to the previous option, this unlinks any advance payments made against Purchase/Sales Orders.") } -]; \ No newline at end of file +]; diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index 703e93c0757..935e29a9d33 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -18,7 +18,9 @@ "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", "determine_address_tax_category_from", "column_break_19", @@ -261,6 +263,19 @@ "fieldname": "post_change_gl_entries", "fieldtype": "Check", "label": "Create Ledger Entries for Change Amount" + }, + { + "default": "0", + "description": "If enabled, additional ledger entries will be made for discounts in a separate Discount Account", + "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", @@ -268,7 +283,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2021-06-17 20:26:03.721202", + "modified": "2021-08-19 11:17:38.788054", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Settings", diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py index ac4a2d6f16d..62c97f24d5f 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py @@ -21,6 +21,7 @@ class AccountsSettings(Document): self.validate_stale_days() self.enable_payment_schedule_in_print() + self.toggle_discount_accounting_fields() def validate_stale_days(self): if not self.allow_stale and cint(self.stale_days) <= 0: @@ -33,3 +34,22 @@ class AccountsSettings(Document): for doctype in ("Sales Order", "Sales Invoice", "Purchase Order", "Purchase Invoice"): make_property_setter(doctype, "due_date", "print_hide", show_in_print, "Check", validate_fields_for_doctype=False) make_property_setter(doctype, "payment_schedule", "print_hide", 0 if show_in_print else 1, "Check", validate_fields_for_doctype=False) + + def toggle_discount_accounting_fields(self): + enable_discount_accounting = cint(self.enable_discount_accounting) + + for doctype in ["Sales Invoice Item", "Purchase Invoice Item"]: + make_property_setter(doctype, "discount_account", "hidden", not(enable_discount_accounting), "Check", validate_fields_for_doctype=False) + if enable_discount_accounting: + make_property_setter(doctype, "discount_account", "mandatory_depends_on", "eval: doc.discount_amount", "Code", validate_fields_for_doctype=False) + else: + make_property_setter(doctype, "discount_account", "mandatory_depends_on", "", "Code", validate_fields_for_doctype=False) + + for doctype in ["Sales Invoice", "Purchase Invoice"]: + make_property_setter(doctype, "additional_discount_account", "hidden", not(enable_discount_accounting), "Check", validate_fields_for_doctype=False) + if enable_discount_accounting: + make_property_setter(doctype, "additional_discount_account", "mandatory_depends_on", "eval: doc.discount_amount", "Code", validate_fields_for_doctype=False) + else: + make_property_setter(doctype, "additional_discount_account", "mandatory_depends_on", "", "Code", validate_fields_for_doctype=False) + + make_property_setter("Item", "default_discount_account", "hidden", not(enable_discount_accounting), "Check", validate_fields_for_doctype=False) diff --git a/erpnext/accounts/doctype/accounts_settings/regional/united_states.js b/erpnext/accounts/doctype/accounts_settings/regional/united_states.js index d47d6e58039..3e38386481c 100644 --- a/erpnext/accounts/doctype/accounts_settings/regional/united_states.js +++ b/erpnext/accounts/doctype/accounts_settings/regional/united_states.js @@ -5,4 +5,4 @@ frappe.ui.form.on('Accounts Settings', { frm.set_df_property("frozen_accounts_modifier", "label", "Role Allowed to Close Books & Make Changes to Closed Periods"); frm.set_df_property("credit_controller", "label", "Credit Manager"); } -}); \ No newline at end of file +}); diff --git a/erpnext/accounts/doctype/bank/bank.js b/erpnext/accounts/doctype/bank/bank.js index 19041a3f73d..059e1d31588 100644 --- a/erpnext/accounts/doctype/bank/bank.js +++ b/erpnext/accounts/doctype/bank/bank.js @@ -120,4 +120,4 @@ erpnext.integrations.refreshPlaidLink = class refreshPlaidLink { plaid_success(token, response) { frappe.show_alert({ message: __('Plaid Link Updated'), indicator: 'green' }); } -}; \ No newline at end of file +}; diff --git a/erpnext/accounts/doctype/bank/bank.py b/erpnext/accounts/doctype/bank/bank.py index 41aae14362f..99fa21c8f9a 100644 --- a/erpnext/accounts/doctype/bank/bank.py +++ b/erpnext/accounts/doctype/bank/bank.py @@ -13,4 +13,4 @@ class Bank(Document): load_address_and_contact(self) def on_trash(self): - delete_contact_and_address('Bank', self.name) \ No newline at end of file + delete_contact_and_address('Bank', self.name) diff --git a/erpnext/accounts/doctype/bank/test_bank.js b/erpnext/accounts/doctype/bank/test_bank.js deleted file mode 100644 index 9ec264415a6..00000000000 --- 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/bank_account_dashboard.py b/erpnext/accounts/doctype/bank_account/bank_account_dashboard.py index a959cea98f2..c7ea1522993 100644 --- a/erpnext/accounts/doctype/bank_account/bank_account_dashboard.py +++ b/erpnext/accounts/doctype/bank_account/bank_account_dashboard.py @@ -26,4 +26,4 @@ def get_data(): 'items': ['Journal Entry'] } ] - } \ No newline at end of file + } 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 c20a7990e8c..00000000000 --- 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 f59999845af..00000000000 --- 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_clearance/bank_clearance.js b/erpnext/accounts/doctype/bank_clearance/bank_clearance.js index ba3f2face63..63cc46518ff 100644 --- a/erpnext/accounts/doctype/bank_clearance/bank_clearance.js +++ b/erpnext/accounts/doctype/bank_clearance/bank_clearance.js @@ -8,7 +8,7 @@ frappe.ui.form.on("Bank Clearance", { onload: function(frm) { - let default_bank_account = frappe.defaults.get_user_default("Company")? + let default_bank_account = frappe.defaults.get_user_default("Company")? locals[":Company"][frappe.defaults.get_user_default("Company")]["default_bank_account"]: ""; frm.set_value("account", default_bank_account); diff --git a/erpnext/accounts/doctype/bank_clearance_detail/bank_clearance_detail.py b/erpnext/accounts/doctype/bank_clearance_detail/bank_clearance_detail.py index ecc536733f2..59299f81e50 100644 --- a/erpnext/accounts/doctype/bank_clearance_detail/bank_clearance_detail.py +++ b/erpnext/accounts/doctype/bank_clearance_detail/bank_clearance_detail.py @@ -6,4 +6,4 @@ import frappe from frappe.model.document import Document class BankClearanceDetail(Document): - pass \ No newline at end of file + pass diff --git a/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.py b/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.py index 88e1055beb4..a0aac6ab170 100644 --- a/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.py +++ b/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.py @@ -25,6 +25,6 @@ class BankGuarantee(Document): def get_vouchar_detials(column_list, doctype, docname): column_list = json.loads(column_list) for col in column_list: - sanitize_searchfield(col) + sanitize_searchfield(col) return frappe.db.sql(''' select {columns} from `tab{doctype}` where name=%s''' .format(columns=", ".join(column_list), doctype=doctype), docname, as_dict=1)[0] 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 0c60920cf98..00000000000 --- 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/bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py index 5246baa02b3..7ea71fc103a 100644 --- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py +++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py @@ -21,6 +21,10 @@ class BankTransaction(StatusUpdater): self.update_allocations() self.clear_linked_payment_entries() self.set_status(update=True) + + def on_cancel(self): + self.clear_linked_payment_entries(for_cancel=True) + self.set_status(update=True) def update_allocations(self): if self.payment_entries: @@ -41,21 +45,46 @@ class BankTransaction(StatusUpdater): frappe.db.set_value(self.doctype, self.name, "status", "Reconciled") self.reload() - - def clear_linked_payment_entries(self): + + def clear_linked_payment_entries(self, for_cancel=False): for payment_entry in self.payment_entries: if payment_entry.payment_document in ["Payment Entry", "Journal Entry", "Purchase Invoice", "Expense Claim"]: - self.clear_simple_entry(payment_entry) + self.clear_simple_entry(payment_entry, for_cancel=for_cancel) elif payment_entry.payment_document == "Sales Invoice": - self.clear_sales_invoice(payment_entry) + self.clear_sales_invoice(payment_entry, for_cancel=for_cancel) - def clear_simple_entry(self, payment_entry): - frappe.db.set_value(payment_entry.payment_document, payment_entry.payment_entry, "clearance_date", self.date) + def clear_simple_entry(self, payment_entry, for_cancel=False): + if payment_entry.payment_document == "Payment Entry": + if frappe.db.get_value("Payment Entry", payment_entry.payment_entry, "payment_type") == "Internal Transfer": + if len(get_reconciled_bank_transactions(payment_entry)) < 2: + return - def clear_sales_invoice(self, payment_entry): - frappe.db.set_value("Sales Invoice Payment", dict(parenttype=payment_entry.payment_document, - parent=payment_entry.payment_entry), "clearance_date", self.date) + clearance_date = self.date if not for_cancel else None + frappe.db.set_value( + payment_entry.payment_document, payment_entry.payment_entry, + "clearance_date", clearance_date) + + def clear_sales_invoice(self, payment_entry, for_cancel=False): + clearance_date = self.date if not for_cancel else None + frappe.db.set_value( + "Sales Invoice Payment", + dict( + parenttype=payment_entry.payment_document, + parent=payment_entry.payment_entry + ), + "clearance_date", clearance_date) + +def get_reconciled_bank_transactions(payment_entry): + reconciled_bank_transactions = frappe.get_all( + 'Bank Transaction Payments', + filters = { + 'payment_entry': payment_entry.payment_entry + }, + fields = ['parent'] + ) + + return reconciled_bank_transactions def get_total_allocated_amount(payment_entry): return frappe.db.sql(""" @@ -105,4 +134,3 @@ def unclear_reference_payment(doctype, docname): frappe.db.set_value(doc.payment_document, doc.payment_entry, "clearance_date", None) return doc.payment_entry - diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction_list.js b/erpnext/accounts/doctype/bank_transaction/bank_transaction_list.js index 2ecc2b0cda3..2585ee9c923 100644 --- a/erpnext/accounts/doctype/bank_transaction/bank_transaction_list.js +++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction_list.js @@ -4,10 +4,12 @@ frappe.listview_settings['Bank Transaction'] = { add_fields: ["unallocated_amount"], get_indicator: function(doc) { - if(flt(doc.unallocated_amount)>0) { - return [__("Unreconciled"), "orange", "unallocated_amount,>,0"]; + if(doc.docstatus == 2) { + return [__("Cancelled"), "red", "docstatus,=,2"]; } else if(flt(doc.unallocated_amount)<=0) { return [__("Reconciled"), "green", "unallocated_amount,=,0"]; + } else if(flt(doc.unallocated_amount)>0) { + return [__("Unreconciled"), "orange", "unallocated_amount,>,0"]; } } -}; \ No newline at end of file +}; diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction_upload.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction_upload.py index 33ae45439e7..dc3b8674700 100644 --- a/erpnext/accounts/doctype/bank_transaction/bank_transaction_upload.py +++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction_upload.py @@ -77,4 +77,4 @@ def get_bank_mapping(bank_account): mapping = {row.file_field:row.bank_transaction_field for row in bank.bank_transaction_mapping} - return mapping \ No newline at end of file + return mapping 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 305119e1370..00000000000 --- 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/bank_transaction/test_bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py index ce149f96e6f..439d4891194 100644 --- a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py +++ b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py @@ -25,7 +25,8 @@ class TestBankTransaction(unittest.TestCase): def tearDownClass(cls): for bt in frappe.get_all("Bank Transaction"): doc = frappe.get_doc("Bank Transaction", bt.name) - doc.cancel() + if doc.docstatus == 1: + doc.cancel() doc.delete() # Delete directly in DB to avoid validation errors for countries not allowing deletion @@ -57,6 +58,12 @@ class TestBankTransaction(unittest.TestCase): clearance_date = frappe.db.get_value("Payment Entry", payment.name, "clearance_date") self.assertTrue(clearance_date is not None) + bank_transaction.reload() + bank_transaction.cancel() + + clearance_date = frappe.db.get_value("Payment Entry", payment.name, "clearance_date") + self.assertFalse(clearance_date) + # Check if ERPNext can correctly filter a linked payments based on the debit/credit amount def test_debit_credit_output(self): bank_transaction = frappe.get_doc("Bank Transaction", dict(description="Auszahlung Karte MC/000002916 AUTOMAT 698769 K002 27.10. 14:07")) diff --git a/erpnext/accounts/doctype/budget/test_budget.py b/erpnext/accounts/doctype/budget/test_budget.py index 603e21ea248..6c25f0024d5 100644 --- a/erpnext/accounts/doctype/budget/test_budget.py +++ b/erpnext/accounts/doctype/budget/test_budget.py @@ -249,7 +249,7 @@ class TestBudget(unittest.TestCase): def set_total_expense_zero(posting_date, budget_against_field=None, budget_against_CC=None): if budget_against_field == "project": - budget_against = "_Test Project" + budget_against = frappe.db.get_value("Project", {"project_name": "_Test Project"}) else: budget_against = budget_against_CC or "_Test Cost Center - _TC" @@ -275,7 +275,7 @@ def set_total_expense_zero(posting_date, budget_against_field=None, budget_again "_Test Bank - _TC", -existing_expense, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True) elif budget_against_field == "project": make_journal_entry("_Test Account Cost for Goods Sold - _TC", - "_Test Bank - _TC", -existing_expense, "_Test Cost Center - _TC", submit=True, project="_Test Project", posting_date=nowdate()) + "_Test Bank - _TC", -existing_expense, "_Test Cost Center - _TC", submit=True, project=budget_against, posting_date=nowdate()) def make_budget(**args): args = frappe._dict(args) diff --git a/erpnext/accounts/doctype/c_form_invoice_detail/c_form_invoice_detail.py b/erpnext/accounts/doctype/c_form_invoice_detail/c_form_invoice_detail.py index ee5098bea12..20e423a610e 100644 --- a/erpnext/accounts/doctype/c_form_invoice_detail/c_form_invoice_detail.py +++ b/erpnext/accounts/doctype/c_form_invoice_detail/c_form_invoice_detail.py @@ -6,4 +6,4 @@ import frappe from frappe.model.document import Document class CFormInvoiceDetail(Document): - pass \ No newline at end of file + pass diff --git a/erpnext/accounts/doctype/campaign_item/__init__.py b/erpnext/accounts/doctype/campaign_item/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/accounts/doctype/campaign_item/campaign_item.json b/erpnext/accounts/doctype/campaign_item/campaign_item.json new file mode 100644 index 00000000000..69383a482b4 --- /dev/null +++ b/erpnext/accounts/doctype/campaign_item/campaign_item.json @@ -0,0 +1,31 @@ +{ + "actions": [], + "creation": "2021-05-06 16:18:25.410476", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "campaign" + ], + "fields": [ + { + "fieldname": "campaign", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Campaign", + "options": "Campaign" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-05-07 10:43:49.717633", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Campaign Item", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/campaign_item/campaign_item.py b/erpnext/accounts/doctype/campaign_item/campaign_item.py new file mode 100644 index 00000000000..4f5fd7f7d78 --- /dev/null +++ b/erpnext/accounts/doctype/campaign_item/campaign_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 CampaignItem(Document): + pass 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 12ca254c5ae..00000000000 --- 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/cash_flow_mapping.py b/erpnext/accounts/doctype/cash_flow_mapping/cash_flow_mapping.py index 28d84b4442f..b1ad2972beb 100644 --- a/erpnext/accounts/doctype/cash_flow_mapping/cash_flow_mapping.py +++ b/erpnext/accounts/doctype/cash_flow_mapping/cash_flow_mapping.py @@ -18,5 +18,3 @@ class CashFlowMapping(Document): frappe._('You can only select a maximum of one option from the list of check boxes.'), title='Error' ) - - 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 1970ca806dc..00000000000 --- 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 12546ce2e3a..00000000000 --- 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 eecabda751b..00000000000 --- 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/cashier_closing.py b/erpnext/accounts/doctype/cashier_closing/cashier_closing.py index 7ad1d3ab831..081c6fa4718 100644 --- a/erpnext/accounts/doctype/cashier_closing/cashier_closing.py +++ b/erpnext/accounts/doctype/cashier_closing/cashier_closing.py @@ -33,4 +33,4 @@ class CashierClosing(Document): def validate_time(self): if self.from_time >= self.time: - frappe.throw(_("From Time Should Be Less Than To Time")) \ No newline at end of file + frappe.throw(_("From Time Should Be Less Than To Time")) 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 a7fcc8d8421..00000000000 --- 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/chart_of_accounts_importer.py b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py index 3b764aab103..8456b49c8ee 100644 --- a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py +++ b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py @@ -13,7 +13,8 @@ from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import from frappe.utils.xlsxutils import read_xlsx_file_from_attached_file, read_xls_file_from_attached_file class ChartofAccountsImporter(Document): - pass + def validate(self): + validate_accounts(self.import_file) @frappe.whitelist() def validate_company(company): @@ -301,28 +302,27 @@ def validate_accounts(file_name): if account["parent_account"] and accounts_dict.get(account["parent_account"]): accounts_dict[account["parent_account"]]["is_group"] = 1 - message = validate_root(accounts_dict) - if message: return message - message = validate_account_types(accounts_dict) - if message: return message + validate_root(accounts_dict) + + validate_account_types(accounts_dict) return [True, len(accounts)] def validate_root(accounts): roots = [accounts[d] for d in accounts if not accounts[d].get('parent_account')] if len(roots) < 4: - return _("Number of root accounts cannot be less than 4") + frappe.throw(_("Number of root accounts cannot be less than 4")) error_messages = [] for account in roots: if not account.get("root_type") and account.get("account_name"): - error_messages.append("Please enter Root Type for account- {0}".format(account.get("account_name"))) + error_messages.append(_("Please enter Root Type for account- {0}").format(account.get("account_name"))) elif account.get("root_type") not in get_root_types() and account.get("account_name"): - error_messages.append("Root Type for {0} must be one of the Asset, Liability, Income, Expense and Equity".format(account.get("account_name"))) + error_messages.append(_("Root Type for {0} must be one of the Asset, Liability, Income, Expense and Equity").format(account.get("account_name"))) if error_messages: - return "
".join(error_messages) + frappe.throw("
".join(error_messages)) def get_root_types(): return ('Asset', 'Liability', 'Expense', 'Income', 'Equity') @@ -356,7 +356,7 @@ def validate_account_types(accounts): missing = list(set(account_types_for_ledger) - set(account_types)) if missing: - return _("Please identify/create Account (Ledger) for type - {0}").format(' , '.join(missing)) + frappe.throw(_("Please identify/create Account (Ledger) for type - {0}").format(' , '.join(missing))) account_types_for_group = ["Bank", "Cash", "Stock"] # fix logic bug @@ -364,7 +364,7 @@ def validate_account_types(accounts): missing = list(set(account_types_for_group) - set(account_groups)) if missing: - return _("Please identify/create Account (Group) for type - {0}").format(' , '.join(missing)) + frappe.throw(_("Please identify/create Account (Group) for type - {0}").format(' , '.join(missing))) def unset_existing_data(company): linked = frappe.db.sql('''select fieldname from tabDocField @@ -391,5 +391,5 @@ def set_default_accounts(company): }) company.save() - install_country_fixtures(company.name) + install_country_fixtures(company.name, company.country) company.create_default_tax_template() 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 b075a015d60..00000000000 --- 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/cheque_print_template/cheque_print_template.js b/erpnext/accounts/doctype/cheque_print_template/cheque_print_template.js index 6a430eb02bf..d10c61858f1 100644 --- a/erpnext/accounts/doctype/cheque_print_template/cheque_print_template.js +++ b/erpnext/accounts/doctype/cheque_print_template/cheque_print_template.js @@ -10,10 +10,10 @@ frappe.ui.form.on('Cheque Print Template', { function() { erpnext.cheque_print.view_cheque_print(frm); }).addClass("btn-primary"); - + $(frm.fields_dict.cheque_print_preview.wrapper).empty() - - + + var template = '
\
Signatory Name \
\
'; - + $(frappe.render(template, frm.doc)).appendTo(frm.fields_dict.cheque_print_preview.wrapper) - + if (frm.doc.scanned_cheque) { $(frm.fields_dict.cheque_print_preview.wrapper).find("#cheque_preview").css('background-image', 'url(' + frm.doc.scanned_cheque + ')'); } diff --git a/erpnext/accounts/doctype/cost_center/cost_center.py b/erpnext/accounts/doctype/cost_center/cost_center.py index 8a5473f3a16..981fec308cc 100644 --- a/erpnext/accounts/doctype/cost_center/cost_center.py +++ b/erpnext/accounts/doctype/cost_center/cost_center.py @@ -129,4 +129,4 @@ def get_name_with_number(new_account, account_number): def check_if_distributed_cost_center_enabled(cost_center_list): value_list = frappe.get_list("Cost Center", {"name": ["in", cost_center_list]}, "enable_distributed_cost_center", as_list=1) - return next((True for x in value_list if x[0]), False) \ No newline at end of file + return next((True for x in value_list if x[0]), False) diff --git a/erpnext/accounts/doctype/cost_center/cost_center_dashboard.py b/erpnext/accounts/doctype/cost_center/cost_center_dashboard.py index 788ac8be83f..24cf3ea0689 100644 --- a/erpnext/accounts/doctype/cost_center/cost_center_dashboard.py +++ b/erpnext/accounts/doctype/cost_center/cost_center_dashboard.py @@ -12,4 +12,4 @@ def get_data(): 'items': ['Budget Variance Report', 'General Ledger'] } ] - } \ No newline at end of file + } diff --git a/erpnext/accounts/doctype/cost_center/cost_center_tree.js b/erpnext/accounts/doctype/cost_center/cost_center_tree.js index fde41233c4d..1d482c58f1a 100644 --- a/erpnext/accounts/doctype/cost_center/cost_center_tree.js +++ b/erpnext/accounts/doctype/cost_center/cost_center_tree.js @@ -51,4 +51,4 @@ frappe.treeview_settings["Cost Center"] = { } -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/cost_center/test_cost_center.py b/erpnext/accounts/doctype/cost_center/test_cost_center.py index b5fc7e3b497..7779ccefc20 100644 --- a/erpnext/accounts/doctype/cost_center/test_cost_center.py +++ b/erpnext/accounts/doctype/cost_center/test_cost_center.py @@ -62,6 +62,3 @@ def create_cost_center(**args): cc.is_group = args.is_group or 0 cc.parent_cost_center = args.parent_cost_center or "_Test Company - _TC" cc.insert() - - - diff --git a/erpnext/accounts/doctype/coupon_code/coupon_code.py b/erpnext/accounts/doctype/coupon_code/coupon_code.py index 55c119315e0..92a816d25e9 100644 --- a/erpnext/accounts/doctype/coupon_code/coupon_code.py +++ b/erpnext/accounts/doctype/coupon_code/coupon_code.py @@ -17,7 +17,7 @@ class CouponCode(Document): self.coupon_code =''.join(i for i in self.coupon_name if not i.isdigit())[0:8].upper() elif self.coupon_type == "Gift Card": self.coupon_code = frappe.generate_hash()[:10].upper() - + def validate(self): if self.coupon_type == "Gift Card": self.maximum_use = 1 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 460fedc97f9..00000000000 --- 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/coupon_code/test_coupon_code.py b/erpnext/accounts/doctype/coupon_code/test_coupon_code.py index 622bd33e20a..06987a8a4a5 100644 --- a/erpnext/accounts/doctype/coupon_code/test_coupon_code.py +++ b/erpnext/accounts/doctype/coupon_code/test_coupon_code.py @@ -57,7 +57,7 @@ def test_create_test_data(): }) item_price.insert() # create test item pricing rule - if not frappe.db.exists("Pricing Rule","_Test Pricing Rule for _Test Item"): + if not frappe.db.exists("Pricing Rule", {"title": "_Test Pricing Rule for _Test Item"}): item_pricing_rule = frappe.get_doc({ "doctype": "Pricing Rule", "title": "_Test Pricing Rule for _Test Item", @@ -86,14 +86,15 @@ def test_create_test_data(): sales_partner.insert() # create test item coupon code if not frappe.db.exists("Coupon Code", "SAVE30"): + pricing_rule = frappe.db.get_value("Pricing Rule", {"title": "_Test Pricing Rule for _Test Item"}, ['name']) coupon_code = frappe.get_doc({ - "doctype": "Coupon Code", - "coupon_name":"SAVE30", - "coupon_code":"SAVE30", - "pricing_rule": "_Test Pricing Rule for _Test Item", - "valid_from": "2014-01-01", - "maximum_use":1, - "used":0 + "doctype": "Coupon Code", + "coupon_name":"SAVE30", + "coupon_code":"SAVE30", + "pricing_rule": pricing_rule, + "valid_from": "2014-01-01", + "maximum_use":1, + "used":0 }) coupon_code.insert() @@ -102,7 +103,7 @@ class TestCouponCode(unittest.TestCase): test_create_test_data() def tearDown(self): - frappe.set_user("Administrator") + frappe.set_user("Administrator") def test_sales_order_with_coupon_code(self): frappe.db.set_value("Coupon Code", "SAVE30", "used", 0) @@ -123,6 +124,3 @@ class TestCouponCode(unittest.TestCase): so.submit() self.assertEqual(frappe.db.get_value("Coupon Code", "SAVE30", "used"), 1) - - - diff --git a/erpnext/accounts/doctype/customer_group_item/__init__.py b/erpnext/accounts/doctype/customer_group_item/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/accounts/doctype/customer_group_item/customer_group_item.json b/erpnext/accounts/doctype/customer_group_item/customer_group_item.json new file mode 100644 index 00000000000..bd1229d4e09 --- /dev/null +++ b/erpnext/accounts/doctype/customer_group_item/customer_group_item.json @@ -0,0 +1,31 @@ +{ + "actions": [], + "creation": "2021-05-06 16:12:42.558878", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "customer_group" + ], + "fields": [ + { + "fieldname": "customer_group", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Customer Group", + "options": "Customer Group" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-05-07 10:39:21.563506", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Customer Group Item", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/customer_group_item/customer_group_item.py b/erpnext/accounts/doctype/customer_group_item/customer_group_item.py new file mode 100644 index 00000000000..df782ac9e0c --- /dev/null +++ b/erpnext/accounts/doctype/customer_group_item/customer_group_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 CustomerGroupItem(Document): + pass diff --git a/erpnext/accounts/doctype/customer_item/__init__.py b/erpnext/accounts/doctype/customer_item/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/accounts/doctype/customer_item/customer_item.json b/erpnext/accounts/doctype/customer_item/customer_item.json new file mode 100644 index 00000000000..f3dac02f942 --- /dev/null +++ b/erpnext/accounts/doctype/customer_item/customer_item.json @@ -0,0 +1,31 @@ +{ + "actions": [], + "creation": "2021-05-05 14:04:54.266353", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "customer" + ], + "fields": [ + { + "fieldname": "customer", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Customer ", + "options": "Customer" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-05-06 10:02:32.967841", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Customer Item", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/customer_item/customer_item.py b/erpnext/accounts/doctype/customer_item/customer_item.py new file mode 100644 index 00000000000..a577145e4ee --- /dev/null +++ b/erpnext/accounts/doctype/customer_item/customer_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 CustomerItem(Document): + pass diff --git a/erpnext/accounts/doctype/discounted_invoice/discounted_invoice.py b/erpnext/accounts/doctype/discounted_invoice/discounted_invoice.py index 109737f7276..93dfcc14bda 100644 --- a/erpnext/accounts/doctype/discounted_invoice/discounted_invoice.py +++ b/erpnext/accounts/doctype/discounted_invoice/discounted_invoice.py @@ -7,4 +7,4 @@ from __future__ import unicode_literals from frappe.model.document import Document class DiscountedInvoice(Document): - pass \ No newline at end of file + pass diff --git a/erpnext/accounts/doctype/dunning/dunning.py b/erpnext/accounts/doctype/dunning/dunning.py index c6c689212b6..1ef512a4894 100644 --- a/erpnext/accounts/doctype/dunning/dunning.py +++ b/erpnext/accounts/doctype/dunning/dunning.py @@ -25,7 +25,7 @@ class Dunning(AccountsController): def validate_amount(self): amounts = calculate_interest_and_amount( - self.posting_date, self.outstanding_amount, self.rate_of_interest, self.dunning_fee, self.overdue_days) + self.outstanding_amount, self.rate_of_interest, self.dunning_fee, self.overdue_days) if self.interest_amount != amounts.get('interest_amount'): self.interest_amount = flt(amounts.get('interest_amount'), self.precision('interest_amount')) if self.dunning_amount != amounts.get('dunning_amount'): @@ -91,13 +91,13 @@ def resolve_dunning(doc, state): for dunning in dunnings: frappe.db.set_value("Dunning", dunning.name, "status", 'Resolved') -def calculate_interest_and_amount(posting_date, outstanding_amount, rate_of_interest, dunning_fee, overdue_days): +def calculate_interest_and_amount(outstanding_amount, rate_of_interest, dunning_fee, overdue_days): interest_amount = 0 - grand_total = 0 + grand_total = flt(outstanding_amount) + flt(dunning_fee) if rate_of_interest: interest_per_year = flt(outstanding_amount) * flt(rate_of_interest) / 100 interest_amount = (interest_per_year * cint(overdue_days)) / 365 - grand_total = flt(outstanding_amount) + flt(interest_amount) + flt(dunning_fee) + grand_total += flt(interest_amount) dunning_amount = flt(interest_amount) + flt(dunning_fee) return { 'interest_amount': interest_amount, diff --git a/erpnext/accounts/doctype/dunning/dunning_dashboard.py b/erpnext/accounts/doctype/dunning/dunning_dashboard.py index 19a73ddfa48..33c6ab080c9 100644 --- a/erpnext/accounts/doctype/dunning/dunning_dashboard.py +++ b/erpnext/accounts/doctype/dunning/dunning_dashboard.py @@ -14,4 +14,4 @@ def get_data(): 'items': ['Payment Entry', 'Journal Entry'] } ] - } \ No newline at end of file + } diff --git a/erpnext/accounts/doctype/dunning/test_dunning.py b/erpnext/accounts/doctype/dunning/test_dunning.py index e2d4d82e418..39ee74658ce 100644 --- a/erpnext/accounts/doctype/dunning/test_dunning.py +++ b/erpnext/accounts/doctype/dunning/test_dunning.py @@ -16,6 +16,7 @@ class TestDunning(unittest.TestCase): @classmethod def setUpClass(self): create_dunning_type() + create_dunning_type_with_zero_interest_rate() unlink_payment_on_cancel_of_invoice() @classmethod @@ -25,11 +26,20 @@ class TestDunning(unittest.TestCase): def test_dunning(self): dunning = create_dunning() amounts = calculate_interest_and_amount( - dunning.posting_date, dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days) + dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days) self.assertEqual(round(amounts.get('interest_amount'), 2), 0.44) self.assertEqual(round(amounts.get('dunning_amount'), 2), 20.44) self.assertEqual(round(amounts.get('grand_total'), 2), 120.44) + def test_dunning_with_zero_interest_rate(self): + dunning = create_dunning_with_zero_interest_rate() + amounts = calculate_interest_and_amount( + dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days) + self.assertEqual(round(amounts.get('interest_amount'), 2), 0) + self.assertEqual(round(amounts.get('dunning_amount'), 2), 20) + self.assertEqual(round(amounts.get('grand_total'), 2), 120) + + def test_gl_entries(self): dunning = create_dunning() dunning.submit() @@ -83,6 +93,27 @@ def create_dunning(): dunning.save() return dunning +def create_dunning_with_zero_interest_rate(): + posting_date = add_days(today(), -20) + due_date = add_days(today(), -15) + sales_invoice = create_sales_invoice_against_cost_center( + posting_date=posting_date, due_date=due_date, status='Overdue') + dunning_type = frappe.get_doc("Dunning Type", 'First Notice with 0% Rate of Interest') + dunning = frappe.new_doc("Dunning") + dunning.sales_invoice = sales_invoice.name + dunning.customer_name = sales_invoice.customer_name + dunning.outstanding_amount = sales_invoice.outstanding_amount + dunning.debit_to = sales_invoice.debit_to + dunning.currency = sales_invoice.currency + dunning.company = sales_invoice.company + dunning.posting_date = nowdate() + dunning.due_date = sales_invoice.due_date + dunning.dunning_type = 'First Notice with 0% Rate of Interest' + dunning.rate_of_interest = dunning_type.rate_of_interest + dunning.dunning_fee = dunning_type.dunning_fee + dunning.save() + return dunning + def create_dunning_type(): dunning_type = frappe.new_doc("Dunning Type") dunning_type.dunning_type = 'First Notice' @@ -98,3 +129,19 @@ def create_dunning_type(): } ) dunning_type.save() + +def create_dunning_type_with_zero_interest_rate(): + dunning_type = frappe.new_doc("Dunning Type") + dunning_type.dunning_type = 'First Notice with 0% Rate of Interest' + dunning_type.start_day = 10 + dunning_type.end_day = 20 + dunning_type.dunning_fee = 20 + dunning_type.rate_of_interest = 0 + dunning_type.append( + "dunning_letter_text", { + 'language': 'en', + 'body_text': 'We have still not received payment for our invoice ', + 'closing_text': 'We kindly request that you pay the outstanding amount immediately, and late fees.' + } + ) + dunning_type.save() diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.js b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.js index b7b6020caa9..926a442f808 100644 --- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.js +++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.js @@ -31,7 +31,7 @@ frappe.ui.form.on('Exchange Rate Revaluation', { }, __('Create')); } } - }); + }); } }, @@ -128,4 +128,4 @@ var get_account_details = function(frm, cdt, cdn) { frm.events.get_total_gain_loss(frm); } }); -}; \ No newline at end of file +}; diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py index 56193216a22..dbbcedcadfa 100644 --- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py +++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py @@ -27,6 +27,9 @@ class ExchangeRateRevaluation(Document): if not (self.company and self.posting_date): frappe.throw(_("Please select Company and Posting Date to getting entries")) + def on_cancel(self): + self.ignore_linked_doctypes = ('GL Entry') + @frappe.whitelist() def check_journal_entry_condition(self): total_debit = frappe.db.get_value("Journal Entry Account", { @@ -41,7 +44,7 @@ class ExchangeRateRevaluation(Document): if total_amt != total_debit: return True - + return False @frappe.whitelist() @@ -99,10 +102,12 @@ class ExchangeRateRevaluation(Document): sum(debit) - sum(credit) as balance from `tabGL Entry` where account in (%s) - group by account, party_type, party + and posting_date <= %s + and is_cancelled = 0 + group by account, NULLIF(party_type,''), NULLIF(party,'') having sum(debit) != sum(credit) order by account - """ % ', '.join(['%s']*len(accounts)), tuple(accounts), as_dict=1) + """ % (', '.join(['%s']*len(accounts)), '%s'), tuple(accounts + [self.posting_date]), as_dict=1) return account_details @@ -143,9 +148,9 @@ class ExchangeRateRevaluation(Document): "party_type": d.get("party_type"), "party": d.get("party"), "account_currency": d.get("account_currency"), - "balance": d.get("balance_in_account_currency"), - dr_or_cr: abs(d.get("balance_in_account_currency")), - "exchange_rate":d.get("new_exchange_rate"), + "balance": flt(d.get("balance_in_account_currency"), d.precision("balance_in_account_currency")), + dr_or_cr: flt(abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency")), + "exchange_rate": flt(d.get("new_exchange_rate"), d.precision("new_exchange_rate")), "reference_type": "Exchange Rate Revaluation", "reference_name": self.name, }) @@ -154,9 +159,9 @@ class ExchangeRateRevaluation(Document): "party_type": d.get("party_type"), "party": d.get("party"), "account_currency": d.get("account_currency"), - "balance": d.get("balance_in_account_currency"), - reverse_dr_or_cr: abs(d.get("balance_in_account_currency")), - "exchange_rate": d.get("current_exchange_rate"), + "balance": flt(d.get("balance_in_account_currency"), d.precision("balance_in_account_currency")), + reverse_dr_or_cr: flt(abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency")), + "exchange_rate": flt(d.get("current_exchange_rate"), d.precision("current_exchange_rate")), "reference_type": "Exchange Rate Revaluation", "reference_name": self.name }) @@ -185,9 +190,9 @@ def get_account_details(account, company, posting_date, party_type=None, party=N account_details = {} company_currency = erpnext.get_company_currency(company) - balance = get_balance_on(account, party_type=party_type, party=party, in_account_currency=False) + balance = get_balance_on(account, date=posting_date, party_type=party_type, party=party, in_account_currency=False) if balance: - balance_in_account_currency = get_balance_on(account, party_type=party_type, party=party) + balance_in_account_currency = get_balance_on(account, date=posting_date, party_type=party_type, party=party) current_exchange_rate = balance / balance_in_account_currency if balance_in_account_currency else 0 new_exchange_rate = get_exchange_rate(account_currency, company_currency, posting_date) new_balance_in_base_currency = balance_in_account_currency * new_exchange_rate @@ -200,4 +205,4 @@ def get_account_details(account, company, posting_date, party_type=None, party=N "new_balance_in_base_currency": new_balance_in_base_currency } - return account_details \ No newline at end of file + return account_details 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 57c6a7871dc..00000000000 --- 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 9fb7d4fcc8e..00000000000 --- 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/finance_book/test_finance_book.py b/erpnext/accounts/doctype/finance_book/test_finance_book.py index 502765812ae..2ba21397ad0 100644 --- a/erpnext/accounts/doctype/finance_book/test_finance_book.py +++ b/erpnext/accounts/doctype/finance_book/test_finance_book.py @@ -9,19 +9,8 @@ import frappe import unittest class TestFinanceBook(unittest.TestCase): - def create_finance_book(self): - if not frappe.db.exists("Finance Book", "_Test Finance Book"): - finance_book = frappe.get_doc({ - "doctype": "Finance Book", - "finance_book_name": "_Test Finance Book" - }).insert() - else: - finance_book = frappe.get_doc("Finance Book", "_Test Finance Book") - - return finance_book - def test_finance_book(self): - finance_book = self.create_finance_book() + finance_book = create_finance_book() # create jv entry jv = make_journal_entry("_Test Bank - _TC", @@ -41,3 +30,14 @@ class TestFinanceBook(unittest.TestCase): for gl_entry in gl_entries: self.assertEqual(gl_entry.finance_book, finance_book.name) + +def create_finance_book(): + if not frappe.db.exists("Finance Book", "_Test Finance Book"): + finance_book = frappe.get_doc({ + "doctype": "Finance Book", + "finance_book_name": "_Test Finance Book" + }).insert() + else: + finance_book = frappe.get_doc("Finance Book", "_Test Finance Book") + + return finance_book \ No newline at end of file diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index 11465b711e3..0844995f296 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -58,8 +58,8 @@ class GLEntry(Document): if not self.get(k): frappe.throw(_("{0} is required").format(_(self.meta.get_label(k)))) - account_type = frappe.get_cached_value("Account", self.account, "account_type") if not (self.party_type and self.party): + account_type = frappe.get_cached_value("Account", self.account, "account_type") if account_type == "Receivable": frappe.throw(_("{0} {1}: Customer is required against Receivable account {2}") .format(self.voucher_type, self.voucher_no, self.account)) @@ -73,15 +73,19 @@ class GLEntry(Document): .format(self.voucher_type, self.voucher_no, self.account)) def pl_must_have_cost_center(self): - if frappe.get_cached_value("Account", self.account, "report_type") == "Profit and Loss": - if not self.cost_center and self.voucher_type != 'Period Closing Voucher': - msg = _("{0} {1}: Cost Center is required for 'Profit and Loss' account {2}.").format( - self.voucher_type, self.voucher_no, self.account) - msg += " " - msg += _("Please set the cost center field in {0} or setup a default Cost Center for the Company.").format( - self.voucher_type) + """Validate that profit and loss type account GL entries have a cost center.""" - frappe.throw(msg, title=_("Missing Cost Center")) + if self.cost_center or self.voucher_type == 'Period Closing Voucher': + return + + if frappe.get_cached_value("Account", self.account, "report_type") == "Profit and Loss": + msg = _("{0} {1}: Cost Center is required for 'Profit and Loss' account {2}.").format( + self.voucher_type, self.voucher_no, self.account) + msg += " " + msg += _("Please set the cost center field in {0} or setup a default Cost Center for the Company.").format( + self.voucher_type) + + frappe.throw(msg, title=_("Missing Cost Center")) def validate_dimensions_for_pl_and_bs(self): account_type = frappe.db.get_value("Account", self.account, "report_type") 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 2986e5e4e3b..00000000000 --- 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/invoice_discounting/invoice_discounting_dashboard.py b/erpnext/accounts/doctype/invoice_discounting/invoice_discounting_dashboard.py index 6523cd3cdb8..6d35ca24397 100644 --- a/erpnext/accounts/doctype/invoice_discounting/invoice_discounting_dashboard.py +++ b/erpnext/accounts/doctype/invoice_discounting/invoice_discounting_dashboard.py @@ -17,4 +17,4 @@ def get_data(): 'items': ['Payment Entry', 'Journal Entry'] } ] - } \ No newline at end of file + } diff --git a/erpnext/accounts/doctype/invoice_discounting/invoice_discounting_list.js b/erpnext/accounts/doctype/invoice_discounting/invoice_discounting_list.js index a72023d8e12..4895efcd4cc 100644 --- a/erpnext/accounts/doctype/invoice_discounting/invoice_discounting_list.js +++ b/erpnext/accounts/doctype/invoice_discounting/invoice_discounting_list.js @@ -18,4 +18,4 @@ frappe.listview_settings['Invoice Discounting'] = { return [__("Canceled"), "red", "status,=,Canceled"]; } } -}; \ No newline at end of file +}; 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 68934993914..00000000000 --- 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 937597bc550..72648753a7a 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -643,7 +643,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/regional/india.js b/erpnext/accounts/doctype/journal_entry/regional/india.js index 75a69ac0cf3..c5f5520479f 100644 --- a/erpnext/accounts/doctype/journal_entry/regional/india.js +++ b/erpnext/accounts/doctype/journal_entry/regional/india.js @@ -14,4 +14,4 @@ frappe.ui.form.on("Journal Entry", { }; }); } -}); \ No newline at end of file +}); diff --git a/erpnext/accounts/doctype/journal_entry/test_journal_entry.py b/erpnext/accounts/doctype/journal_entry/test_journal_entry.py index 5f003e022a0..5835d462ae9 100644 --- a/erpnext/accounts/doctype/journal_entry/test_journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/test_journal_entry.py @@ -100,7 +100,7 @@ class TestJournalEntry(unittest.TestCase): "debit_in_account_currency": 0 if diff > 0 else abs(diff), "credit_in_account_currency": diff if diff > 0 else 0 }) - + jv.append("accounts", { "account": "Stock Adjustment - TCP1", "cost_center": "Main - TCP1", diff --git a/erpnext/accounts/doctype/journal_entry_template/journal_entry_template.js b/erpnext/accounts/doctype/journal_entry_template/journal_entry_template.js index cbb9fc4b0f9..1c19c1d2255 100644 --- a/erpnext/accounts/doctype/journal_entry_template/journal_entry_template.js +++ b/erpnext/accounts/doctype/journal_entry_template/journal_entry_template.js @@ -88,4 +88,4 @@ frappe.ui.form.on("Journal Entry Template", { frappe.model.clear_table(frm.doc, "accounts"); frm.refresh_field("accounts"); } -}); \ No newline at end of file +}); 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 a916b67522c..00000000000 --- 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 9321c14e1f0..00000000000 --- 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/mode_of_payment/mode_of_payment.js b/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.js index 7a06d3572a6..103fa96d02d 100644 --- a/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.js +++ b/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.js @@ -14,4 +14,4 @@ frappe.ui.form.on('Mode of Payment', { }; }); }, -}); \ No newline at end of file +}); diff --git a/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.py b/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.py index 32473694c80..cea921e999e 100644 --- a/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.py +++ b/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.py @@ -39,4 +39,3 @@ class ModeofPayment(Document): message = "POS Profile " + frappe.bold(", ".join(pos_profiles)) + " contains \ Mode of Payment " + frappe.bold(str(self.name)) + ". Please remove them to disable this mode." frappe.throw(_(message), title="Not Allowed") - diff --git a/erpnext/accounts/doctype/monthly_distribution/monthly_distribution.py b/erpnext/accounts/doctype/monthly_distribution/monthly_distribution.py index bff64227325..ad8623fb4ee 100644 --- a/erpnext/accounts/doctype/monthly_distribution/monthly_distribution.py +++ b/erpnext/accounts/doctype/monthly_distribution/monthly_distribution.py @@ -55,4 +55,4 @@ def get_percentage(doc, start_date, period): if d.month in months: percentage += d.percentage_allocation - return percentage \ No newline at end of file + return percentage diff --git a/erpnext/accounts/doctype/monthly_distribution/monthly_distribution_dashboard.py b/erpnext/accounts/doctype/monthly_distribution/monthly_distribution_dashboard.py index a6794998159..912bd9e331a 100644 --- a/erpnext/accounts/doctype/monthly_distribution/monthly_distribution_dashboard.py +++ b/erpnext/accounts/doctype/monthly_distribution/monthly_distribution_dashboard.py @@ -20,4 +20,4 @@ def get_data(): 'items': ['Budget'] } ] - } \ No newline at end of file + } diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js index b2e86267c8f..9f22ab0d76c 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js @@ -154,4 +154,4 @@ frappe.ui.form.on('Opening Invoice Creation Tool Item', { invoices_add: (frm) => { frm.trigger('update_invoice_table'); } -}); \ No newline at end of file +}); diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py index 29dc96e8c6f..3586e683d4d 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py @@ -240,5 +240,3 @@ def get_temporary_opening_account(company=None): frappe.throw(_("Please add a Temporary Opening account in Chart of Accounts")) return accounts[0].name - - 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 f95d0d82138..00000000000 --- 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/accounts/doctype/party_link/__init__.py b/erpnext/accounts/doctype/party_link/__init__.py new file mode 100644 index 00000000000..e69de29bb2d 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 00000000000..6da9291d64d --- /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 00000000000..a1bb15f0d6b --- /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 00000000000..7d58506ce74 --- /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 00000000000..a3ea3959ba4 --- /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 d3ac3a66760..727ef55b3c7 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -7,6 +7,8 @@ cur_frm.cscript.tax_table = "Advance Taxes and Charges"; frappe.ui.form.on('Payment Entry', { onload: function(frm) { + frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice']; + if(frm.doc.__islocal) { if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null); if (!frm.doc.paid_to) frm.set_value("paid_to_account_currency", null); @@ -531,8 +533,8 @@ frappe.ui.form.on('Payment Entry', { source_exchange_rate: function(frm) { if (frm.doc.paid_amount) { frm.set_value("base_paid_amount", flt(frm.doc.paid_amount) * flt(frm.doc.source_exchange_rate)); - if(!frm.set_paid_amount_based_on_received_amount && - (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency)) { + // target exchange rate should always be same as source if both account currencies are same + if(frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) { frm.set_value("target_exchange_rate", frm.doc.source_exchange_rate); frm.set_value("base_received_amount", frm.doc.base_paid_amount); } @@ -870,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.json b/erpnext/accounts/doctype/payment_entry/payment_entry.json index 51f18a5a4e3..6f362c1fbb9 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.json +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.json @@ -667,6 +667,7 @@ { "fieldname": "base_paid_amount_after_tax", "fieldtype": "Currency", + "hidden": 1, "label": "Paid Amount After Tax (Company Currency)", "options": "Company:company:default_currency", "read_only": 1 @@ -693,21 +694,25 @@ "depends_on": "eval:doc.received_amount && doc.payment_type != 'Internal Transfer'", "fieldname": "received_amount_after_tax", "fieldtype": "Currency", + "hidden": 1, "label": "Received Amount After Tax", - "options": "paid_to_account_currency" + "options": "paid_to_account_currency", + "read_only": 1 }, { "depends_on": "doc.received_amount", "fieldname": "base_received_amount_after_tax", "fieldtype": "Currency", + "hidden": 1, "label": "Received Amount After Tax (Company Currency)", - "options": "Company:company:default_currency" + "options": "Company:company:default_currency", + "read_only": 1 } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-06-22 20:37:06.154206", + "modified": "2021-07-09 08:58:15.008761", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Entry", diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index adaf99a7900..e34d651f0c4 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -55,14 +55,17 @@ class PaymentEntry(AccountsController): self.validate_mandatory() self.validate_reference_documents() self.set_tax_withholding() - self.apply_taxes() self.set_amounts() + self.validate_amounts() + self.apply_taxes() + self.set_amounts_after_tax() self.clear_unallocated_reference_document_rows() self.validate_payment_against_negative_invoice() self.validate_transaction_reference() self.set_title() self.set_remarks() self.validate_duplicate_entry() + self.validate_payment_type_with_outstanding() self.validate_allocated_amount() self.validate_paid_invoices() self.ensure_supplier_is_not_blocked() @@ -72,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() @@ -82,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) @@ -118,6 +121,11 @@ class PaymentEntry(AccountsController): if not self.get(field): self.set(field, bank_data.account) + def validate_payment_type_with_outstanding(self): + total_outstanding = sum(d.allocated_amount for d in self.get('references')) + if total_outstanding < 0 and self.party_type == 'Customer' and self.payment_type == 'Receive': + frappe.throw(_("Cannot receive from customer against negative outstanding"), title=_("Incorrect Payment Type")) + def validate_allocated_amount(self): for d in self.get("references"): if (flt(d.allocated_amount))> 0: @@ -183,6 +191,13 @@ class PaymentEntry(AccountsController): d.reference_name, self.party_account_currency) for field, value in iteritems(ref_details): + if d.exchange_gain_loss: + # for cases where gain/loss is booked into invoice + # exchange_gain_loss is calculated from invoice & populated + # and row.exchange_rate is already set to payment entry's exchange rate + # refer -> `update_reference_in_payment_entry()` in utils.py + continue + if field == 'exchange_rate' or not d.get(field) or force: d.db_set(field, value) @@ -229,7 +244,9 @@ class PaymentEntry(AccountsController): self.company_currency, self.posting_date) def set_target_exchange_rate(self, ref_doc=None): - if self.paid_to and not self.target_exchange_rate: + if self.paid_from_account_currency == self.paid_to_account_currency: + self.target_exchange_rate = self.source_exchange_rate + elif self.paid_to and not self.target_exchange_rate: if ref_doc: if self.paid_to_account_currency == ref_doc.currency: self.target_exchange_rate = ref_doc.get("exchange_rate") @@ -404,9 +421,15 @@ class PaymentEntry(AccountsController): if not self.advance_tax_account: frappe.throw(_("Advance TDS account is mandatory for advance TDS deduction")) - reference_doclist = [] net_total = self.paid_amount - included_in_paid_amount = 0 + + for reference in self.get("references"): + net_total_for_tds = 0 + if reference.reference_doctype == 'Purchase Order': + net_total_for_tds += flt(frappe.db.get_value('Purchase Order', reference.reference_name, 'net_total')) + + if net_total_for_tds: + net_total = net_total_for_tds # Adding args as purchase invoice to get TDS amount args = frappe._dict({ @@ -423,7 +446,7 @@ class PaymentEntry(AccountsController): return tax_withholding_details.update({ - 'included_in_paid_amount': included_in_paid_amount, + 'add_deduct_tax': 'Add', 'cost_center': self.cost_center or erpnext.get_default_cost_center(self.company) }) @@ -455,13 +478,22 @@ class PaymentEntry(AccountsController): def set_amounts(self): self.set_received_amount() self.set_amounts_in_company_currency() - self.set_amounts_after_tax() self.set_total_allocated_amount() self.set_unallocated_amount() self.set_difference_amount() + def validate_amounts(self): + self.validate_received_amount() + + def validate_received_amount(self): + if self.paid_from_account_currency == self.paid_to_account_currency: + if self.paid_amount != self.received_amount: + frappe.throw(_("Received Amount should be same as Paid Amount")) + def set_received_amount(self): self.base_received_amount = self.base_paid_amount + if self.paid_from_account_currency == self.paid_to_account_currency: + self.received_amount = self.paid_amount def set_amounts_after_tax(self): applicable_tax = 0 @@ -512,16 +544,19 @@ class PaymentEntry(AccountsController): self.unallocated_amount = 0 if self.party: total_deductions = sum(flt(d.amount) for d in self.get("deductions")) + included_taxes = self.get_included_taxes() if self.payment_type == "Receive" \ - and self.base_total_allocated_amount < self.base_received_amount_after_tax + total_deductions \ - and self.total_allocated_amount < self.paid_amount_after_tax + (total_deductions / self.source_exchange_rate): - self.unallocated_amount = (self.received_amount_after_tax + total_deductions - + and self.base_total_allocated_amount < self.base_received_amount + total_deductions \ + and self.total_allocated_amount < self.paid_amount + (total_deductions / self.source_exchange_rate): + self.unallocated_amount = (self.base_received_amount + total_deductions - self.base_total_allocated_amount) / self.source_exchange_rate + self.unallocated_amount -= included_taxes elif self.payment_type == "Pay" \ - and self.base_total_allocated_amount < (self.base_paid_amount_after_tax - total_deductions) \ - and self.total_allocated_amount < self.received_amount_after_tax + (total_deductions / self.target_exchange_rate): - self.unallocated_amount = (self.base_paid_amount_after_tax - (total_deductions + + and self.base_total_allocated_amount < (self.base_paid_amount - total_deductions) \ + and self.total_allocated_amount < self.received_amount + (total_deductions / self.target_exchange_rate): + self.unallocated_amount = (self.base_paid_amount - (total_deductions + self.base_total_allocated_amount)) / self.target_exchange_rate + self.unallocated_amount -= included_taxes def set_difference_amount(self): base_unallocated_amount = flt(self.unallocated_amount) * (flt(self.source_exchange_rate) @@ -530,17 +565,29 @@ class PaymentEntry(AccountsController): base_party_amount = flt(self.base_total_allocated_amount) + flt(base_unallocated_amount) if self.payment_type == "Receive": - self.difference_amount = base_party_amount - self.base_received_amount_after_tax + self.difference_amount = base_party_amount - self.base_received_amount elif self.payment_type == "Pay": - self.difference_amount = self.base_paid_amount_after_tax - base_party_amount + self.difference_amount = self.base_paid_amount - base_party_amount else: - self.difference_amount = self.base_paid_amount_after_tax - flt(self.base_received_amount_after_tax) + self.difference_amount = self.base_paid_amount - flt(self.base_received_amount) total_deductions = sum(flt(d.amount) for d in self.get("deductions")) + included_taxes = self.get_included_taxes() - self.difference_amount = flt(self.difference_amount - total_deductions, + self.difference_amount = flt(self.difference_amount - total_deductions - included_taxes, self.precision("difference_amount")) + def get_included_taxes(self): + included_taxes = 0 + for tax in self.get('taxes'): + if tax.included_in_paid_amount: + if tax.add_deduct_tax == 'Add': + included_taxes += tax.base_tax_amount + else: + included_taxes -= tax.base_tax_amount + + return included_taxes + # Paid amount is auto allocated in the reference document by default. # Clear the reference document which doesn't have allocated amount on validate so that form can be loaded fast def clear_unallocated_reference_document_rows(self): @@ -664,8 +711,8 @@ class PaymentEntry(AccountsController): gl_entries.append(gle) if self.unallocated_amount: - base_unallocated_amount = self.unallocated_amount * \ - (self.source_exchange_rate if self.payment_type=="Receive" else self.target_exchange_rate) + exchange_rate = self.get_exchange_rate() + base_unallocated_amount = (self.unallocated_amount * exchange_rate) gle = party_gl_dict.copy() @@ -683,8 +730,8 @@ class PaymentEntry(AccountsController): "account": self.paid_from, "account_currency": self.paid_from_account_currency, "against": self.party if self.payment_type=="Pay" else self.paid_to, - "credit_in_account_currency": self.paid_amount_after_tax, - "credit": self.base_paid_amount_after_tax, + "credit_in_account_currency": self.paid_amount, + "credit": self.base_paid_amount, "cost_center": self.cost_center }, item=self) ) @@ -694,8 +741,8 @@ class PaymentEntry(AccountsController): "account": self.paid_to, "account_currency": self.paid_to_account_currency, "against": self.party if self.payment_type=="Receive" else self.paid_from, - "debit_in_account_currency": self.received_amount_after_tax, - "debit": self.base_received_amount_after_tax, + "debit_in_account_currency": self.received_amount, + "debit": self.base_received_amount, "cost_center": self.cost_center }, item=self) ) @@ -708,35 +755,43 @@ 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() + tax_amount = d.tax_amount + base_tax_amount = d.base_tax_amount + + if self.advance_tax_account: + tax_amount = -1 * tax_amount + base_tax_amount = -1 * base_tax_amount gl_entries.append( self.get_gl_dict({ "account": d.account_head, - "against": self.party if self.payment_type=="Receive" else self.paid_from, - dr_or_cr: d.base_tax_amount, - dr_or_cr + "_in_account_currency": d.base_tax_amount + "against": against, + dr_or_cr: tax_amount, + dr_or_cr + "_in_account_currency": base_tax_amount if account_currency==self.company_currency else d.tax_amount, "cost_center": d.cost_center }, account_currency, item=d)) - #Intentionally use -1 to get net values in party account - gl_entries.append( - self.get_gl_dict({ - "account": payment_or_advance_account, - "against": self.party if self.payment_type=="Receive" else self.paid_from, - dr_or_cr: -1 * d.base_tax_amount, - dr_or_cr + "_in_account_currency": -1*d.base_tax_amount - if account_currency==self.company_currency - else d.tax_amount, - "cost_center": self.cost_center, - "party_type": self.party_type, - "party": self.party - }, account_currency, item=d)) + 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, + 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, + }, account_currency, item=d)) def add_deductions_gl_entries(self, gl_entries): for d in self.get("deductions"): @@ -760,9 +815,9 @@ class PaymentEntry(AccountsController): if self.advance_tax_account: return self.advance_tax_account elif self.payment_type == 'Receive': - return self.paid_from - elif self.payment_type in ('Pay', 'Internal Transfer'): return self.paid_to + elif self.payment_type in ('Pay', 'Internal Transfer'): + return self.paid_from def update_advance_paid(self): if self.payment_type in ("Receive", "Pay") and self.party: @@ -776,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: @@ -807,9 +865,16 @@ class PaymentEntry(AccountsController): if account_details: row.update(account_details) + if not row.get('amount'): + # if no difference amount + return + self.append('deductions', row) self.set_unallocated_amount() + def get_exchange_rate(self): + return self.source_exchange_rate if self.payment_type=="Receive" else self.target_exchange_rate + def initialize_taxes(self): for tax in self.get("taxes"): validate_taxes_and_charges(tax) @@ -1318,9 +1383,9 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre return frappe._dict({ "due_date": ref_doc.get("due_date"), - "total_amount": total_amount, - "outstanding_amount": outstanding_amount, - "exchange_rate": exchange_rate, + "total_amount": flt(total_amount), + "outstanding_amount": flt(outstanding_amount), + "exchange_rate": flt(exchange_rate), "bill_no": bill_no }) @@ -1634,12 +1699,6 @@ def set_paid_amount_and_received_amount(dt, party_account_currency, bank, outsta if dt == "Employee Advance": paid_amount = received_amount * doc.get('exchange_rate', 1) - if dt == "Purchase Order" and doc.apply_tds: - if party_account_currency == bank.account_currency: - paid_amount = received_amount = doc.base_net_total - else: - paid_amount = received_amount = doc.base_net_total * doc.get('exchange_rate', 1) - return paid_amount, received_amount def apply_early_payment_discount(paid_amount, received_amount, doc): diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry_list.js b/erpnext/accounts/doctype/payment_entry/payment_entry_list.js index e6d83b9f683..2d76fe69ef9 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry_list.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry_list.js @@ -11,4 +11,4 @@ frappe.listview_settings['Payment Entry'] = { }; } } -}; \ No newline at end of file +}; diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index 4641d6b5ffa..dac927b2cef 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -107,7 +107,7 @@ class TestPaymentEntry(unittest.TestCase): pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank USD - _TC") pe.reference_no = "1" pe.reference_date = "2016-01-01" - pe.target_exchange_rate = 50 + pe.source_exchange_rate = 50 pe.insert() pe.submit() @@ -154,7 +154,7 @@ class TestPaymentEntry(unittest.TestCase): pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank USD - _TC") pe.reference_no = "1" pe.reference_date = "2016-01-01" - pe.target_exchange_rate = 50 + pe.source_exchange_rate = 50 pe.insert() pe.submit() @@ -295,6 +295,34 @@ class TestPaymentEntry(unittest.TestCase): outstanding_amount = flt(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount")) self.assertEqual(outstanding_amount, 80) + def test_payment_entry_against_si_usd_to_usd_with_deduction_in_base_currency (self): + si = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC", + currency="USD", conversion_rate=50, do_not_save=1) + + si.plc_conversion_rate = 50 + si.save() + si.submit() + + pe = get_payment_entry("Sales Invoice", si.name, party_amount=20, + bank_account="_Test Bank USD - _TC", bank_amount=900) + + pe.source_exchange_rate = 45.263 + pe.target_exchange_rate = 45.263 + pe.reference_no = "1" + pe.reference_date = "2016-01-01" + + + pe.append("deductions", { + "account": "_Test Exchange Gain/Loss - _TC", + "cost_center": "_Test Cost Center - _TC", + "amount": 94.80 + }) + + pe.save() + + self.assertEqual(flt(pe.difference_amount, 2), 0.0) + self.assertEqual(flt(pe.unallocated_amount, 2), 0.0) + def test_payment_entry_retrieves_last_exchange_rate(self): from erpnext.setup.doctype.currency_exchange.test_currency_exchange import test_records, save_new_records @@ -463,7 +491,7 @@ class TestPaymentEntry(unittest.TestCase): pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank USD - _TC") pe.reference_no = "1" pe.reference_date = "2016-01-01" - pe.target_exchange_rate = 55 + pe.source_exchange_rate = 55 pe.append("deductions", { "account": "_Test Exchange Gain/Loss - _TC", @@ -589,9 +617,9 @@ class TestPaymentEntry(unittest.TestCase): party_account_balance = get_balance_on(account=pe.paid_from, cost_center=pe.cost_center) self.assertEqual(pe.cost_center, si.cost_center) - self.assertEqual(expected_account_balance, account_balance) - self.assertEqual(expected_party_balance, party_balance) - self.assertEqual(expected_party_account_balance, party_account_balance) + self.assertEqual(flt(expected_account_balance), account_balance) + self.assertEqual(flt(expected_party_balance), party_balance) + self.assertEqual(flt(expected_party_account_balance), party_account_balance) def create_payment_terms_template(): diff --git a/erpnext/accounts/doctype/payment_entry/tests/test_payment_against_purchase_invoice.js b/erpnext/accounts/doctype/payment_entry/tests/test_payment_against_purchase_invoice.js index 14aa0736d40..e8db2c3159d 100644 --- a/erpnext/accounts/doctype/payment_entry/tests/test_payment_against_purchase_invoice.js +++ b/erpnext/accounts/doctype/payment_entry/tests/test_payment_against_purchase_invoice.js @@ -57,4 +57,4 @@ QUnit.test("test payment entry", function(assert) { () => frappe.timeout(3), () => done() ]); -}); \ No newline at end of file +}); diff --git a/erpnext/accounts/doctype/payment_entry/tests/test_payment_entry.js b/erpnext/accounts/doctype/payment_entry/tests/test_payment_entry.js index 0c76343fa90..34af79fcd10 100644 --- a/erpnext/accounts/doctype/payment_entry/tests/test_payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/tests/test_payment_entry.js @@ -25,4 +25,4 @@ QUnit.test("test payment entry", function(assert) { () => frappe.timeout(0.3), () => done() ]); -}); \ No newline at end of file +}); diff --git a/erpnext/accounts/doctype/payment_entry/tests/test_payment_entry_write_off.js b/erpnext/accounts/doctype/payment_entry/tests/test_payment_entry_write_off.js index 9849d767271..8c7f6f47dd3 100644 --- a/erpnext/accounts/doctype/payment_entry/tests/test_payment_entry_write_off.js +++ b/erpnext/accounts/doctype/payment_entry/tests/test_payment_entry_write_off.js @@ -64,4 +64,4 @@ QUnit.test("test payment entry", function(assert) { }, () => done() ]); -}); \ No newline at end of file +}); diff --git a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json index 912ad0977a2..43eb0b6e2aa 100644 --- a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json +++ b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json @@ -14,7 +14,8 @@ "total_amount", "outstanding_amount", "allocated_amount", - "exchange_rate" + "exchange_rate", + "exchange_gain_loss" ], "fields": [ { @@ -90,12 +91,19 @@ "fieldtype": "Link", "label": "Payment Term", "options": "Payment Term" + }, + { + "fieldname": "exchange_gain_loss", + "fieldtype": "Currency", + "label": "Exchange Gain/Loss", + "options": "Company:company:default_currency", + "read_only": 1 } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-02-10 11:25:47.144392", + "modified": "2021-04-21 13:30:11.605388", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Entry Reference", diff --git a/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account.py b/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account.py index fd213a47a1e..3529c16a1c2 100644 --- a/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account.py +++ b/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account.py @@ -9,19 +9,19 @@ from frappe.model.document import Document class PaymentGatewayAccount(Document): def autoname(self): self.name = self.payment_gateway + " - " + self.currency - + def validate(self): self.currency = frappe.db.get_value("Account", self.payment_account, "account_currency") - + self.update_default_payment_gateway() self.set_as_default_if_not_set() - + def update_default_payment_gateway(self): if self.is_default: frappe.db.sql("""update `tabPayment Gateway Account` set is_default = 0 where is_default = 1 """) - + def set_as_default_if_not_set(self): - if not frappe.db.get_value("Payment Gateway Account", + if not frappe.db.get_value("Payment Gateway Account", {"is_default": 1, "name": ("!=", self.name)}, "name"): self.is_default = 1 diff --git a/erpnext/accounts/doctype/payment_order/payment_order.js b/erpnext/accounts/doctype/payment_order/payment_order.js index d12e474c5b1..aa373bc2fcc 100644 --- a/erpnext/accounts/doctype/payment_order/payment_order.js +++ b/erpnext/accounts/doctype/payment_order/payment_order.js @@ -136,4 +136,4 @@ frappe.ui.form.on('Payment Order', { dialog.show(); }, -}); \ No newline at end of file +}); diff --git a/erpnext/accounts/doctype/payment_order/payment_order_dashboard.py b/erpnext/accounts/doctype/payment_order/payment_order_dashboard.py index 6b93f926cdf..a4f335833ee 100644 --- a/erpnext/accounts/doctype/payment_order/payment_order_dashboard.py +++ b/erpnext/accounts/doctype/payment_order/payment_order_dashboard.py @@ -9,4 +9,4 @@ def get_data(): 'items': ['Payment Entry', 'Journal Entry'] } ] - } \ No newline at end of file + } 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 f63fc54521c..00000000000 --- 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_order/test_payment_order.py b/erpnext/accounts/doctype/payment_order/test_payment_order.py index 5fdde07faa4..9ba57aef300 100644 --- a/erpnext/accounts/doctype/payment_order/test_payment_order.py +++ b/erpnext/accounts/doctype/payment_order/test_payment_order.py @@ -46,4 +46,4 @@ def create_payment_order_against_payment_entry(ref_doc, order_type): doc = make_payment_order(ref_doc.name, payment_order) doc.save() doc.submit() - return doc \ No newline at end of file + return doc diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 6635128f9ef..acfe1fef2ee 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -306,5 +306,5 @@ def reconcile_dr_cr_note(dr_cr_notes, company): } ] }) - - jv.submit() \ No newline at end of file + jv.flags.ignore_mandatory = True + jv.submit() diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index 438951db627..f83cb375fcf 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -541,4 +541,4 @@ def make_payment_order(source_name, target_doc=None): } }, target_doc, set_missing_values) - return doclist \ No newline at end of file + return doclist 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 070b595fc6a..00000000000 --- 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_request/test_payment_request.py b/erpnext/accounts/doctype/payment_request/test_payment_request.py index 5eba62c0b31..ad6ff6f5553 100644 --- a/erpnext/accounts/doctype/payment_request/test_payment_request.py +++ b/erpnext/accounts/doctype/payment_request/test_payment_request.py @@ -138,4 +138,4 @@ class TestPaymentRequest(unittest.TestCase): # Try to make Payment Request more than SO amount, should give validation pr2.grand_total = 900 - self.assertRaises(frappe.ValidationError, pr2.save) \ No newline at end of file + self.assertRaises(frappe.ValidationError, pr2.save) diff --git a/erpnext/accounts/doctype/payment_term/payment_term.js b/erpnext/accounts/doctype/payment_term/payment_term.js index acd0144c2ea..feecf93484c 100644 --- a/erpnext/accounts/doctype/payment_term/payment_term.js +++ b/erpnext/accounts/doctype/payment_term/payment_term.js @@ -19,4 +19,4 @@ frappe.ui.form.on('Payment Term', { frm.set_df_property("discount", "description", description); } } -}); \ No newline at end of file +}); 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 b26e42aa37d..00000000000 --- 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/payment_terms_template.js b/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.js index 84c8d09b164..ea18adefa35 100644 --- a/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.js +++ b/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.js @@ -3,6 +3,6 @@ frappe.ui.form.on('Payment Terms Template', { setup: function(frm) { - + } }); diff --git a/erpnext/accounts/doctype/payment_terms_template/payment_terms_template_dashboard.py b/erpnext/accounts/doctype/payment_terms_template/payment_terms_template_dashboard.py index c705097ac66..5c8cb4fbdc1 100644 --- a/erpnext/accounts/doctype/payment_terms_template/payment_terms_template_dashboard.py +++ b/erpnext/accounts/doctype/payment_terms_template/payment_terms_template_dashboard.py @@ -30,4 +30,4 @@ def get_data(): 'items': ['Customer Group', 'Supplier Group'] } ] - } \ No newline at end of file + } 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 494a0ed21fb..00000000000 --- 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/period_closing_voucher.py b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py index b0a5b04de64..16fc98b38c2 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py +++ b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py @@ -50,9 +50,13 @@ class PeriodClosingVoucher(AccountsController): .format(pce[0][0], self.posting_date)) def make_gl_entries(self): + gl_entries = self.get_gl_entries() + if gl_entries: + from erpnext.accounts.general_ledger import make_gl_entries + make_gl_entries(gl_entries) + + def get_gl_entries(self): gl_entries = [] - net_pl_balance = 0 - pl_accounts = self.get_pl_balances() for acc in pl_accounts: @@ -60,6 +64,7 @@ class PeriodClosingVoucher(AccountsController): gl_entries.append(self.get_gl_dict({ "account": acc.account, "cost_center": acc.cost_center, + "finance_book": acc.finance_book, "account_currency": acc.account_currency, "debit_in_account_currency": abs(flt(acc.bal_in_account_currency)) if flt(acc.bal_in_account_currency) < 0 else 0, "debit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) < 0 else 0, @@ -67,35 +72,13 @@ class PeriodClosingVoucher(AccountsController): "credit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) > 0 else 0 }, item=acc)) - net_pl_balance += flt(acc.bal_in_company_currency) + if gl_entries: + gle_for_net_pl_bal = self.get_pnl_gl_entry(pl_accounts) + gl_entries += gle_for_net_pl_bal - if net_pl_balance: - if self.cost_center_wise_pnl: - costcenter_wise_gl_entries = self.get_costcenter_wise_pnl_gl_entries(pl_accounts) - gl_entries += costcenter_wise_gl_entries - else: - gl_entry = self.get_pnl_gl_entry(net_pl_balance) - gl_entries.append(gl_entry) - - from erpnext.accounts.general_ledger import make_gl_entries - make_gl_entries(gl_entries) + return gl_entries - def get_pnl_gl_entry(self, net_pl_balance): - cost_center = frappe.db.get_value("Company", self.company, "cost_center") - gl_entry = self.get_gl_dict({ - "account": self.closing_account_head, - "debit_in_account_currency": abs(net_pl_balance) if net_pl_balance > 0 else 0, - "debit": abs(net_pl_balance) if net_pl_balance > 0 else 0, - "credit_in_account_currency": abs(net_pl_balance) if net_pl_balance < 0 else 0, - "credit": abs(net_pl_balance) if net_pl_balance < 0 else 0, - "cost_center": cost_center - }) - - self.update_default_dimensions(gl_entry) - - return gl_entry - - def get_costcenter_wise_pnl_gl_entries(self, pl_accounts): + def get_pnl_gl_entry(self, pl_accounts): company_cost_center = frappe.db.get_value("Company", self.company, "cost_center") gl_entries = [] @@ -104,6 +87,7 @@ class PeriodClosingVoucher(AccountsController): gl_entry = self.get_gl_dict({ "account": self.closing_account_head, "cost_center": acc.cost_center or company_cost_center, + "finance_book": acc.finance_book, "account_currency": acc.account_currency, "debit_in_account_currency": abs(flt(acc.bal_in_account_currency)) if flt(acc.bal_in_account_currency) > 0 else 0, "debit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) > 0 else 0, @@ -130,7 +114,7 @@ class PeriodClosingVoucher(AccountsController): def get_pl_balances(self): """Get balance for dimension-wise pl accounts""" - dimension_fields = ['t1.cost_center'] + dimension_fields = ['t1.cost_center', 't1.finance_book'] self.accounting_dimensions = get_accounting_dimensions() for dimension in self.accounting_dimensions: 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 2f29372b01c..2a636bb3381 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 @@ -8,63 +8,54 @@ import frappe from frappe.utils import flt, today from erpnext.accounts.utils import get_fiscal_year, now from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry +from erpnext.accounts.doctype.finance_book.test_finance_book import create_finance_book from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice 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'") @@ -92,40 +83,80 @@ 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'") + + company = create_company() + surplus_account = create_account() + cost_center = create_cost_center("Test Cost Center 1") + + create_sales_invoice( + company=company, + income_account="Sales - TPC", + expense_account="Cost of Goods Sold - TPC", + cost_center=cost_center, + rate=400, + debit_to="Debtors - TPC" + ) + jv = make_journal_entry( + account1="Cash - TPC", + account2="Sales - TPC", + amount=400, + cost_center=cost_center, + posting_date=now() + ) + jv.company = company + jv.finance_book = create_finance_book().name + jv.save() + jv.submit() + + pcv = self.make_period_closing_voucher() + surplus_account = pcv.closing_account_head + + expected_gle = ( + (surplus_account, 0.0, 400.0, None), + (surplus_account, 0.0, 400.0, jv.finance_book), + ('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 + order by account, finance_book + """, (pcv.name)) + + 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() @@ -139,7 +170,7 @@ def create_company(): 'company_name': "Test PCV Company", 'country': 'United States', 'default_currency': 'USD' - }) + }) company.insert(ignore_if_duplicate = True) return company.name diff --git a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js index 6418d730903..264d4a68b00 100644 --- a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js +++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js @@ -20,9 +20,9 @@ frappe.ui.form.on('POS Closing Entry', { frm.set_query("pos_opening_entry", function(doc) { return { filters: { 'status': 'Open', 'docstatus': 1 } }; }); - + if (frm.doc.docstatus === 0 && !frm.doc.amended_from) frm.set_value("period_end_date", frappe.datetime.now_datetime()); - + frappe.realtime.on('closing_process_complete', async function(data) { await frm.reload_doc(); if (frm.doc.status == 'Failed' && frm.doc.error_message && data.user == frappe.session.user) { @@ -43,7 +43,7 @@ frappe.ui.form.on('POS Closing Entry', { const issue = 'issue'; frm.dashboard.set_headline( __('POS Closing failed while running in a background process. You can resolve the {0} and retry the process again.', [issue])); - + $('#jump_to_error').on('click', (e) => { e.preventDefault(); frappe.utils.scroll_to( 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 48109b159c6..00000000000 --- 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_invoice/pos_invoice.js b/erpnext/accounts/doctype/pos_invoice/pos_invoice.js index 493bd448024..33f04cbaa63 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.js +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.js @@ -110,17 +110,13 @@ erpnext.selling.POSInvoiceController = erpnext.selling.SellingController.extend( this.frm.refresh_field("base_paid_amount"); }, - write_off_outstanding_amount_automatically: function() { - if(cint(this.frm.doc.write_off_outstanding_amount_automatically)) { + write_off_outstanding_amount_automatically() { + if (cint(this.frm.doc.write_off_outstanding_amount_automatically)) { frappe.model.round_floats_in(this.frm.doc, ["grand_total", "paid_amount"]); // this will make outstanding amount 0 this.frm.set_value("write_off_amount", flt(this.frm.doc.grand_total - this.frm.doc.paid_amount - this.frm.doc.total_advance, precision("write_off_amount")) ); - this.frm.toggle_enable("write_off_amount", false); - - } else { - this.frm.toggle_enable("write_off_amount", true); } this.calculate_outstanding_amount(false); @@ -235,4 +231,4 @@ frappe.ui.form.on('POS Invoice', { }); }); } -}); \ No newline at end of file +}); diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json index 7459c11d4d9..19c6c8f3472 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json @@ -99,6 +99,7 @@ "loyalty_redemption_account", "loyalty_redemption_cost_center", "section_break_49", + "coupon_code", "apply_discount_on", "base_discount_amount", "column_break_51", @@ -595,7 +596,8 @@ { "fieldname": "scan_barcode", "fieldtype": "Data", - "label": "Scan Barcode" + "label": "Scan Barcode", + "options": "Barcode" }, { "allow_bulk_edit": 1, @@ -1182,7 +1184,8 @@ "label": "Write Off Amount", "no_copy": 1, "options": "currency", - "print_hide": 1 + "print_hide": 1, + "read_only_depends_on": "eval: doc.write_off_outstanding_amount_automatically" }, { "fieldname": "base_write_off_amount", @@ -1545,14 +1548,23 @@ "fieldname": "consolidated_invoice", "fieldtype": "Link", "label": "Consolidated Sales Invoice", + "no_copy": 1, "options": "Sales Invoice", "read_only": 1 + }, + { + "depends_on": "coupon_code", + "fieldname": "coupon_code", + "fieldtype": "Link", + "label": "Coupon Code", + "options": "Coupon Code", + "print_hide": 1 } ], "icon": "fa fa-file-text", "is_submittable": 1, "links": [], - "modified": "2021-02-01 15:03:33.800707", + "modified": "2021-08-24 18:19:20.728433", "modified_by": "Administrator", "module": "Accounts", "name": "POS Invoice", diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index 8ec4ef224cd..034a217a26d 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -44,6 +44,9 @@ class POSInvoice(SalesInvoice): self.validate_pos() self.validate_payment_amount() self.validate_loyalty_transaction() + if self.coupon_code: + from erpnext.accounts.doctype.pricing_rule.utils import validate_coupon_code + validate_coupon_code(self.coupon_code) def on_submit(self): # create the loyalty point ledger entry if the customer is enrolled in any loyalty program @@ -58,6 +61,10 @@ class POSInvoice(SalesInvoice): self.check_phone_payments() self.set_status(update=True) + if self.coupon_code: + from erpnext.accounts.doctype.pricing_rule.utils import update_coupon_code_count + update_coupon_code_count(self.coupon_code,'used') + def before_cancel(self): if self.consolidated_invoice and frappe.db.get_value('Sales Invoice', self.consolidated_invoice, 'docstatus') == 1: pos_closing_entry = frappe.get_all( @@ -84,6 +91,10 @@ class POSInvoice(SalesInvoice): against_psi_doc.delete_loyalty_point_entry() against_psi_doc.make_loyalty_point_entry() + if self.coupon_code: + from erpnext.accounts.doctype.pricing_rule.utils import update_coupon_code_count + update_coupon_code_count(self.coupon_code,'cancelled') + def check_phone_payments(self): for pay in self.payments: if pay.type == "Phone" and pay.amount >= 0: @@ -127,7 +138,7 @@ class POSInvoice(SalesInvoice): .format(item.idx, bold_delivered_serial_nos), title=_("Item Unavailable")) def validate_stock_availablility(self): - if self.is_return: + if self.is_return or self.docstatus != 1: return allow_negative_stock = frappe.db.get_single_value('Stock Settings', 'allow_negative_stock') diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py index 6172796129f..d2527fb2e50 100644 --- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py @@ -320,7 +320,8 @@ class TestPOSInvoice(unittest.TestCase): pos2.get("items")[0].serial_no = serial_nos[0] pos2.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 1000}) - self.assertRaises(frappe.ValidationError, pos2.insert) + pos2.insert() + self.assertRaises(frappe.ValidationError, pos2.submit) def test_delivered_serialized_item_transaction(self): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item @@ -348,7 +349,8 @@ class TestPOSInvoice(unittest.TestCase): pos2.get("items")[0].serial_no = serial_nos[0] pos2.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 1000}) - self.assertRaises(frappe.ValidationError, pos2.insert) + pos2.insert() + self.assertRaises(frappe.ValidationError, pos2.submit) def test_loyalty_points(self): from erpnext.accounts.doctype.loyalty_program.test_loyalty_program import create_records diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.js b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.js index cd08efc55fb..2f8081b95ce 100644 --- a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.js +++ b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.js @@ -5,10 +5,10 @@ frappe.ui.form.on('POS Invoice Merge Log', { setup: function(frm) { frm.set_query("pos_invoice", "pos_invoices", doc => { return{ - filters: { + filters: { 'docstatus': 1, - 'customer': doc.customer, - 'consolidated_invoice': '' + 'customer': doc.customer, + 'consolidated_invoice': '' } } }); diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py index 08e072e2049..e50d437ba6a 100644 --- a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py +++ b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py @@ -354,4 +354,4 @@ def safe_load_json(message): except Exception: json_message = message - return json_message \ No newline at end of file + return json_message diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py index 040a815fab3..1b9659409c0 100644 --- a/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py +++ b/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py @@ -147,4 +147,3 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): frappe.set_user("Administrator") frappe.db.sql("delete from `tabPOS Profile`") frappe.db.sql("delete from `tabPOS Invoice`") - diff --git a/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.js b/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.js index 372e75649b3..d23f348f04e 100644 --- a/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.js +++ b/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.js @@ -53,4 +53,4 @@ frappe.ui.form.on('POS Opening Entry', { }); } } -}); \ No newline at end of file +}); diff --git a/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py b/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py index 0023a84a46e..3318fefab14 100644 --- a/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py +++ b/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py @@ -38,4 +38,4 @@ class POSOpeningEntry(StatusUpdater): frappe.throw(msg.format(", ".join(invalid_modes)), title=_("Missing Account")) def on_submit(self): - self.set_status(update=True) \ No newline at end of file + self.set_status(update=True) diff --git a/erpnext/accounts/doctype/pos_opening_entry/test_pos_opening_entry.py b/erpnext/accounts/doctype/pos_opening_entry/test_pos_opening_entry.py index 2e36391714b..c115be5ae94 100644 --- a/erpnext/accounts/doctype/pos_opening_entry/test_pos_opening_entry.py +++ b/erpnext/accounts/doctype/pos_opening_entry/test_pos_opening_entry.py @@ -21,8 +21,8 @@ def create_opening_entry(pos_profile, user): balance_details.append(frappe._dict({ 'mode_of_payment': d.mode_of_payment })) - + entry.set("balance_details", balance_details) entry.submit() - - return entry.as_dict() + + return entry.as_dict() 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 42e5b7f92f1..00000000000 --- 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 5449ab76a38..00000000000 --- 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/pos_settings.py b/erpnext/accounts/doctype/pos_settings/pos_settings.py index 913f49829c1..d925dd9d86e 100644 --- a/erpnext/accounts/doctype/pos_settings/pos_settings.py +++ b/erpnext/accounts/doctype/pos_settings/pos_settings.py @@ -8,4 +8,4 @@ from frappe.model.document import Document class POSSettings(Document): def validate(self): - pass \ No newline at end of file + pass 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 639c94ed10d..00000000000 --- 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/pricing_rule.json b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json index 428989aa965..99c5b34fa35 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json @@ -2,12 +2,13 @@ "actions": [], "allow_import": 1, "allow_rename": 1, - "autoname": "field:title", + "autoname": "naming_series:", "creation": "2014-02-21 15:02:51", "doctype": "DocType", "engine": "InnoDB", "field_order": [ "applicability_section", + "naming_series", "title", "disable", "apply_on", @@ -95,8 +96,7 @@ "fieldtype": "Data", "label": "Title", "no_copy": 1, - "reqd": 1, - "unique": 1 + "reqd": 1 }, { "default": "0", @@ -558,7 +558,8 @@ "description": "Simple Python Expression, Example: territory != 'All Territories'", "fieldname": "condition", "fieldtype": "Code", - "label": "Condition" + "label": "Condition", + "options": "PythonExpression" }, { "fieldname": "column_break_42", @@ -570,12 +571,19 @@ "fieldname": "is_recursive", "fieldtype": "Check", "label": "Is Recursive" + }, + { + "default": "PRLE-.####", + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Naming Series", + "options": "PRLE-.####" } ], "icon": "fa fa-gift", "idx": 1, "links": [], - "modified": "2021-03-06 22:01:24.840422", + "modified": "2021-08-06 15:10:04.219321", "modified_by": "Administrator", "module": "Accounts", "name": "Pricing Rule", @@ -633,5 +641,6 @@ ], "show_name_in_global_search": 1, "sort_field": "modified", - "sort_order": "DESC" -} \ No newline at end of file + "sort_order": "DESC", + "title_field": "title" +} diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py index 556f49d34c0..4903c50e17b 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -198,12 +198,19 @@ def apply_pricing_rule(args, doc=None): set_serial_nos_based_on_fifo = frappe.db.get_single_value("Stock Settings", "automatically_set_serial_nos_based_on_fifo") + item_code_list = tuple(item.get('item_code') for item in item_list) + query_items = frappe.get_all('Item', fields=['item_code','has_serial_no'], filters=[['item_code','in',item_code_list]],as_list=1) + serialized_items = dict() + for item_code, val in query_items: + serialized_items.setdefault(item_code, val) + for item in item_list: args_copy = copy.deepcopy(args) args_copy.update(item) data = get_pricing_rule_for_item(args_copy, item.get('price_list_rate'), doc=doc) out.append(data) - if not item.get("serial_no") and set_serial_nos_based_on_fifo and not args.get('is_return'): + + if serialized_items.get(item.get('item_code')) and not item.get("serial_no") and set_serial_nos_based_on_fifo and not args.get('is_return'): out[0].update(get_serial_no_for_item(args_copy)) return out diff --git a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py index ffe8be1162f..662d7704a55 100644 --- a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py @@ -605,4 +605,4 @@ def delete_existing_pricing_rules(): for doctype in ["Pricing Rule", "Pricing Rule Item Code", "Pricing Rule Item Group", "Pricing Rule Brand"]: - frappe.db.sql("delete from `tab{0}`".format(doctype)) \ No newline at end of file + frappe.db.sql("delete from `tab{0}`".format(doctype)) diff --git a/erpnext/accounts/doctype/pricing_rule/tests/test_pricing_rule.js b/erpnext/accounts/doctype/pricing_rule/tests/test_pricing_rule.js index 8155e7d799a..8279b59cb41 100644 --- a/erpnext/accounts/doctype/pricing_rule/tests/test_pricing_rule.js +++ b/erpnext/accounts/doctype/pricing_rule/tests/test_pricing_rule.js @@ -26,4 +26,3 @@ QUnit.test("test pricing rule", function(assert) { () => done() ]); }); - diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py index b54d0e73a8c..5467cb0bc5b 100644 --- a/erpnext/accounts/doctype/pricing_rule/utils.py +++ b/erpnext/accounts/doctype/pricing_rule/utils.py @@ -168,7 +168,7 @@ def _get_tree_conditions(args, parenttype, table, allow_blank=True): frappe.throw(_("Invalid {0}").format(args.get(field))) parent_groups = frappe.db.sql_list("""select name from `tab%s` - where lft>=%s and rgt<=%s""" % (parenttype, '%s', '%s'), (lft, rgt)) + where lft<=%s and rgt>=%s""" % (parenttype, '%s', '%s'), (lft, rgt)) if parenttype in ["Customer Group", "Item Group", "Territory"]: parent_field = "parent_{0}".format(frappe.scrub(parenttype)) @@ -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/process_deferred_accounting/process_deferred_accounting.py b/erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.py index 0eac73236e9..5e7583a9745 100644 --- a/erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.py +++ b/erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.py @@ -31,4 +31,4 @@ class ProcessDeferredAccounting(Document): 'against_voucher': self.name }) - make_reverse_gl_entries(gl_entries=gl_entries) \ No newline at end of file + make_reverse_gl_entries(gl_entries=gl_entries) diff --git a/erpnext/accounts/doctype/process_deferred_accounting/test_process_deferred_accounting.py b/erpnext/accounts/doctype/process_deferred_accounting/test_process_deferred_accounting.py index e08a0e5cc2b..03c269ac766 100644 --- a/erpnext/accounts/doctype/process_deferred_accounting/test_process_deferred_accounting.py +++ b/erpnext/accounts/doctype/process_deferred_accounting/test_process_deferred_accounting.py @@ -45,4 +45,4 @@ class TestProcessDeferredAccounting(unittest.TestCase): ["Sales - _TC", 0.0, 33.85, "2019-01-31"] ] - check_gl_entries(self, si.name, expected_gle, "2019-01-10") \ No newline at end of file + check_gl_entries(self, si.name, expected_gle, "2019-01-10") diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html index 7328f168e3d..f8d191cc3f8 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html @@ -106,4 +106,4 @@ {{ terms_and_conditions }} {% endif %} - \ No newline at end of file + diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py index 0b0ee904ff9..a12ea4033df 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py @@ -207,10 +207,9 @@ def fetch_customers(customer_collection, collection_name, primary_mandatory): @frappe.whitelist() def get_customer_emails(customer_name, primary_mandatory, billing_and_primary=True): billing_email = frappe.db.sql(""" - SELECT c.email_id FROM `tabContact` AS c JOIN `tabDynamic Link` AS l ON c.name=l.parent \ - WHERE l.link_doctype='Customer' and l.link_name='""" + customer_name + """' and \ - c.is_billing_contact=1 \ - order by c.creation desc""") + SELECT c.email_id FROM `tabContact` AS c JOIN `tabDynamic Link` AS l ON c.name=l.parent + WHERE l.link_doctype='Customer' and l.link_name=%s and c.is_billing_contact=1 + order by c.creation desc""", customer_name) if len(billing_email) == 0 or (billing_email[0][0] is None): if billing_and_primary: @@ -285,4 +284,4 @@ def send_auto_email(): selected = frappe.get_list('Process Statement Of Accounts', filters={'to_date': format_date(today()), 'enable_auto_email': 1}) for entry in selected: send_emails(entry.name, from_scheduler=True) - return True \ No newline at end of file + return True diff --git a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.js b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.js index 890a1871bdd..e840c79cd75 100644 --- a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.js +++ b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.js @@ -48,4 +48,4 @@ frappe.ui.form.on('Promotional Scheme', { frm.doc.apply_on === key ? 1 : 0); } } -}); \ No newline at end of file +}); diff --git a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.json b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.json index cc71324dbc3..1d68b23d6c7 100644 --- a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.json +++ b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.json @@ -1,1381 +1,339 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, + "actions": [], "allow_import": 1, "allow_rename": 1, "autoname": "Prompt", - "beta": 0, "creation": "2019-02-08 17:10:36.077402", - "custom": 0, - "docstatus": 0, "doctype": "DocType", - "document_type": "", "editable_grid": 1, "engine": "InnoDB", + "field_order": [ + "section_break_1", + "apply_on", + "disable", + "column_break_3", + "items", + "item_groups", + "brands", + "mixed_conditions", + "is_cumulative", + "section_break_10", + "apply_rule_on_other", + "column_break_11", + "other_item_code", + "other_item_group", + "other_brand", + "section_break_8", + "selling", + "buying", + "column_break_12", + "applicable_for", + "customer", + "customer_group", + "territory", + "sales_partner", + "campaign", + "supplier", + "supplier_group", + "period_settings_section", + "valid_from", + "valid_upto", + "column_break_26", + "company", + "currency", + "section_break_14", + "price_discount_slabs", + "section_break_15", + "product_discount_slabs" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "section_break_1", - "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": "", - "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 + "fieldtype": "Section Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "Item Code", - "fetch_if_empty": 0, "fieldname": "apply_on", "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, "label": "Apply On", - "length": 0, - "no_copy": 0, "options": "\nItem Code\nItem Group\nBrand\nTransaction", - "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 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, + "default": "0", "fieldname": "disable", "fieldtype": "Check", - "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": "Disable", - "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 + "label": "Disable" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "fetch_if_empty": 0, "fieldname": "column_break_3", - "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, - "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 + "fieldtype": "Column Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "depends_on": "eval:doc.apply_on == 'Item Code'", - "fetch_if_empty": 0, "fieldname": "items", "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": "Pricing Rule Item Code", - "length": 0, - "no_copy": 0, - "options": "Pricing Rule Item Code", - "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 + "options": "Pricing Rule Item Code" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "depends_on": "eval:doc.apply_on == 'Item Group'", - "fetch_if_empty": 0, "fieldname": "item_groups", "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": "Pricing Rule Item Group", - "length": 0, - "no_copy": 0, - "options": "Pricing Rule Item Group", - "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 + "options": "Pricing Rule Item Group" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "depends_on": "eval:doc.apply_on == 'Brand'", - "fetch_if_empty": 0, "fieldname": "brands", "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": "Pricing Rule Brand", - "length": 0, - "no_copy": 0, - "options": "Pricing Rule Brand", - "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 + "options": "Pricing Rule Brand" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, + "default": "0", "fieldname": "mixed_conditions", "fieldtype": "Check", - "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": "Mixed Conditions", - "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 + "label": "Mixed Conditions" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, + "default": "0", "fieldname": "is_cumulative", "fieldtype": "Check", - "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": "Is Cumulative", - "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 + "label": "Is Cumulative" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, "collapsible": 1, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "section_break_10", "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": "Discount on Other Item", - "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 + "label": "Discount on Other Item" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "fetch_if_empty": 0, "fieldname": "apply_rule_on_other", "fieldtype": "Select", - "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": "Apply Rule On Other", - "length": 0, - "no_copy": 0, - "options": "\nItem Code\nItem Group\nBrand", - "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 + "options": "\nItem Code\nItem Group\nBrand" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "fetch_if_empty": 0, "fieldname": "column_break_11", - "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, - "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 + "fieldtype": "Column Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "depends_on": "eval:doc.apply_rule_on_other == 'Item Code'", - "fetch_if_empty": 0, "fieldname": "other_item_code", "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": "Item Code", - "length": 0, - "no_copy": 0, - "options": "Item", - "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 + "options": "Item" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "depends_on": "eval:doc.apply_rule_on_other == 'Item Group'", - "fetch_if_empty": 0, "fieldname": "other_item_group", "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": "Item Group", - "length": 0, - "no_copy": 0, - "options": "Item Group", - "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 + "options": "Item Group" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "depends_on": "eval:doc.apply_rule_on_other == 'Brand'", - "fetch_if_empty": 0, "fieldname": "other_brand", "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": "Brand", - "length": 0, - "no_copy": 0, - "options": "Brand", - "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 + "options": "Brand" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, "collapsible": 1, - "columns": 0, - "depends_on": "", - "fetch_if_empty": 0, "fieldname": "section_break_8", "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": "Party Information", - "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 + "label": "Party Information" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, + "default": "0", "fieldname": "selling", "fieldtype": "Check", - "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": "Selling", - "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 + "label": "Selling" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, + "default": "0", "fieldname": "buying", "fieldtype": "Check", - "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": "Buying", - "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 + "label": "Buying" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "fetch_if_empty": 0, "fieldname": "column_break_12", - "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, - "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 + "fieldtype": "Column Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "depends_on": "eval: doc.buying || doc.selling", - "fetch_if_empty": 0, "fieldname": "applicable_for", "fieldtype": "Select", - "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": "Applicable For", - "length": 0, - "no_copy": 0, - "options": "\nCustomer\nCustomer Group\nTerritory\nSales Partner\nCampaign\nSupplier\nSupplier Group", - "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 + "options": "\nCustomer\nCustomer Group\nTerritory\nSales Partner\nCampaign\nSupplier\nSupplier Group" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "depends_on": "eval:doc.applicable_for=='Customer'", - "fetch_if_empty": 0, "fieldname": "customer", - "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, + "fieldtype": "Table MultiSelect", "label": "Customer", - "length": 0, - "no_copy": 0, - "options": "Customer", - "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 + "options": "Customer Item" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "depends_on": "eval:doc.applicable_for==\"Customer Group\"", - "fetch_if_empty": 0, "fieldname": "customer_group", - "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, + "fieldtype": "Table MultiSelect", "label": "Customer Group", - "length": 0, - "no_copy": 0, - "options": "Customer Group", - "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 + "options": "Customer Group Item" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "depends_on": "eval:doc.applicable_for==\"Territory\"", - "fetch_if_empty": 0, "fieldname": "territory", - "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, + "fieldtype": "Table MultiSelect", "label": "Territory", - "length": 0, - "no_copy": 0, - "options": "Territory", - "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 + "options": "Territory Item" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "depends_on": "eval:doc.applicable_for==\"Sales Partner\"", - "fetch_if_empty": 0, "fieldname": "sales_partner", - "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, + "fieldtype": "Table MultiSelect", "label": "Sales Partner", - "length": 0, - "no_copy": 0, - "options": "Sales Partner", - "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 + "options": "Sales Partner Item" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "depends_on": "eval:doc.applicable_for==\"Campaign\"", - "fetch_if_empty": 0, "fieldname": "campaign", - "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, + "fieldtype": "Table MultiSelect", "label": "Campaign", - "length": 0, - "no_copy": 0, - "options": "Campaign", - "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 + "options": "Campaign Item" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "depends_on": "eval:doc.applicable_for=='Supplier'", - "fetch_if_empty": 0, "fieldname": "supplier", - "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, + "fieldtype": "Table MultiSelect", "label": "Supplier", - "length": 0, - "no_copy": 0, - "options": "Supplier", - "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 + "options": "Supplier Item" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "depends_on": "eval:doc.applicable_for==\"Supplier Group\"", - "fetch_if_empty": 0, "fieldname": "supplier_group", - "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, + "fieldtype": "Table MultiSelect", "label": "Supplier Group", - "length": 0, - "no_copy": 0, - "options": "Supplier Group", - "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 + "options": "Supplier Group Item" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "period_settings_section", "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": "Period Settings", - "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 + "label": "Period Settings" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "Today", - "fetch_if_empty": 0, "fieldname": "valid_from", "fieldtype": "Date", - "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": "Valid From", - "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 + "label": "Valid From" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "valid_upto", "fieldtype": "Date", - "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": "Valid Upto", - "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 + "label": "Valid Upto" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "column_break_26", - "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, - "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 + "fieldtype": "Column Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 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": 1, - "in_standard_filter": 0, "label": "Company", - "length": 0, - "no_copy": 0, "options": "Company", - "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 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "currency", "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": "Currency", - "length": 0, - "no_copy": 0, - "options": "Currency", - "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 + "options": "Currency" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "fetch_if_empty": 0, "fieldname": "section_break_14", "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": "Price Discount Slabs", - "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 + "label": "Price Discount Slabs" }, { "allow_bulk_edit": 1, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "price_discount_slabs", "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": "Promotional Scheme Price Discount", - "length": 0, - "no_copy": 0, - "options": "Promotional Scheme Price Discount", - "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 + "options": "Promotional Scheme Price Discount" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "fetch_if_empty": 0, "fieldname": "section_break_15", "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": "Product Discount Slabs", - "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 + "label": "Product Discount Slabs" }, { "allow_bulk_edit": 1, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "product_discount_slabs", "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": "Promotional Scheme Product Discount", - "length": 0, - "no_copy": 0, - "options": "Promotional Scheme Product Discount", - "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 + "options": "Promotional Scheme Product Discount" } ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2019-03-25 12:14:27.486586", + "links": [], + "modified": "2021-05-06 16:20:22.039078", "modified_by": "Administrator", "module": "Accounts", "name": "Promotional Scheme", - "name_case": "", "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, "create": 1, "delete": 1, "email": 1, "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "System Manager", - "set_user_permissions": 0, "share": 1, - "submit": 0, "write": 1 }, { - "amend": 0, - "cancel": 0, "create": 1, "delete": 1, "email": 1, "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "Accounts Manager", - "set_user_permissions": 0, "share": 1, - "submit": 0, "write": 1 }, { - "amend": 0, - "cancel": 0, "create": 1, "delete": 1, "email": 1, "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "Sales Manager", - "set_user_permissions": 0, "share": 1, - "submit": 0, "write": 1 }, { - "amend": 0, - "cancel": 0, "create": 1, "delete": 1, "email": 1, "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "Accounts User", - "set_user_permissions": 0, "share": 1, - "submit": 0, "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 + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py index 7d9302382f1..3d7a891f333 100644 --- a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py +++ b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py @@ -25,22 +25,31 @@ product_discount_fields = ['free_item', 'free_qty', 'free_item_uom', class PromotionalScheme(Document): def validate(self): + if not self.selling and not self.buying: + frappe.throw(_("Either 'Selling' or 'Buying' must be selected"), title=_("Mandatory")) if not (self.price_discount_slabs or self.product_discount_slabs): frappe.throw(_("Price or product discount slabs are required")) def on_update(self): - data = frappe.get_all('Pricing Rule', fields = ["promotional_scheme_id", "name"], - filters = {'promotional_scheme': self.name}) or {} + pricing_rules = frappe.get_all( + 'Pricing Rule', + fields = ["promotional_scheme_id", "name", "creation"], + filters = { + 'promotional_scheme': self.name, + 'applicable_for': self.applicable_for + }, + order_by = 'creation asc', + ) or {} + self.update_pricing_rules(pricing_rules) - self.update_pricing_rules(data) - - def update_pricing_rules(self, data): + def update_pricing_rules(self, pricing_rules): rules = {} count = 0 - - for d in data: - rules[d.get('promotional_scheme_id')] = d.get('name') + names = [] + for rule in pricing_rules: + names.append(rule.name) + rules[rule.get('promotional_scheme_id')] = names docs = get_pricing_rules(self, rules) @@ -57,9 +66,9 @@ class PromotionalScheme(Document): frappe.msgprint(_("New {0} pricing rules are created").format(count)) def on_trash(self): - for d in frappe.get_all('Pricing Rule', + for rule in frappe.get_all('Pricing Rule', {'promotional_scheme': self.name}): - frappe.delete_doc('Pricing Rule', d.name) + frappe.delete_doc('Pricing Rule', rule.name) def get_pricing_rules(doc, rules = {}): new_doc = [] @@ -73,42 +82,80 @@ def get_pricing_rules(doc, rules = {}): def _get_pricing_rules(doc, child_doc, discount_fields, rules = {}): new_doc = [] args = get_args_for_pricing_rule(doc) - for d in doc.get(child_doc): + applicable_for = frappe.scrub(doc.get('applicable_for')) + for idx, d in enumerate(doc.get(child_doc)): if d.name in rules: - pr = frappe.get_doc('Pricing Rule', rules.get(d.name)) + for applicable_for_value in args.get(applicable_for): + temp_args = args.copy() + docname = frappe.get_all( + 'Pricing Rule', + fields = ["promotional_scheme_id", "name", applicable_for], + filters = { + 'promotional_scheme_id': d.name, + applicable_for: applicable_for_value + } + ) + + if docname: + pr = frappe.get_doc('Pricing Rule', docname[0].get('name')) + temp_args[applicable_for] = applicable_for_value + pr = set_args(temp_args, pr, doc, child_doc, discount_fields, d) + else: + pr = frappe.new_doc("Pricing Rule") + pr.title = doc.name + temp_args[applicable_for] = applicable_for_value + pr = set_args(temp_args, pr, doc, child_doc, discount_fields, d) + + new_doc.append(pr) + else: - pr = frappe.new_doc("Pricing Rule") - pr.title = make_autoname("{0}/.####".format(doc.name)) - - pr.update(args) - for field in (other_fields + discount_fields): - pr.set(field, d.get(field)) - - pr.promotional_scheme_id = d.name - pr.promotional_scheme = doc.name - pr.disable = d.disable if d.disable else doc.disable - pr.price_or_product_discount = ('Price' - if child_doc == 'price_discount_slabs' else 'Product') - - for field in ['items', 'item_groups', 'brands']: - if doc.get(field): - pr.set(field, []) - - apply_on = frappe.scrub(doc.get('apply_on')) - for d in doc.get(field): - pr.append(field, { - apply_on: d.get(apply_on), - 'uom': d.uom - }) - - new_doc.append(pr) + applicable_for_values = args.get(applicable_for) or [] + for applicable_for_value in applicable_for_values: + pr = frappe.new_doc("Pricing Rule") + pr.title = doc.name + temp_args = args.copy() + temp_args[applicable_for] = applicable_for_value + pr = set_args(temp_args, pr, doc, child_doc, discount_fields, d) + new_doc.append(pr) return new_doc + + + +def set_args(args, pr, doc, child_doc, discount_fields, child_doc_fields): + pr.update(args) + for field in (other_fields + discount_fields): + pr.set(field, child_doc_fields.get(field)) + + pr.promotional_scheme_id = child_doc_fields.name + pr.promotional_scheme = doc.name + pr.disable = child_doc_fields.disable if child_doc_fields.disable else doc.disable + pr.price_or_product_discount = ('Price' + if child_doc == 'price_discount_slabs' else 'Product') + + for field in ['items', 'item_groups', 'brands']: + if doc.get(field): + pr.set(field, []) + + apply_on = frappe.scrub(doc.get('apply_on')) + for d in doc.get(field): + pr.append(field, { + apply_on: d.get(apply_on), + 'uom': d.uom + }) + return pr + def get_args_for_pricing_rule(doc): args = { 'promotional_scheme': doc.name } + applicable_for = frappe.scrub(doc.get('applicable_for')) for d in pricing_rule_fields: - args[d] = doc.get(d) - + if d == applicable_for: + items = [] + for applicable_for_values in doc.get(applicable_for): + items.append(applicable_for_values.get(applicable_for)) + args[d] = items + else: + args[d] = doc.get(d) return args diff --git a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme_dashboard.py b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme_dashboard.py index 28c4c61b9fa..54fedb77387 100644 --- a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme_dashboard.py +++ b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme_dashboard.py @@ -9,4 +9,4 @@ def get_data(): 'items': ['Pricing Rule'] } ] - } \ No newline at end of file + } diff --git a/erpnext/accounts/doctype/promotional_scheme/test_promotional_scheme.py b/erpnext/accounts/doctype/promotional_scheme/test_promotional_scheme.py index 8dc04997796..286f7cf6edd 100644 --- a/erpnext/accounts/doctype/promotional_scheme/test_promotional_scheme.py +++ b/erpnext/accounts/doctype/promotional_scheme/test_promotional_scheme.py @@ -7,4 +7,54 @@ import frappe import unittest class TestPromotionalScheme(unittest.TestCase): - pass + def test_promotional_scheme(self): + ps = make_promotional_scheme() + price_rules = frappe.get_all('Pricing Rule', fields = ["promotional_scheme_id", "name", "creation"], + filters = {'promotional_scheme': ps.name}) + self.assertTrue(len(price_rules),1) + price_doc_details = frappe.db.get_value('Pricing Rule', price_rules[0].name, ['customer', 'min_qty', 'discount_percentage'], as_dict = 1) + self.assertTrue(price_doc_details.customer, '_Test Customer') + self.assertTrue(price_doc_details.min_qty, 4) + self.assertTrue(price_doc_details.discount_percentage, 20) + + ps.price_discount_slabs[0].min_qty = 6 + ps.append('customer', { + 'customer': "_Test Customer 2"}) + ps.save() + price_rules = frappe.get_all('Pricing Rule', fields = ["promotional_scheme_id", "name"], + filters = {'promotional_scheme': ps.name}) + self.assertTrue(len(price_rules), 2) + + price_doc_details = frappe.db.get_value('Pricing Rule', price_rules[1].name, ['customer', 'min_qty', 'discount_percentage'], as_dict = 1) + self.assertTrue(price_doc_details.customer, '_Test Customer 2') + self.assertTrue(price_doc_details.min_qty, 6) + self.assertTrue(price_doc_details.discount_percentage, 20) + + price_doc_details = frappe.db.get_value('Pricing Rule', price_rules[0].name, ['customer', 'min_qty', 'discount_percentage'], as_dict = 1) + self.assertTrue(price_doc_details.customer, '_Test Customer') + self.assertTrue(price_doc_details.min_qty, 6) + + frappe.delete_doc('Promotional Scheme', ps.name) + price_rules = frappe.get_all('Pricing Rule', fields = ["promotional_scheme_id", "name"], + filters = {'promotional_scheme': ps.name}) + self.assertEqual(price_rules, []) + +def make_promotional_scheme(): + ps = frappe.new_doc('Promotional Scheme') + ps.name = '_Test Scheme' + ps.append('items',{ + 'item_code': '_Test Item' + }) + ps.selling = 1 + ps.append('price_discount_slabs',{ + 'min_qty': 4, + 'discount_percentage': 20, + 'rule_description': 'Test' + }) + ps.applicable_for = 'Customer' + ps.append('customer',{ + 'customer': "_Test Customer" + }) + ps.save() + + return ps diff --git a/erpnext/accounts/doctype/promotional_scheme_price_discount/promotional_scheme_price_discount.json b/erpnext/accounts/doctype/promotional_scheme_price_discount/promotional_scheme_price_discount.json index 795fb1c6f46..a70d5c9d430 100644 --- a/erpnext/accounts/doctype/promotional_scheme_price_discount/promotional_scheme_price_discount.json +++ b/erpnext/accounts/doctype/promotional_scheme_price_discount/promotional_scheme_price_discount.json @@ -106,7 +106,6 @@ "depends_on": "eval:doc.rate_or_discount==\"Rate\"", "fieldname": "rate", "fieldtype": "Currency", - "in_list_view": 1, "label": "Rate" }, { @@ -170,7 +169,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-03-07 11:56:23.424137", + "modified": "2021-08-19 15:49:29.598727", "modified_by": "Administrator", "module": "Accounts", "name": "Promotional Scheme Price Discount", diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index dc9094c3e91..aef9243aad0 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -134,7 +134,7 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({ }, get_query_filters: { docstatus: 1, - status: ["not in", ["Closed", "Completed"]], + status: ["not in", ["Closed", "Completed", "Return Issued"]], company: me.frm.doc.company, is_return: 0 } @@ -283,7 +283,8 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({ party: this.frm.doc.supplier, party_type: "Supplier", account: this.frm.doc.credit_to, - price_list: this.frm.doc.buying_price_list + price_list: this.frm.doc.buying_price_list, + fetch_payment_terms_template: cint(!this.frm.doc.ignore_default_payment_terms_template) }, function() { me.apply_pricing_rule(); me.frm.doc.apply_tds = me.frm.supplier_tds ? 1 : 0; @@ -365,7 +366,7 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({ items_add: function(doc, cdt, cdn) { var row = frappe.get_doc(cdt, cdn); this.frm.script_manager.copy_from_first_row("items", row, - ["expense_account", "cost_center", "project"]); + ["expense_account", "discount_account", "cost_center", "project"]); }, on_submit: function() { @@ -499,6 +500,16 @@ frappe.ui.form.on("Purchase Invoice", { 'Payment Entry': 'Payment' } + frm.set_query("additional_discount_account", function() { + return { + filters: { + company: frm.doc.company, + is_group: 0, + report_type: "Profit and Loss", + } + }; + }); + frm.fields_dict['items'].grid.get_field('deferred_expense_account').get_query = function(doc) { return { filters: { @@ -508,6 +519,16 @@ frappe.ui.form.on("Purchase Invoice", { } } } + + frm.fields_dict['items'].grid.get_field('discount_account').get_query = function(doc) { + return { + filters: { + 'report_type': 'Profit and Loss', + 'company': doc.company, + "is_group": 0 + } + } + } }, refresh: function(frm) { diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index 00ef7d5c184..7822f747f64 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -96,6 +96,7 @@ "section_break_44", "apply_discount_on", "base_discount_amount", + "additional_discount_account", "column_break_46", "additional_discount_percentage", "discount_amount", @@ -131,6 +132,7 @@ "advances", "payment_schedule_section", "payment_terms_template", + "ignore_default_payment_terms_template", "payment_schedule", "terms_section_break", "tc_name", @@ -175,7 +177,9 @@ "hidden": 1, "label": "Title", "no_copy": 1, - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "naming_series", @@ -187,7 +191,9 @@ "options": "ACC-PINV-.YYYY.-\nACC-PINV-RET-.YYYY.-", "print_hide": 1, "reqd": 1, - "set_only_once": 1 + "set_only_once": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "supplier", @@ -199,7 +205,9 @@ "options": "Supplier", "print_hide": 1, "reqd": 1, - "search_index": 1 + "search_index": 1, + "show_days": 1, + "show_seconds": 1 }, { "bold": 1, @@ -211,7 +219,9 @@ "label": "Supplier Name", "oldfieldname": "supplier_name", "oldfieldtype": "Data", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fetch_from": "supplier.tax_id", @@ -219,21 +229,27 @@ "fieldtype": "Read Only", "label": "Tax Id", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "due_date", "fieldtype": "Date", "label": "Due Date", "oldfieldname": "due_date", - "oldfieldtype": "Date" + "oldfieldtype": "Date", + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "fieldname": "is_paid", "fieldtype": "Check", "label": "Is Paid", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "default": "0", @@ -241,19 +257,25 @@ "fieldtype": "Check", "label": "Is Return (Debit Note)", "no_copy": 1, - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "fieldname": "apply_tds", "fieldtype": "Check", "label": "Apply Tax Withholding Amount", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break1", "fieldtype": "Column Break", "oldfieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1, "width": "50%" }, { @@ -263,13 +285,17 @@ "label": "Company", "options": "Company", "print_hide": 1, - "remember_last_selected_value": 1 + "remember_last_selected_value": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "cost_center", "fieldtype": "Link", "label": "Cost Center", - "options": "Cost Center" + "options": "Cost Center", + "show_days": 1, + "show_seconds": 1 }, { "default": "Today", @@ -281,7 +307,9 @@ "oldfieldtype": "Date", "print_hide": 1, "reqd": 1, - "search_index": 1 + "search_index": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "posting_time", @@ -290,6 +318,8 @@ "no_copy": 1, "print_hide": 1, "print_width": "100px", + "show_days": 1, + "show_seconds": 1, "width": "100px" }, { @@ -298,7 +328,9 @@ "fieldname": "set_posting_time", "fieldtype": "Check", "label": "Edit Posting Date and Time", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "amended_from", @@ -310,44 +342,58 @@ "oldfieldtype": "Link", "options": "Purchase Invoice", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "collapsible_depends_on": "eval:doc.on_hold", "fieldname": "sb_14", "fieldtype": "Section Break", - "label": "Hold Invoice" + "label": "Hold Invoice", + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "fieldname": "on_hold", "fieldtype": "Check", - "label": "Hold Invoice" + "label": "Hold Invoice", + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:doc.on_hold", "description": "Once set, this invoice will be on hold till the set date", "fieldname": "release_date", "fieldtype": "Date", - "label": "Release Date" + "label": "Release Date", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "cb_17", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:doc.on_hold", "fieldname": "hold_comment", "fieldtype": "Small Text", - "label": "Reason For Putting On Hold" + "label": "Reason For Putting On Hold", + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "collapsible_depends_on": "bill_no", "fieldname": "supplier_invoice_details", "fieldtype": "Section Break", - "label": "Supplier Invoice Details" + "label": "Supplier Invoice Details", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "bill_no", @@ -355,11 +401,15 @@ "label": "Supplier Invoice No", "oldfieldname": "bill_no", "oldfieldtype": "Data", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_15", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "bill_date", @@ -368,13 +418,17 @@ "no_copy": 1, "oldfieldname": "bill_date", "oldfieldtype": "Date", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "return_against", "fieldname": "returns", "fieldtype": "Section Break", - "label": "Returns" + "label": "Returns", + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "return_against", @@ -384,26 +438,34 @@ "no_copy": 1, "options": "Purchase Invoice", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "fieldname": "section_addresses", "fieldtype": "Section Break", - "label": "Address and Contact" + "label": "Address and Contact", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "supplier_address", "fieldtype": "Link", "label": "Select Supplier Address", "options": "Address", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "address_display", "fieldtype": "Small Text", "label": "Address", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "contact_person", @@ -411,51 +473,67 @@ "in_global_search": 1, "label": "Contact Person", "options": "Contact", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "contact_display", "fieldtype": "Small Text", "label": "Contact", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "contact_mobile", "fieldtype": "Small Text", "label": "Mobile No", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "contact_email", "fieldtype": "Small Text", "label": "Contact Email", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "col_break_address", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "shipping_address", "fieldtype": "Link", "label": "Select Shipping Address", "options": "Address", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "shipping_address_display", "fieldtype": "Small Text", "label": "Shipping Address", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "fieldname": "currency_and_price_list", "fieldtype": "Section Break", "label": "Currency and Price List", - "options": "fa fa-tag" + "options": "fa fa-tag", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "currency", @@ -464,7 +542,9 @@ "oldfieldname": "currency", "oldfieldtype": "Select", "options": "Currency", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "conversion_rate", @@ -473,18 +553,24 @@ "oldfieldname": "conversion_rate", "oldfieldtype": "Currency", "precision": "9", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break2", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "buying_price_list", "fieldtype": "Link", "label": "Price List", "options": "Price List", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "price_list_currency", @@ -492,14 +578,18 @@ "label": "Price List Currency", "options": "Currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "plc_conversion_rate", "fieldtype": "Float", "label": "Price List Exchange Rate", "precision": "9", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "default": "0", @@ -508,11 +598,15 @@ "label": "Ignore Pricing Rule", "no_copy": 1, "permlevel": 1, - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "sec_warehouse", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "update_stock", @@ -521,7 +615,9 @@ "fieldtype": "Link", "label": "Set Accepted Warehouse", "options": "Warehouse", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "update_stock", @@ -531,11 +627,15 @@ "label": "Rejected Warehouse", "no_copy": 1, "options": "Warehouse", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "col_break_warehouse", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "default": "No", @@ -543,25 +643,32 @@ "fieldtype": "Select", "label": "Raw Materials Supplied", "options": "No\nYes", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "items_section", "fieldtype": "Section Break", "oldfieldtype": "Section Break", - "options": "fa fa-shopping-cart" + "options": "fa fa-shopping-cart", + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "fieldname": "update_stock", "fieldtype": "Check", "label": "Update Stock", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "scan_barcode", "fieldtype": "Data", - "label": "Scan Barcode" + "label": "Scan Barcode", + "options": "Barcode" }, { "allow_bulk_edit": 1, @@ -571,25 +678,33 @@ "oldfieldname": "entries", "oldfieldtype": "Table", "options": "Purchase Invoice Item", - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "pricing_rule_details", "fieldtype": "Section Break", - "label": "Pricing Rules" + "label": "Pricing Rules", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "pricing_rules", "fieldtype": "Table", "label": "Pricing Rule Detail", "options": "Pricing Rule Detail", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible_depends_on": "supplied_items", "fieldname": "raw_materials_supplied", "fieldtype": "Section Break", - "label": "Raw Materials Supplied" + "label": "Raw Materials Supplied", + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "update_stock", @@ -597,17 +712,23 @@ "fieldtype": "Table", "label": "Supplied Items", "no_copy": 1, - "options": "Purchase Receipt Item Supplied" + "options": "Purchase Receipt Item Supplied", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "section_break_26", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "total_qty", "fieldtype": "Float", "label": "Total Quantity", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_total", @@ -615,7 +736,9 @@ "label": "Total (Company Currency)", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_net_total", @@ -625,18 +748,24 @@ "oldfieldtype": "Currency", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_28", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "total", "fieldtype": "Currency", "label": "Total", "options": "currency", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "net_total", @@ -646,42 +775,56 @@ "oldfieldtype": "Currency", "options": "currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "total_net_weight", "fieldtype": "Float", "label": "Total Net Weight", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "taxes_section", "fieldtype": "Section Break", "oldfieldtype": "Section Break", - "options": "fa fa-money" + "options": "fa fa-money", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "tax_category", "fieldtype": "Link", "label": "Tax Category", "options": "Tax Category", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_49", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "shipping_rule", "fieldtype": "Link", "label": "Shipping Rule", "options": "Shipping Rule", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "section_break_51", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "taxes_and_charges", @@ -690,7 +833,9 @@ "oldfieldname": "purchase_other_charges", "oldfieldtype": "Link", "options": "Purchase Taxes and Charges Template", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "taxes", @@ -698,13 +843,17 @@ "label": "Purchase Taxes and Charges", "oldfieldname": "purchase_tax_details", "oldfieldtype": "Table", - "options": "Purchase Taxes and Charges" + "options": "Purchase Taxes and Charges", + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "fieldname": "sec_tax_breakup", "fieldtype": "Section Break", - "label": "Tax Breakup" + "label": "Tax Breakup", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "other_charges_calculation", @@ -713,13 +862,17 @@ "no_copy": 1, "oldfieldtype": "HTML", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "totals", "fieldtype": "Section Break", "oldfieldtype": "Section Break", - "options": "fa fa-money" + "options": "fa fa-money", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_taxes_and_charges_added", @@ -729,7 +882,9 @@ "oldfieldtype": "Currency", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_taxes_and_charges_deducted", @@ -739,7 +894,9 @@ "oldfieldtype": "Currency", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_total_taxes_and_charges", @@ -749,11 +906,15 @@ "oldfieldtype": "Currency", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_40", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "taxes_and_charges_added", @@ -763,7 +924,9 @@ "oldfieldtype": "Currency", "options": "currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "taxes_and_charges_deducted", @@ -773,7 +936,9 @@ "oldfieldtype": "Currency", "options": "currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "total_taxes_and_charges", @@ -781,14 +946,18 @@ "label": "Total Taxes and Charges", "options": "currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "collapsible_depends_on": "discount_amount", "fieldname": "section_break_44", "fieldtype": "Section Break", - "label": "Additional Discount" + "label": "Additional Discount", + "show_days": 1, + "show_seconds": 1 }, { "default": "Grand Total", @@ -796,7 +965,9 @@ "fieldtype": "Select", "label": "Apply Additional Discount On", "options": "\nGrand Total\nNet Total", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_discount_amount", @@ -804,28 +975,38 @@ "label": "Additional Discount Amount (Company Currency)", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_46", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "additional_discount_percentage", "fieldtype": "Float", "label": "Additional Discount Percentage", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "discount_amount", "fieldtype": "Currency", "label": "Additional Discount Amount", "options": "currency", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "section_break_49", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_grand_total", @@ -835,7 +1016,9 @@ "oldfieldtype": "Currency", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:!doc.disable_rounded_total", @@ -845,7 +1028,9 @@ "no_copy": 1, "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:!doc.disable_rounded_total", @@ -855,7 +1040,9 @@ "no_copy": 1, "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_in_words", @@ -865,13 +1052,17 @@ "oldfieldname": "in_words", "oldfieldtype": "Data", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break8", "fieldtype": "Column Break", "oldfieldtype": "Column Break", "print_hide": 1, + "show_days": 1, + "show_seconds": 1, "width": "50%" }, { @@ -882,7 +1073,9 @@ "oldfieldname": "grand_total_import", "oldfieldtype": "Currency", "options": "currency", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:!doc.disable_rounded_total", @@ -892,7 +1085,9 @@ "no_copy": 1, "options": "currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:!doc.disable_rounded_total", @@ -902,7 +1097,9 @@ "no_copy": 1, "options": "currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "in_words", @@ -912,7 +1109,9 @@ "oldfieldname": "in_words_import", "oldfieldtype": "Data", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "total_advance", @@ -923,7 +1122,9 @@ "oldfieldtype": "Currency", "options": "party_account_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "outstanding_amount", @@ -934,14 +1135,18 @@ "oldfieldtype": "Currency", "options": "party_account_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "depends_on": "grand_total", "fieldname": "disable_rounded_total", "fieldtype": "Check", - "label": "Disable Rounded Total" + "label": "Disable Rounded Total", + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, @@ -949,20 +1154,26 @@ "depends_on": "eval:doc.is_paid===1||(doc.advances && doc.advances.length>0)", "fieldname": "payments_section", "fieldtype": "Section Break", - "label": "Payments" + "label": "Payments", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "mode_of_payment", "fieldtype": "Link", "label": "Mode of Payment", "options": "Mode of Payment", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "cash_bank_account", "fieldtype": "Link", "label": "Cash/Bank Account", - "options": "Account" + "options": "Account", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "clearance_date", @@ -970,11 +1181,15 @@ "label": "Clearance Date", "no_copy": 1, "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "col_br_payments", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "is_paid", @@ -983,7 +1198,9 @@ "label": "Paid Amount", "no_copy": 1, "options": "currency", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_paid_amount", @@ -992,7 +1209,9 @@ "no_copy": 1, "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, @@ -1000,7 +1219,9 @@ "depends_on": "grand_total", "fieldname": "write_off", "fieldtype": "Section Break", - "label": "Write Off" + "label": "Write Off", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "write_off_amount", @@ -1008,7 +1229,9 @@ "label": "Write Off Amount", "no_copy": 1, "options": "currency", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_write_off_amount", @@ -1017,11 +1240,15 @@ "no_copy": 1, "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_61", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:flt(doc.write_off_amount)!=0", @@ -1029,7 +1256,9 @@ "fieldtype": "Link", "label": "Write Off Account", "options": "Account", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:flt(doc.write_off_amount)!=0", @@ -1037,7 +1266,9 @@ "fieldtype": "Link", "label": "Write Off Cost Center", "options": "Cost Center", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, @@ -1047,13 +1278,17 @@ "label": "Advance Payments", "oldfieldtype": "Section Break", "options": "fa fa-money", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "fieldname": "allocate_advances_automatically", "fieldtype": "Check", - "label": "Set Advances and Allocate (FIFO)" + "label": "Set Advances and Allocate (FIFO)", + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:!doc.allocate_advances_automatically", @@ -1061,7 +1296,9 @@ "fieldtype": "Button", "label": "Get Advances Paid", "oldfieldtype": "Button", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "advances", @@ -1071,20 +1308,26 @@ "oldfieldname": "advance_allocation_details", "oldfieldtype": "Table", "options": "Purchase Invoice Advance", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "collapsible_depends_on": "eval:(!doc.is_return)", "fieldname": "payment_schedule_section", "fieldtype": "Section Break", - "label": "Payment Terms" + "label": "Payment Terms", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "payment_terms_template", "fieldtype": "Link", "label": "Payment Terms Template", - "options": "Payment Terms Template" + "options": "Payment Terms Template", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "payment_schedule", @@ -1092,7 +1335,9 @@ "label": "Payment Schedule", "no_copy": 1, "options": "Payment Schedule", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, @@ -1100,25 +1345,33 @@ "fieldname": "terms_section_break", "fieldtype": "Section Break", "label": "Terms and Conditions", - "options": "fa fa-legal" + "options": "fa fa-legal", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "tc_name", "fieldtype": "Link", "label": "Terms", "options": "Terms and Conditions", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "terms", "fieldtype": "Text Editor", - "label": "Terms and Conditions1" + "label": "Terms and Conditions1", + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "fieldname": "printing_settings", "fieldtype": "Section Break", - "label": "Printing Settings" + "label": "Printing Settings", + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, @@ -1126,7 +1379,9 @@ "fieldtype": "Link", "label": "Letter Head", "options": "Letter Head", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, @@ -1134,11 +1389,15 @@ "fieldname": "group_same_items", "fieldtype": "Check", "label": "Group same items", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_112", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, @@ -1150,14 +1409,18 @@ "oldfieldtype": "Link", "options": "Print Heading", "print_hide": 1, - "report_hide": 1 + "report_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "language", "fieldtype": "Data", "label": "Print Language", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, @@ -1166,7 +1429,9 @@ "label": "More Information", "oldfieldtype": "Section Break", "options": "fa fa-file-text", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "credit_to", @@ -1177,7 +1442,9 @@ "options": "Account", "print_hide": 1, "reqd": 1, - "search_index": 1 + "search_index": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "party_account_currency", @@ -1187,7 +1454,9 @@ "no_copy": 1, "options": "Currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "default": "No", @@ -1197,7 +1466,9 @@ "oldfieldname": "is_opening", "oldfieldtype": "Select", "options": "No\nYes", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "against_expense_account", @@ -1207,11 +1478,15 @@ "no_copy": 1, "oldfieldname": "against_expense_account", "oldfieldtype": "Small Text", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_63", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "default": "Draft", @@ -1220,7 +1495,9 @@ "in_standard_filter": 1, "label": "Status", "options": "\nDraft\nReturn\nDebit Note Issued\nSubmitted\nPaid\nUnpaid\nOverdue\nCancelled\nInternal Transfer", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "inter_company_invoice_reference", @@ -1229,7 +1506,9 @@ "no_copy": 1, "options": "Sales Invoice", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "remarks", @@ -1238,14 +1517,18 @@ "no_copy": 1, "oldfieldname": "remarks", "oldfieldtype": "Text", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "fieldname": "subscription_section", "fieldtype": "Section Break", "label": "Subscription Section", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, @@ -1254,7 +1537,9 @@ "fieldtype": "Date", "label": "From Date", "no_copy": 1, - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, @@ -1263,11 +1548,15 @@ "fieldtype": "Date", "label": "To Date", "no_copy": 1, - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_114", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "auto_repeat", @@ -1276,24 +1565,32 @@ "no_copy": 1, "options": "Auto Repeat", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, "depends_on": "eval: doc.auto_repeat", "fieldname": "update_auto_repeat_reference", "fieldtype": "Button", - "label": "Update Auto Repeat Reference" + "label": "Update Auto Repeat Reference", + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "fieldname": "accounting_dimensions_section", "fieldtype": "Section Break", - "label": "Accounting Dimensions " + "label": "Accounting Dimensions ", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "dimension_col_break", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "default": "0", @@ -1301,7 +1598,9 @@ "fieldname": "is_internal_supplier", "fieldtype": "Check", "label": "Is Internal Supplier", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "tax_withholding_category", @@ -1309,25 +1608,33 @@ "hidden": 1, "label": "Tax Withholding Category", "options": "Tax Withholding Category", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "billing_address", "fieldtype": "Link", "label": "Select Billing Address", - "options": "Address" + "options": "Address", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "billing_address_display", "fieldtype": "Small Text", "label": "Billing Address", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "project", "fieldtype": "Link", "label": "Project", - "options": "Project" + "options": "Project", + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:doc.is_internal_supplier", @@ -1335,7 +1642,9 @@ "fieldname": "unrealized_profit_loss_account", "fieldtype": "Link", "label": "Unrealized Profit / Loss Account", - "options": "Account" + "options": "Account", + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:doc.is_internal_supplier", @@ -1344,7 +1653,9 @@ "fieldname": "represents_company", "fieldtype": "Link", "label": "Represents Company", - "options": "Company" + "options": "Company", + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:doc.update_stock && doc.is_internal_supplier", @@ -1356,6 +1667,8 @@ "options": "Warehouse", "print_hide": 1, "print_width": "50px", + "show_days": 1, + "show_seconds": 1, "width": "50px" }, { @@ -1367,6 +1680,8 @@ "options": "Warehouse", "print_hide": 1, "print_width": "50px", + "show_days": 1, + "show_seconds": 1, "width": "50px" }, { @@ -1377,13 +1692,29 @@ "no_copy": 1, "print_hide": 1, "read_only": 1 + }, + { + "fieldname": "additional_discount_account", + "fieldtype": "Link", + "label": "Additional Discount Account", + "options": "Account" + }, + { + "default": "0", + "fieldname": "ignore_default_payment_terms_template", + "fieldtype": "Check", + "hidden": 1, + "label": "Ignore Default Payment Terms Template", + "read_only": 1, + "show_days": 1, + "show_seconds": 1 } ], "icon": "fa fa-file-text", "idx": 204, "is_submittable": 1, "links": [], - "modified": "2021-06-15 18:20:56.806195", + "modified": "2021-08-17 20:16:12.737743", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 45d89ad1c87..fdd8765b411 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -22,7 +22,7 @@ from erpnext.assets.doctype.asset.asset import get_asset_account, is_cwip_accoun from frappe.model.mapper import get_mapped_doc from six import iteritems from erpnext.accounts.doctype.sales_invoice.sales_invoice import validate_inter_company_party, update_linked_doc,\ - unlink_inter_company_doc + unlink_inter_company_doc, check_if_return_invoice_linked_with_payment_entry from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import get_party_tax_withholding_details from erpnext.accounts.deferred_revenue import validate_service_stop_date from erpnext.stock.doctype.purchase_receipt.purchase_receipt import get_item_account_wise_additional_cost @@ -413,6 +413,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() @@ -446,11 +448,13 @@ class PurchaseInvoice(BuyingController): self.make_supplier_gl_entry(gl_entries) self.make_item_gl_entries(gl_entries) + self.make_discount_gl_entries(gl_entries) if self.check_asset_cwip_enabled(): self.get_asset_gl_entry(gl_entries) self.make_tax_gl_entries(gl_entries) + self.make_exchange_gain_loss_gl_entries(gl_entries) self.make_internal_transfer_gl_entries(gl_entries) self.allocate_advance_taxes(gl_entries) @@ -607,7 +611,7 @@ class PurchaseInvoice(BuyingController): if (not item.enable_deferred_expense or self.is_return) else item.deferred_expense_account) if not item.is_fixed_asset: - amount = flt(item.base_net_amount, item.precision("base_net_amount")) + dummy, amount = self.get_amount_and_base_amount(item, self.enable_discount_accounting) else: amount = flt(item.base_net_amount + item.item_tax_amount, item.precision("base_net_amount")) @@ -821,8 +825,10 @@ class PurchaseInvoice(BuyingController): def make_tax_gl_entries(self, gl_entries): # tax table gl entries valuation_tax = {} + for tax in self.get("taxes"): - if tax.category in ("Total", "Valuation and Total") and flt(tax.base_tax_amount_after_discount_amount): + amount, base_amount = self.get_tax_amounts(tax, self.enable_discount_accounting) + if tax.category in ("Total", "Valuation and Total") and flt(base_amount): account_currency = get_account_currency(tax.account_head) dr_or_cr = "debit" if tax.add_deduct_tax == "Add" else "credit" @@ -831,21 +837,21 @@ class PurchaseInvoice(BuyingController): self.get_gl_dict({ "account": tax.account_head, "against": self.supplier, - dr_or_cr: tax.base_tax_amount_after_discount_amount, - dr_or_cr + "_in_account_currency": tax.base_tax_amount_after_discount_amount \ - if account_currency==self.company_currency \ - else tax.tax_amount_after_discount_amount, + dr_or_cr: base_amount, + dr_or_cr + "_in_account_currency": base_amount + if account_currency==self.company_currency + else amount, "cost_center": tax.cost_center }, account_currency, item=tax) ) # accumulate valuation tax - if self.is_opening == "No" and tax.category in ("Valuation", "Valuation and Total") and flt(tax.base_tax_amount_after_discount_amount) \ + if self.is_opening == "No" and tax.category in ("Valuation", "Valuation and Total") and flt(base_amount) \ and not self.is_internal_transfer(): if self.auto_accounting_for_stock and not tax.cost_center: frappe.throw(_("Cost Center is required in row {0} in Taxes table for type {1}").format(tax.idx, _(tax.category))) valuation_tax.setdefault(tax.name, 0) valuation_tax[tax.name] += \ - (tax.add_deduct_tax == "Add" and 1 or -1) * flt(tax.base_tax_amount_after_discount_amount) + (tax.add_deduct_tax == "Add" and 1 or -1) * flt(base_amount) if self.is_opening == "No" and self.negative_expense_to_be_booked and valuation_tax: # credit valuation tax amount in "Expenses Included In Valuation" @@ -886,6 +892,13 @@ class PurchaseInvoice(BuyingController): "remarks": self.remarks or "Accounting Entry for Stock" }, item=tax)) + @property + def enable_discount_accounting(self): + if not hasattr(self, "_enable_discount_accounting"): + self._enable_discount_accounting = cint(frappe.db.get_single_value('Accounts Settings', 'enable_discount_accounting')) + + return self._enable_discount_accounting + def make_internal_transfer_gl_entries(self, gl_entries): if self.is_internal_transfer() and flt(self.base_total_taxes_and_charges): account_currency = get_account_currency(self.unrealized_profit_loss_account) @@ -981,6 +994,8 @@ class PurchaseInvoice(BuyingController): }, item=self)) def on_cancel(self): + check_if_return_invoice_linked_with_payment_entry(self) + super(PurchaseInvoice, self).on_cancel() self.check_on_hold_or_closed_status() diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_dashboard.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_dashboard.py index 173939df008..b6467a3d5ca 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_dashboard.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_dashboard.py @@ -34,4 +34,4 @@ def get_data(): 'items': ['Auto Repeat'] }, ] - } \ No newline at end of file + } diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_list.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_list.js index 914a2457d45..771b49ac629 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_list.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_list.js @@ -26,4 +26,4 @@ frappe.listview_settings['Purchase Invoice'] = { return [__("Paid"), "green", "outstanding_amount,=,0"]; } } -}; \ No newline at end of file +}; diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.js index b470051b51d..94b3b9ed338 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.js @@ -72,4 +72,3 @@ QUnit.test("test purchase invoice", function(assert) { () => done() ]); }); - diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 311745d3cd8..bfe34bb4a73 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -230,6 +230,50 @@ class TestPurchaseInvoice(unittest.TestCase): self.assertEqual(expected_values[gle.account][1], gle.debit) self.assertEqual(expected_values[gle.account][2], gle.credit) + def test_purchase_invoice_with_discount_accounting_enabled(self): + enable_discount_accounting() + + discount_account = create_account(account_name="Discount Account", + parent_account="Indirect Expenses - _TC", company="_Test Company") + pi = make_purchase_invoice(discount_account=discount_account, rate=45) + + expected_gle = [ + ["_Test Account Cost for Goods Sold - _TC", 250.0, 0.0, nowdate()], + ["Creditors - _TC", 0.0, 225.0, nowdate()], + ["Discount Account - _TC", 0.0, 25.0, nowdate()] + ] + + check_gl_entries(self, pi.name, expected_gle, nowdate()) + enable_discount_accounting(enable=0) + + def test_additional_discount_for_purchase_invoice_with_discount_accounting_enabled(self): + enable_discount_accounting() + additional_discount_account = create_account(account_name="Discount Account", + parent_account="Indirect Expenses - _TC", company="_Test Company") + + pi = make_purchase_invoice(do_not_save=1, parent_cost_center="Main - _TC") + pi.apply_discount_on = "Grand Total" + pi.additional_discount_account = additional_discount_account + pi.additional_discount_percentage = 10 + pi.disable_rounded_total = 1 + pi.append("taxes", { + "charge_type": "On Net Total", + "account_head": "_Test Account VAT - _TC", + "cost_center": "Main - _TC", + "description": "Test", + "rate": 10 + }) + pi.submit() + + expected_gle = [ + ["_Test Account Cost for Goods Sold - _TC", 250.0, 0.0, nowdate()], + ["_Test Account VAT - _TC", 25.0, 0.0, nowdate()], + ["Creditors - _TC", 0.0, 247.5, nowdate()], + ["Discount Account - _TC", 0.0, 27.5, nowdate()] + ] + + check_gl_entries(self, pi.name, expected_gle, nowdate()) + def test_purchase_invoice_change_naming_series(self): pi = frappe.copy_doc(test_records[1]) pi.insert() @@ -953,6 +997,120 @@ class TestPurchaseInvoice(unittest.TestCase): acc_settings.submit_journal_entriessubmit_journal_entries = 0 acc_settings.save() + def test_gain_loss_with_advance_entry(self): + unlink_enabled = frappe.db.get_value( + "Accounts Settings", "Accounts Settings", + "unlink_payment_on_cancel_of_invoice") + + frappe.db.set_value( + "Accounts Settings", "Accounts Settings", + "unlink_payment_on_cancel_of_invoice", 1) + + original_account = frappe.db.get_value("Company", "_Test Company", "exchange_gain_loss_account") + frappe.db.set_value("Company", "_Test Company", "exchange_gain_loss_account", "Exchange Gain/Loss - _TC") + + pay = frappe.get_doc({ + 'doctype': 'Payment Entry', + 'company': '_Test Company', + 'payment_type': 'Pay', + 'party_type': 'Supplier', + 'party': '_Test Supplier USD', + 'paid_to': '_Test Payable USD - _TC', + 'paid_from': 'Cash - _TC', + 'paid_amount': 70000, + 'target_exchange_rate': 70, + 'received_amount': 1000, + }) + pay.insert() + pay.submit() + + pi = make_purchase_invoice(supplier='_Test Supplier USD', currency="USD", + conversion_rate=75, rate=500, do_not_save=1, qty=1) + pi.cost_center = "_Test Cost Center - _TC" + pi.advances = [] + pi.append("advances", { + "reference_type": "Payment Entry", + "reference_name": pay.name, + "advance_amount": 1000, + "remarks": pay.remarks, + "allocated_amount": 500, + "ref_exchange_rate": 70 + }) + pi.save() + pi.submit() + + expected_gle = [ + ["_Test Account Cost for Goods Sold - _TC", 37500.0], + ["_Test Payable USD - _TC", -35000.0], + ["Exchange Gain/Loss - _TC", -2500.0] + ] + + gl_entries = frappe.db.sql(""" + select account, sum(debit - credit) as balance from `tabGL Entry` + where voucher_no=%s + group by account + order by account asc""", (pi.name), as_dict=1) + + for i, gle in enumerate(gl_entries): + self.assertEqual(expected_gle[i][0], gle.account) + self.assertEqual(expected_gle[i][1], gle.balance) + + pi_2 = make_purchase_invoice(supplier='_Test Supplier USD', currency="USD", + conversion_rate=73, rate=500, do_not_save=1, qty=1) + pi_2.cost_center = "_Test Cost Center - _TC" + pi_2.advances = [] + pi_2.append("advances", { + "reference_type": "Payment Entry", + "reference_name": pay.name, + "advance_amount": 500, + "remarks": pay.remarks, + "allocated_amount": 500, + "ref_exchange_rate": 70 + }) + pi_2.save() + pi_2.submit() + + expected_gle = [ + ["_Test Account Cost for Goods Sold - _TC", 36500.0], + ["_Test Payable USD - _TC", -35000.0], + ["Exchange Gain/Loss - _TC", -1500.0] + ] + + gl_entries = frappe.db.sql(""" + select account, sum(debit - credit) as balance from `tabGL Entry` + where voucher_no=%s + group by account order by account asc""", (pi_2.name), as_dict=1) + + for i, gle in enumerate(gl_entries): + self.assertEqual(expected_gle[i][0], gle.account) + self.assertEqual(expected_gle[i][1], gle.balance) + + expected_gle = [ + ["_Test Payable USD - _TC", 70000.0], + ["Cash - _TC", -70000.0] + ] + + gl_entries = frappe.db.sql(""" + select account, sum(debit - credit) as balance from `tabGL Entry` + where voucher_no=%s and is_cancelled=0 + group by account order by account asc""", (pay.name), as_dict=1) + + for i, gle in enumerate(gl_entries): + self.assertEqual(expected_gle[i][0], gle.account) + self.assertEqual(expected_gle[i][1], gle.balance) + + pi.reload() + pi.cancel() + + pi_2.reload() + pi_2.cancel() + + pay.reload() + pay.cancel() + + frappe.db.set_value("Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice", unlink_enabled) + frappe.db.set_value("Company", "_Test Company", "exchange_gain_loss_account", original_account) + def test_purchase_invoice_advance_taxes(self): from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry @@ -1026,6 +1184,18 @@ class TestPurchaseInvoice(unittest.TestCase): self.assertEqual(expected_gle[i][0], gle.account) self.assertEqual(expected_gle[i][1], gle.amount) +def check_gl_entries(doc, voucher_no, expected_gle, posting_date): + gl_entries = frappe.db.sql("""select account, debit, credit, posting_date + from `tabGL Entry` + where voucher_type='Purchase Invoice' and voucher_no=%s and posting_date >= %s + order by posting_date asc, account asc""", (voucher_no, posting_date), as_dict=1) + + for i, gle in enumerate(gl_entries): + doc.assertEqual(expected_gle[i][0], gle.account) + doc.assertEqual(expected_gle[i][1], gle.debit) + doc.assertEqual(expected_gle[i][2], gle.credit) + doc.assertEqual(getdate(expected_gle[i][3]), gle.posting_date) + def update_tax_witholding_category(company, account, date): from erpnext.accounts.utils import get_fiscal_year @@ -1056,6 +1226,11 @@ def unlink_payment_on_cancel_of_invoice(enable=1): accounts_settings.unlink_payment_on_cancellation_of_invoice = enable accounts_settings.save() +def enable_discount_accounting(enable=1): + accounts_settings = frappe.get_doc("Accounts Settings") + accounts_settings.enable_discount_accounting = enable + accounts_settings.save() + def make_purchase_invoice(**args): pi = frappe.new_doc("Purchase Invoice") args = frappe._dict(args) @@ -1078,6 +1253,7 @@ def make_purchase_invoice(**args): pi.return_against = args.return_against pi.is_subcontracted = args.is_subcontracted or "No" pi.supplier_warehouse = args.supplier_warehouse or "_Test Warehouse 1 - _TC" + pi.cost_center = args.parent_cost_center pi.append("items", { "item_code": args.item or args.item_code or "_Test Item", @@ -1086,7 +1262,10 @@ def make_purchase_invoice(**args): "received_qty": args.received_qty or 0, "rejected_qty": args.rejected_qty or 0, "rate": args.rate or 50, - 'expense_account': args.expense_account or '_Test Account Cost for Goods Sold - _TC', + "price_list_rate": args.price_list_rate or 50, + "expense_account": args.expense_account or '_Test Account Cost for Goods Sold - _TC', + "discount_account": args.discount_account or None, + "discount_amount": args.discount_amount or 0, "conversion_factor": 1.0, "serial_no": args.serial_no, "stock_uom": args.uom or "_Test UOM", diff --git a/erpnext/accounts/doctype/purchase_invoice_advance/purchase_invoice_advance.json b/erpnext/accounts/doctype/purchase_invoice_advance/purchase_invoice_advance.json index 5801b17f66f..63dfff8921f 100644 --- a/erpnext/accounts/doctype/purchase_invoice_advance/purchase_invoice_advance.json +++ b/erpnext/accounts/doctype/purchase_invoice_advance/purchase_invoice_advance.json @@ -1,235 +1,127 @@ { - "allow_copy": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2013-03-08 15:36:46", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Document", - "editable_grid": 1, + "actions": [], + "creation": "2013-03-08 15:36:46", + "doctype": "DocType", + "document_type": "Document", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "reference_type", + "reference_name", + "remarks", + "reference_row", + "col_break1", + "advance_amount", + "allocated_amount", + "exchange_gain_loss", + "ref_exchange_rate" + ], "fields": [ { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "reference_type", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Reference Type", - "length": 0, - "no_copy": 1, - "oldfieldname": "journal_voucher", - "oldfieldtype": "Link", - "options": "DocType", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "180px", - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0, + "fieldname": "reference_type", + "fieldtype": "Link", + "label": "Reference Type", + "no_copy": 1, + "oldfieldname": "journal_voucher", + "oldfieldtype": "Link", + "options": "DocType", + "print_width": "180px", + "read_only": 1, "width": "180px" - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 3, - "fieldname": "reference_name", - "fieldtype": "Dynamic Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "label": "Reference Name", - "length": 0, - "no_copy": 1, - "options": "reference_type", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "columns": 3, + "fieldname": "reference_name", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "label": "Reference Name", + "no_copy": 1, + "options": "reference_type", + "read_only": 1 + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 3, - "fieldname": "remarks", - "fieldtype": "Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "label": "Remarks", - "length": 0, - "no_copy": 1, - "oldfieldname": "remarks", - "oldfieldtype": "Small Text", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "150px", - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0, + "columns": 3, + "fieldname": "remarks", + "fieldtype": "Text", + "in_list_view": 1, + "label": "Remarks", + "no_copy": 1, + "oldfieldname": "remarks", + "oldfieldtype": "Small Text", + "print_width": "150px", + "read_only": 1, "width": "150px" - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "reference_row", - "fieldtype": "Data", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Reference Row", - "length": 0, - "no_copy": 1, - "oldfieldname": "jv_detail_no", - "oldfieldtype": "Date", - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "print_width": "80px", - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0, + "fieldname": "reference_row", + "fieldtype": "Data", + "hidden": 1, + "label": "Reference Row", + "no_copy": 1, + "oldfieldname": "jv_detail_no", + "oldfieldtype": "Date", + "print_hide": 1, + "print_width": "80px", + "read_only": 1, "width": "80px" - }, + }, { - "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_list_view": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "col_break1", + "fieldtype": "Column Break" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 2, - "fieldname": "advance_amount", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "label": "Advance Amount", - "length": 0, - "no_copy": 1, - "oldfieldname": "advance_amount", - "oldfieldtype": "Currency", - "options": "party_account_currency", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "100px", - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0, + "columns": 2, + "fieldname": "advance_amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Advance Amount", + "no_copy": 1, + "oldfieldname": "advance_amount", + "oldfieldtype": "Currency", + "options": "party_account_currency", + "print_width": "100px", + "read_only": 1, "width": "100px" - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 2, - "fieldname": "allocated_amount", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "label": "Allocated Amount", - "length": 0, - "no_copy": 1, - "oldfieldname": "allocated_amount", - "oldfieldtype": "Currency", - "options": "party_account_currency", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "100px", - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0, + "columns": 2, + "fieldname": "allocated_amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Allocated Amount", + "no_copy": 1, + "oldfieldname": "allocated_amount", + "oldfieldtype": "Currency", + "options": "party_account_currency", + "print_width": "100px", "width": "100px" + }, + { + "fieldname": "exchange_gain_loss", + "fieldtype": "Currency", + "label": "Exchange Gain/Loss", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "ref_exchange_rate", + "fieldtype": "Float", + "label": "Reference Exchange Rate", + "non_negative": 1, + "read_only": 1 } - ], - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 1, - "image_view": 0, - "in_create": 0, - - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "menu_index": 0, - "modified": "2016-08-26 02:30:54.407138", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Purchase Invoice Advance", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "sort_order": "DESC", - "track_seen": 0 + ], + "idx": 1, + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-04-20 16:26:53.820530", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Purchase Invoice Advance", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC" } \ No newline at end of file diff --git a/erpnext/accounts/doctype/purchase_invoice_advance/purchase_invoice_advance.py b/erpnext/accounts/doctype/purchase_invoice_advance/purchase_invoice_advance.py index bfaa849200e..d157837a7a5 100644 --- a/erpnext/accounts/doctype/purchase_invoice_advance/purchase_invoice_advance.py +++ b/erpnext/accounts/doctype/purchase_invoice_advance/purchase_invoice_advance.py @@ -7,4 +7,4 @@ import frappe from frappe.model.document import Document class PurchaseInvoiceAdvance(Document): - pass \ No newline at end of file + pass diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json index 8a55ff87e39..51e5c1a6a9a 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -73,6 +73,7 @@ "manufacturer_part_no", "accounting", "expense_account", + "discount_account", "col_break5", "is_fixed_asset", "asset_location", @@ -501,6 +502,7 @@ }, { "collapsible": 1, + "collapsible_depends_on": "enable_deferred_expense", "fieldname": "deferred_expense_section", "fieldtype": "Section Break", "label": "Deferred Expense" @@ -849,12 +851,18 @@ "options": "Company:company:default_currency", "print_hide": 1, "read_only": 1 + }, + { + "fieldname": "discount_account", + "fieldtype": "Link", + "label": "Discount Account", + "options": "Account" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2021-06-16 19:57:03.101571", + "modified": "2021-08-12 20:14:45.506639", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice Item", diff --git a/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json b/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json index 1fa68e0a8a8..d86abade924 100644 --- a/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json +++ b/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json @@ -22,7 +22,7 @@ "cost_center", "dimension_col_break", "section_break_9", - "currency", + "account_currency", "tax_amount", "tax_amount_after_discount_amount", "total", @@ -208,14 +208,6 @@ "fieldname": "dimension_col_break", "fieldtype": "Column Break" }, - { - "fetch_from": "account_head.account_currency", - "fieldname": "currency", - "fieldtype": "Link", - "label": "Account Currency", - "options": "Currency", - "read_only": 1 - }, { "default": "0", "depends_on": "eval:['Purchase Taxes and Charges Template', 'Payment Entry'].includes(parent.doctype)", @@ -223,12 +215,20 @@ "fieldname": "included_in_paid_amount", "fieldtype": "Check", "label": "Considered In Paid Amount" + }, + { + "fetch_from": "account_head.account_currency", + "fieldname": "account_currency", + "fieldtype": "Link", + "label": "Account Currency", + "options": "Currency", + "read_only": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2021-06-14 01:43:50.750455", + "modified": "2021-08-05 20:04:36.618240", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Taxes and Charges", diff --git a/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.py b/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.py index a7489da316f..5854ddee940 100644 --- a/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.py +++ b/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.py @@ -7,4 +7,4 @@ import frappe from frappe.model.document import Document class PurchaseTaxesandCharges(Document): - pass \ No newline at end of file + pass diff --git a/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template_dashboard.py b/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template_dashboard.py index 11c220bf2db..db9793d77a6 100644 --- a/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template_dashboard.py +++ b/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template_dashboard.py @@ -19,4 +19,4 @@ def get_data(): 'items': ['Supplier Quotation', 'Tax Rule'] } ] - } \ No newline at end of file + } diff --git a/erpnext/accounts/doctype/purchase_taxes_and_charges_template/test_purchase_taxes_and_charges_template.js b/erpnext/accounts/doctype/purchase_taxes_and_charges_template/test_purchase_taxes_and_charges_template.js index c73f03b57bb..10b05d0594f 100644 --- a/erpnext/accounts/doctype/purchase_taxes_and_charges_template/test_purchase_taxes_and_charges_template.js +++ b/erpnext/accounts/doctype/purchase_taxes_and_charges_template/test_purchase_taxes_and_charges_template.js @@ -26,4 +26,3 @@ QUnit.test("test sales taxes and charges template", function(assert) { () => done() ]); }); - diff --git a/erpnext/accounts/doctype/sales_invoice/regional/india_list.js b/erpnext/accounts/doctype/sales_invoice/regional/india_list.js index ada665a0ca9..f01325d80bd 100644 --- a/erpnext/accounts/doctype/sales_invoice/regional/india_list.js +++ b/erpnext/accounts/doctype/sales_invoice/regional/india_list.js @@ -67,7 +67,7 @@ frappe.listview_settings['Sales Invoice'].onload = function (list_view) { "default": "1-Duplicate", "options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"] }, - { + { "label": "Remark", "fieldname": "remark", "fieldtype": "Data", @@ -82,7 +82,7 @@ frappe.listview_settings['Sales Invoice'].onload = function (list_view) { const data = d.get_values(); frappe.call({ method: 'erpnext.regional.india.e_invoice.utils.cancel_irns', - args: { + args: { doctype: list_view.doctype, docnames, reason: data.reason.split('-')[0], @@ -122,7 +122,7 @@ frappe.listview_settings['Sales Invoice'].onload = function (list_view) { frappe.realtime.on("bulk_einvoice_generation_complete", (data) => { const { failures, user, invoices } = data; - + if (invoices.length != failures.length) { frappe.msgprint({ message: __('{0} e-invoices generated successfully', [invoices.length]), @@ -171,4 +171,4 @@ frappe.listview_settings['Sales Invoice'].onload = function (list_view) { }); } }); -}; \ No newline at end of file +}; diff --git a/erpnext/accounts/doctype/sales_invoice/regional/italy.js b/erpnext/accounts/doctype/sales_invoice/regional/italy.js index 1c47d3ab9fd..21eb8ce6619 100644 --- a/erpnext/accounts/doctype/sales_invoice/regional/italy.js +++ b/erpnext/accounts/doctype/sales_invoice/regional/italy.js @@ -1,3 +1,3 @@ {% include "erpnext/regional/italy/sales_invoice.js" %} -erpnext.setup_e_invoice_button('Sales Invoice') \ No newline at end of file +erpnext.setup_e_invoice_button('Sales Invoice') diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index f813425e6b5..7427f381aba 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -323,17 +323,13 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte this.frm.refresh_fields(); }, - write_off_outstanding_amount_automatically: function() { - if(cint(this.frm.doc.write_off_outstanding_amount_automatically)) { + write_off_outstanding_amount_automatically() { + if (cint(this.frm.doc.write_off_outstanding_amount_automatically)) { frappe.model.round_floats_in(this.frm.doc, ["grand_total", "paid_amount"]); // this will make outstanding amount 0 this.frm.set_value("write_off_amount", flt(this.frm.doc.grand_total - this.frm.doc.paid_amount - this.frm.doc.total_advance, precision("write_off_amount")) ); - this.frm.toggle_enable("write_off_amount", false); - - } else { - this.frm.toggle_enable("write_off_amount", true); } this.calculate_outstanding_amount(false); @@ -347,7 +343,7 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte items_add: function(doc, cdt, cdn) { var row = frappe.get_doc(cdt, cdn); - this.frm.script_manager.copy_from_first_row("items", row, ["income_account", "cost_center"]); + this.frm.script_manager.copy_from_first_row("items", row, ["income_account", "discount_account", "cost_center"]); }, set_dynamic_labels: function() { @@ -447,6 +443,15 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte this.frm.refresh_field("outstanding_amount"); this.frm.refresh_field("paid_amount"); this.frm.refresh_field("base_paid_amount"); + }, + + currency() { + this._super(); + $.each(cur_frm.doc.timesheets, function(i, d) { + let row = frappe.get_doc(d.doctype, d.name) + set_timesheet_detail_rate(row.doctype, row.name, cur_frm.doc.currency, row.timesheet_detail) + }); + calculate_total_billing_amount(cur_frm) } }); @@ -510,7 +515,6 @@ cur_frm.set_query("income_account", "items", function(doc) { } }); - // Cost Center in Details Table // ----------------------------- cur_frm.fields_dict["items"].grid.get_field("cost_center").get_query = function(doc) { @@ -592,6 +596,16 @@ frappe.ui.form.on('Sales Invoice', { }; }); + frm.set_query("additional_discount_account", function() { + return { + filters: { + company: frm.doc.company, + is_group: 0, + report_type: "Profit and Loss", + } + }; + }); + frm.custom_make_buttons = { 'Delivery Note': 'Delivery', 'Sales Invoice': 'Return / Credit Note', @@ -618,6 +632,17 @@ frappe.ui.form.on('Sales Invoice', { } } + // discount account + frm.fields_dict['items'].grid.get_field('discount_account').get_query = function(doc) { + return { + filters: { + 'report_type': 'Profit and Loss', + 'company': doc.company, + "is_group": 0 + } + } + } + frm.fields_dict['items'].grid.get_field('deferred_revenue_account').get_query = function(doc) { return { filters: { @@ -694,19 +719,6 @@ frappe.ui.form.on('Sales Invoice', { } }, - project: function(frm){ - if (!frm.doc.is_return) { - frm.call({ - method: "add_timesheet_data", - doc: frm.doc, - callback: function(r, rt) { - refresh_field(['timesheets']) - } - }) - frm.refresh(); - } - }, - onload: function(frm) { frm.redemption_conversion_factor = null; }, @@ -758,8 +770,6 @@ frappe.ui.form.on('Sales Invoice', { if (frappe.boot.sysdefaults.country == 'India') unhide_field(['c_form_applicable', 'c_form_no']); else hide_field(['c_form_applicable', 'c_form_no']); - frm.toggle_enable("write_off_amount", !!!cint(doc.write_off_outstanding_amount_automatically)); - frm.refresh_fields(); }, @@ -819,24 +829,92 @@ frappe.ui.form.on('Sales Invoice', { } }, - add_timesheet_row: function(frm, row, exchange_rate) { - frm.add_child('timesheets', { - 'activity_type': row.activity_type, - 'description': row.description, - 'time_sheet': row.parent, - 'billing_hours': row.billing_hours, - 'billing_amount': flt(row.billing_amount) * flt(exchange_rate), - 'timesheet_detail': row.name + project: function(frm) { + if (frm.doc.project) { + frm.events.add_timesheet_data(frm, { + project: frm.doc.project + }); + } + }, + + async add_timesheet_data(frm, kwargs) { + if (kwargs === "Sales Invoice") { + // called via frm.trigger() + kwargs = Object(); + } + + if (!kwargs.hasOwnProperty("project") && frm.doc.project) { + kwargs.project = frm.doc.project; + } + + const timesheets = await frm.events.get_timesheet_data(frm, kwargs); + return frm.events.set_timesheet_data(frm, timesheets); + }, + + async get_timesheet_data(frm, kwargs) { + return frappe.call({ + method: "erpnext.projects.doctype.timesheet.timesheet.get_projectwise_timesheet_data", + args: kwargs + }).then(r => { + if (!r.exc && r.message.length > 0) { + return r.message + } else { + return [] + } }); - frm.refresh_field('timesheets'); - calculate_total_billing_amount(frm); + }, + + set_timesheet_data: function(frm, timesheets) { + frm.clear_table("timesheets") + timesheets.forEach(timesheet => { + if (frm.doc.currency != timesheet.currency) { + frappe.call({ + method: "erpnext.setup.utils.get_exchange_rate", + args: { + from_currency: timesheet.currency, + to_currency: frm.doc.currency + }, + callback: function(r) { + if (r.message) { + exchange_rate = r.message; + frm.events.append_time_log(frm, timesheet, exchange_rate); + } + } + }); + } else { + frm.events.append_time_log(frm, timesheet, 1.0); + } + }); + }, + + append_time_log: function(frm, time_log, exchange_rate) { + const row = frm.add_child("timesheets"); + row.activity_type = time_log.activity_type; + row.description = time_log.description; + row.time_sheet = time_log.time_sheet; + row.from_time = time_log.from_time; + row.to_time = time_log.to_time; + row.billing_hours = time_log.billing_hours; + row.billing_amount = flt(time_log.billing_amount) * flt(exchange_rate); + row.timesheet_detail = time_log.name; + row.project_name = time_log.project_name; + + frm.refresh_field("timesheets"); + frm.trigger("calculate_timesheet_totals"); + }, + + calculate_timesheet_totals: function(frm) { + frm.set_value("total_billing_amount", + frm.doc.timesheets.reduce((a, b) => a + (b["billing_amount"] || 0.0), 0.0)); + frm.set_value("total_billing_hours", + frm.doc.timesheets.reduce((a, b) => a + (b["billing_hours"] || 0.0), 0.0)); }, refresh: function(frm) { if (frm.doc.docstatus===0 && !frm.doc.is_return) { - frm.add_custom_button(__('Fetch Timesheet'), function() { + frm.add_custom_button(__("Fetch Timesheet"), function() { let d = new frappe.ui.Dialog({ - title: __('Fetch Timesheet'), + title: __("Fetch Timesheet"), fields: [ { "label" : __("From"), @@ -845,8 +923,8 @@ frappe.ui.form.on('Sales Invoice', { "reqd": 1, }, { - fieldtype: 'Column Break', - fieldname: 'col_break_1', + fieldtype: "Column Break", + fieldname: "col_break_1", }, { "label" : __("To"), @@ -863,48 +941,18 @@ frappe.ui.form.on('Sales Invoice', { }, ], primary_action: function() { - let data = d.get_values(); - frappe.call({ - method: "erpnext.projects.doctype.timesheet.timesheet.get_projectwise_timesheet_data", - args: { - from_time: data.from_time, - to_time: data.to_time, - project: data.project - }, - callback: function(r) { - if (!r.exc && r.message.length > 0) { - frm.clear_table('timesheets') - r.message.forEach((d) => { - let exchange_rate = 1.0; - if (frm.doc.currency != d.currency) { - frappe.call({ - method: 'erpnext.setup.utils.get_exchange_rate', - args: { - from_currency: d.currency, - to_currency: frm.doc.currency - }, - callback: function(r) { - if (r.message) { - exchange_rate = r.message; - frm.events.add_timesheet_row(frm, d, exchange_rate); - } - } - }); - } else { - frm.events.add_timesheet_row(frm, d, exchange_rate); - } - }); - } else { - frappe.msgprint(__('No Timesheets found with the selected filters.')) - } - d.hide(); - } + const data = d.get_values(); + frm.events.add_timesheet_data(frm, { + from_time: data.from_time, + to_time: data.to_time, + project: data.project }); + d.hide(); }, - primary_action_label: __('Get Timesheets') + primary_action_label: __("Get Timesheets") }); d.show(); - }) + }); } if (frm.doc.is_debit_note) { @@ -937,49 +985,36 @@ frappe.ui.form.on('Sales Invoice', { frm: frm }); }, + create_dunning: function(frm) { frappe.model.open_mapped_doc({ method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.create_dunning", frm: frm }); } -}) +}); -frappe.ui.form.on('Sales Invoice Timesheet', { - time_sheet: function(frm, cdt, cdn){ - var d = locals[cdt][cdn]; - if(d.time_sheet) { - frappe.call({ - method: "erpnext.projects.doctype.timesheet.timesheet.get_timesheet_data", - args: { - 'name': d.time_sheet, - 'project': frm.doc.project || null - }, - callback: function(r, rt) { - if(r.message){ - let data = r.message; - frappe.model.set_value(cdt, cdn, "billing_hours", data.billing_hours); - frappe.model.set_value(cdt, cdn, "billing_amount", data.billing_amount); - frappe.model.set_value(cdt, cdn, "timesheet_detail", data.timesheet_detail); - calculate_total_billing_amount(frm) - } - } - }) + +frappe.ui.form.on("Sales Invoice Timesheet", { + timesheets_remove(frm, cdt, cdn) { + frm.trigger("calculate_timesheet_totals"); + } +}); + + +var set_timesheet_detail_rate = function(cdt, cdn, currency, timelog) { + frappe.call({ + method: "erpnext.projects.doctype.timesheet.timesheet.get_timesheet_detail_rate", + args: { + timelog: timelog, + currency: currency + }, + callback: function(r) { + if (!r.exc && r.message) { + frappe.model.set_value(cdt, cdn, 'billing_amount', r.message); + } } - } -}) - -var calculate_total_billing_amount = function(frm) { - var doc = frm.doc; - - doc.total_billing_amount = 0.0 - if(doc.timesheets) { - $.each(doc.timesheets, function(index, data){ - doc.total_billing_amount += data.billing_amount - }) - } - - refresh_field('total_billing_amount') + }); } var select_loyalty_program = function(frm, loyalty_programs) { diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index e7dd6b8a606..2f8de068840 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -48,6 +48,8 @@ "shipping_address", "company_address", "company_address_display", + "dispatch_address_name", + "dispatch_address", "currency_and_price_list", "currency", "conversion_rate", @@ -72,6 +74,7 @@ "time_sheet_list", "timesheets", "total_billing_amount", + "total_billing_hours", "section_break_30", "total_qty", "base_total", @@ -104,6 +107,7 @@ "section_break_49", "apply_discount_on", "base_discount_amount", + "additional_discount_account", "column_break_51", "additional_discount_percentage", "discount_amount", @@ -125,6 +129,7 @@ "get_advances", "advances", "payment_schedule_section", + "ignore_default_payment_terms_template", "payment_terms_template", "payment_schedule", "payments_section", @@ -243,7 +248,7 @@ "depends_on": "customer", "fetch_from": "customer.customer_name", "fieldname": "customer_name", - "fieldtype": "Data", + "fieldtype": "Small Text", "hide_days": 1, "hide_seconds": 1, "in_global_search": 1, @@ -690,7 +695,9 @@ "fieldtype": "Data", "hide_days": 1, "hide_seconds": 1, - "label": "Scan Barcode" + "label": "Scan Barcode", + "length": 1, + "options": "Barcode" }, { "allow_bulk_edit": 1, @@ -1054,6 +1061,7 @@ "hide_days": 1, "hide_seconds": 1, "label": "Apply Additional Discount On", + "length": 15, "options": "\nGrand Total\nNet Total", "print_hide": 1 }, @@ -1140,7 +1148,7 @@ { "description": "In Words will be visible once you save the Sales Invoice.", "fieldname": "base_in_words", - "fieldtype": "Data", + "fieldtype": "Small Text", "hide_days": 1, "hide_seconds": 1, "label": "In Words (Company Currency)", @@ -1200,7 +1208,7 @@ }, { "fieldname": "in_words", - "fieldtype": "Data", + "fieldtype": "Small Text", "hide_days": 1, "hide_seconds": 1, "label": "In Words", @@ -1439,7 +1447,8 @@ "label": "Write Off Amount", "no_copy": 1, "options": "currency", - "print_hide": 1 + "print_hide": 1, + "read_only_depends_on": "eval:doc.write_off_outstanding_amount_automatically" }, { "fieldname": "base_write_off_amount", @@ -1552,6 +1561,7 @@ "hide_days": 1, "hide_seconds": 1, "label": "Print Language", + "length": 6, "print_hide": 1, "read_only": 1 }, @@ -1639,6 +1649,7 @@ "hide_seconds": 1, "in_standard_filter": 1, "label": "Status", + "length": 30, "no_copy": 1, "options": "\nDraft\nReturn\nCredit Note Issued\nSubmitted\nPaid\nUnpaid\nUnpaid and Discounted\nOverdue and Discounted\nOverdue\nCancelled\nInternal Transfer", "print_hide": 1, @@ -1698,6 +1709,7 @@ "hide_days": 1, "hide_seconds": 1, "label": "Is Opening Entry", + "length": 4, "oldfieldname": "is_opening", "oldfieldtype": "Select", "options": "No\nYes", @@ -1709,6 +1721,7 @@ "hide_days": 1, "hide_seconds": 1, "label": "C-Form Applicable", + "length": 4, "no_copy": 1, "options": "No\nYes", "print_hide": 1 @@ -1930,6 +1943,7 @@ "description": "Unrealized Profit / Loss account for intra-company transfers", "fieldname": "unrealized_profit_loss_account", "fieldtype": "Link", + "ignore_user_permissions": 1, "label": "Unrealized Profit / Loss Account", "options": "Account" }, @@ -1951,6 +1965,7 @@ "depends_on": "eval: doc.is_internal_customer && doc.update_stock", "fieldname": "set_target_warehouse", "fieldtype": "Link", + "ignore_user_permissions": 1, "label": "Set Target Warehouse", "options": "Warehouse" }, @@ -1966,6 +1981,44 @@ "fieldname": "disable_rounded_total", "fieldtype": "Check", "label": "Disable Rounded Total" + }, + { + "fieldname": "additional_discount_account", + "fieldtype": "Link", + "label": "Additional Discount Account", + "options": "Account" + }, + { + "default": "0", + "fieldname": "ignore_default_payment_terms_template", + "fieldtype": "Check", + "hidden": 1, + "label": "Ignore Default Payment Terms Template", + "read_only": 1, + "show_days": 1, + "show_seconds": 1 + }, + { + "allow_on_submit": 1, + "fieldname": "dispatch_address_name", + "fieldtype": "Link", + "label": "Dispatch Address Name", + "options": "Address", + "print_hide": 1 + }, + { + "allow_on_submit": 1, + "fieldname": "dispatch_address", + "fieldtype": "Small Text", + "label": "Dispatch Address", + "read_only": 1 + }, + { + "fieldname": "total_billing_hours", + "fieldtype": "Float", + "label": "Total Billing Hours", + "print_hide": 1, + "read_only": 1 } ], "icon": "fa fa-file-text", @@ -1978,7 +2031,7 @@ "link_fieldname": "consolidated_invoice" } ], - "modified": "2021-05-20 22:48:33.988881", + "modified": "2021-08-25 15:46:05.279588", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", @@ -2032,4 +2085,4 @@ "title_field": "title", "track_changes": 1, "track_seen": 1 -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 55a5b99907b..0a7042fa47c 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe, erpnext import frappe.defaults -from frappe.utils import cint, flt, getdate, add_days, cstr, nowdate, get_link_to_form, formatdate +from frappe.utils import cint, flt, getdate, add_days, add_months, cstr, nowdate, get_link_to_form, formatdate from frappe import _, msgprint, throw from erpnext.accounts.party import get_party_account, get_due_date, get_party_details from frappe.model.mapper import get_mapped_doc @@ -13,7 +13,7 @@ from erpnext.accounts.utils import get_account_currency from erpnext.stock.doctype.delivery_note.delivery_note import update_billed_amount_based_on_so from erpnext.projects.doctype.timesheet.timesheet import get_projectwise_timesheet_data from erpnext.assets.doctype.asset.depreciation \ - import get_disposal_account_and_cost_center, get_gl_entries_on_asset_disposal + import get_disposal_account_and_cost_center, get_gl_entries_on_asset_disposal, get_gl_entries_on_asset_regain, post_depreciation_entries from erpnext.stock.doctype.batch.batch import set_batch_nos from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos, get_delivery_note_serial_no from erpnext.setup.doctype.company.company import update_company_current_month_sales @@ -149,7 +149,7 @@ class SalesInvoice(SellingController): if self.update_stock: frappe.throw(_("'Update Stock' cannot be checked for fixed asset sale")) - elif asset.status in ("Scrapped", "Cancelled", "Sold"): + elif asset.status in ("Scrapped", "Cancelled") or (asset.status == "Sold" and not self.is_return): frappe.throw(_("Row #{0}: Asset {1} cannot be submitted, it is already {2}").format(d.idx, d.asset, asset.status)) def validate_item_cost_centers(self): @@ -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: @@ -290,6 +292,8 @@ class SalesInvoice(SellingController): self.update_time_sheet(None) def on_cancel(self): + check_if_return_invoice_linked_with_payment_entry(self) + super(SalesInvoice, self).on_cancel() self.check_sales_order_on_hold_or_close("sales_order") @@ -476,11 +480,14 @@ class SalesInvoice(SellingController): if cint(self.is_pos) != 1: return + if not self.account_for_change_amount: + self.account_for_change_amount = frappe.get_cached_value('Company', self.company, 'default_cash_account') + from erpnext.stock.get_item_details import get_pos_profile_item_details, get_pos_profile if not self.pos_profile: pos_profile = get_pos_profile(self.company) or {} if not pos_profile: - frappe.throw(_("No POS Profile found. Please create a New POS Profile first")) + return self.pos_profile = pos_profile.get('name') pos = {} @@ -490,9 +497,6 @@ class SalesInvoice(SellingController): if not self.get('payments') and not for_validate: update_multi_mode_option(self, pos) - if not self.account_for_change_amount: - self.account_for_change_amount = frappe.get_cached_value('Company', self.company, 'default_cash_account') - if pos: if not for_validate: self.tax_category = pos.get("tax_category") @@ -753,7 +757,7 @@ class SalesInvoice(SellingController): if self.project: for data in get_projectwise_timesheet_data(self.project): self.append('timesheets', { - 'time_sheet': data.parent, + 'time_sheet': data.time_sheet, 'billing_hours': data.billing_hours, 'billing_amount': data.billing_amount, 'timesheet_detail': data.name, @@ -764,12 +768,11 @@ class SalesInvoice(SellingController): self.calculate_billing_amount_for_timesheet() def calculate_billing_amount_for_timesheet(self): - total_billing_amount = 0.0 - for data in self.timesheets: - if data.billing_amount: - total_billing_amount += data.billing_amount + def timesheet_sum(field): + return sum((ts.get(field) or 0.0) for ts in self.timesheets) - self.total_billing_amount = total_billing_amount + self.total_billing_amount = timesheet_sum("billing_amount") + self.total_billing_hours = timesheet_sum("billing_hours") def get_warehouse(self): user_pos_profile = frappe.db.sql("""select name, warehouse from `tabPOS Profile` @@ -840,11 +843,13 @@ class SalesInvoice(SellingController): self.make_customer_gl_entry(gl_entries) self.make_tax_gl_entries(gl_entries) + self.make_exchange_gain_loss_gl_entries(gl_entries) self.make_internal_transfer_gl_entries(gl_entries) self.allocate_advance_taxes(gl_entries) self.make_item_gl_entries(gl_entries) + self.make_discount_gl_entries(gl_entries) # merge gl entries before adding pos entries gl_entries = merge_similar_entries(gl_entries) @@ -885,17 +890,19 @@ class SalesInvoice(SellingController): def make_tax_gl_entries(self, gl_entries): for tax in self.get("taxes"): + amount, base_amount = self.get_tax_amounts(tax, self.enable_discount_accounting) + if flt(tax.base_tax_amount_after_discount_amount): account_currency = get_account_currency(tax.account_head) gl_entries.append( self.get_gl_dict({ "account": tax.account_head, "against": self.customer, - "credit": flt(tax.base_tax_amount_after_discount_amount, + "credit": flt(base_amount, tax.precision("tax_amount_after_discount_amount")), - "credit_in_account_currency": (flt(tax.base_tax_amount_after_discount_amount, + "credit_in_account_currency": (flt(base_amount, tax.precision("base_tax_amount_after_discount_amount")) if account_currency==self.company_currency else - flt(tax.tax_amount_after_discount_amount, tax.precision("tax_amount_after_discount_amount"))), + flt(amount, tax.precision("tax_amount_after_discount_amount"))), "cost_center": tax.cost_center }, account_currency, item=tax) ) @@ -917,37 +924,47 @@ class SalesInvoice(SellingController): for item in self.get("items"): if flt(item.base_net_amount, item.precision("base_net_amount")): if item.is_fixed_asset: - asset = frappe.get_doc("Asset", item.asset) + asset = self.get_asset(item) - if (len(asset.finance_books) > 1 and not item.finance_book - and asset.finance_books[0].finance_book): - frappe.throw(_("Select finance book for the item {0} at row {1}") - .format(item.item_code, item.idx)) + if self.is_return: + fixed_asset_gl_entries = get_gl_entries_on_asset_regain(asset, + item.base_net_amount, item.finance_book) + asset.db_set("disposal_date", None) - fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(asset, - item.base_net_amount, item.finance_book) + if asset.calculate_depreciation: + self.reset_depreciation_schedule(asset) + + else: + fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(asset, + item.base_net_amount, item.finance_book) + asset.db_set("disposal_date", self.posting_date) + + if asset.calculate_depreciation: + self.depreciate_asset(asset) for gle in fixed_asset_gl_entries: gle["against"] = self.customer gl_entries.append(self.get_gl_dict(gle, item=item)) - asset.db_set("disposal_date", self.posting_date) - asset.set_status("Sold" if self.docstatus==1 else None) + self.set_asset_status(asset) + else: # Do not book income for transfer within same company if not self.is_internal_transfer(): income_account = (item.income_account if (not item.enable_deferred_revenue or self.is_return) else item.deferred_revenue_account) + amount, base_amount = self.get_amount_and_base_amount(item, self.enable_discount_accounting) + account_currency = get_account_currency(income_account) gl_entries.append( self.get_gl_dict({ "account": income_account, "against": self.customer, - "credit": flt(item.base_net_amount, item.precision("base_net_amount")), - "credit_in_account_currency": (flt(item.base_net_amount, item.precision("base_net_amount")) + "credit": flt(base_amount, item.precision("base_net_amount")), + "credit_in_account_currency": (flt(base_amount, item.precision("base_net_amount")) if account_currency==self.company_currency - else flt(item.net_amount, item.precision("net_amount"))), + else flt(amount, item.precision("net_amount"))), "cost_center": item.cost_center, "project": item.project or self.project }, account_currency, item=item) @@ -958,6 +975,102 @@ class SalesInvoice(SellingController): erpnext.is_perpetual_inventory_enabled(self.company): gl_entries += super(SalesInvoice, self).get_gl_entries() + def get_asset(self, item): + if item.get('asset'): + asset = frappe.get_doc("Asset", item.asset) + else: + frappe.throw(_( + "Row #{0}: You must select an Asset for Item {1}.").format(item.idx, item.item_name), + title=_("Missing Asset") + ) + + self.check_finance_books(item, asset) + return asset + + def check_finance_books(self, item, asset): + if (len(asset.finance_books) > 1 and not item.finance_book + and asset.finance_books[0].finance_book): + frappe.throw(_("Select finance book for the item {0} at row {1}") + .format(item.item_code, item.idx)) + + def depreciate_asset(self, asset): + asset.flags.ignore_validate_update_after_submit = True + asset.prepare_depreciation_data(self.posting_date) + asset.save() + + post_depreciation_entries(self.posting_date) + + def reset_depreciation_schedule(self, asset): + asset.flags.ignore_validate_update_after_submit = True + + # recreate original depreciation schedule of the asset + asset.prepare_depreciation_data() + + self.modify_depreciation_schedule_for_asset_repairs(asset) + asset.save() + + self.delete_depreciation_entry_made_after_sale(asset) + + def modify_depreciation_schedule_for_asset_repairs(self, asset): + asset_repairs = frappe.get_all( + 'Asset Repair', + filters = {'asset': asset.name}, + fields = ['name', 'increase_in_asset_life'] + ) + + for repair in asset_repairs: + if repair.increase_in_asset_life: + asset_repair = frappe.get_doc('Asset Repair', repair.name) + asset_repair.modify_depreciation_schedule() + asset.prepare_depreciation_data() + + def delete_depreciation_entry_made_after_sale(self, asset): + from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry + + posting_date_of_original_invoice = self.get_posting_date_of_sales_invoice() + + row = -1 + finance_book = asset.get('schedules')[0].get('finance_book') + for schedule in asset.get('schedules'): + if schedule.finance_book != finance_book: + row = 0 + finance_book = schedule.finance_book + else: + row += 1 + + if schedule.schedule_date == posting_date_of_original_invoice: + if not self.sale_was_made_on_original_schedule_date(asset, schedule, row, posting_date_of_original_invoice): + reverse_journal_entry = make_reverse_journal_entry(schedule.journal_entry) + reverse_journal_entry.posting_date = nowdate() + reverse_journal_entry.submit() + + def get_posting_date_of_sales_invoice(self): + return frappe.db.get_value('Sales Invoice', self.return_against, 'posting_date') + + # if the invoice had been posted on the date the depreciation was initially supposed to happen, the depreciation shouldn't be undone + def sale_was_made_on_original_schedule_date(self, asset, schedule, row, posting_date_of_original_invoice): + for finance_book in asset.get('finance_books'): + if schedule.finance_book == finance_book.finance_book: + orginal_schedule_date = add_months(finance_book.depreciation_start_date, + row * cint(finance_book.frequency_of_depreciation)) + + if orginal_schedule_date == posting_date_of_original_invoice: + return True + return False + + @property + def enable_discount_accounting(self): + if not hasattr(self, "_enable_discount_accounting"): + self._enable_discount_accounting = cint(frappe.db.get_single_value('Accounts Settings', 'enable_discount_accounting')) + + return self._enable_discount_accounting + + def set_asset_status(self, asset): + if self.is_return: + asset.set_status() + else: + asset.set_status("Sold" if self.docstatus==1 else None) + def make_loyalty_point_redemption_gle(self, gl_entries): if cint(self.redeem_loyalty_points): gl_entries.append( @@ -1347,7 +1460,7 @@ class SalesInvoice(SellingController): discounting_status = None if self.is_discounted: - discountng_status = get_discounting_status(self.name) + discounting_status = get_discounting_status(self.name) if not status: if self.docstatus == 2: @@ -1355,11 +1468,11 @@ class SalesInvoice(SellingController): elif self.docstatus == 1: if self.is_internal_transfer(): self.status = 'Internal Transfer' - elif outstanding_amount > 0 and due_date < nowdate and self.is_discounted and discountng_status=='Disbursed': + elif outstanding_amount > 0 and due_date < nowdate and self.is_discounted and discounting_status=='Disbursed': self.status = "Overdue and Discounted" elif outstanding_amount > 0 and due_date < nowdate: self.status = "Overdue" - elif outstanding_amount > 0 and due_date >= nowdate and self.is_discounted and discountng_status=='Disbursed': + elif outstanding_amount > 0 and due_date >= nowdate and self.is_discounted and discounting_status=='Disbursed': self.status = "Unpaid and Discounted" elif outstanding_amount > 0 and due_date >= nowdate: self.status = "Unpaid" @@ -1923,3 +2036,41 @@ def create_dunning(source_name, target_doc=None): } }, target_doc, set_missing_values) return doclist + +def check_if_return_invoice_linked_with_payment_entry(self): + # If a Return invoice is linked with payment entry along with other invoices, + # the cancellation of the Return causes allocated amount to be greater than paid + + if not frappe.db.get_single_value('Accounts Settings', 'unlink_payment_on_cancellation_of_invoice'): + return + + payment_entries = [] + if self.is_return and self.return_against: + invoice = self.return_against + else: + invoice = self.name + + payment_entries = frappe.db.sql_list(""" + SELECT + t1.name + FROM + `tabPayment Entry` t1, `tabPayment Entry Reference` t2 + WHERE + t1.name = t2.parent + and t1.docstatus = 1 + and t2.reference_name = %s + and t2.allocated_amount < 0 + """, invoice) + + links_to_pe = [] + if payment_entries: + for payment in payment_entries: + payment_entry = frappe.get_doc("Payment Entry", payment) + if len(payment_entry.references) > 1: + links_to_pe.append(payment_entry.name) + if links_to_pe: + payment_entries_link = [get_link_to_form('Payment Entry', name, label=name) for name in links_to_pe] + message = _("Please cancel and amend the Payment Entry") + message += " " + ", ".join(payment_entries_link) + " " + message += _("to unallocate the amount of this Return Invoice before cancelling it.") + frappe.throw(message) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py index f1069282edc..3238ead4316 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py @@ -33,4 +33,4 @@ def get_data(): 'items': ['Auto Repeat'] }, ] - } \ No newline at end of file + } diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index dbc7f8632fc..8fa36509d21 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -10,6 +10,8 @@ from frappe.model.dynamic_links import get_dynamic_link_map from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry, get_qty_after_transaction from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import unlink_payment_on_cancel_of_invoice from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile +from erpnext.assets.doctype.asset.test_asset import create_asset, create_asset_data +from erpnext.assets.doctype.asset.depreciation import post_depreciation_entries from erpnext.exceptions import InvalidAccountCurrency, InvalidCurrency from erpnext.stock.doctype.serial_no.serial_no import SerialNoWarehouseError from frappe.model.naming import make_autoname @@ -23,6 +25,7 @@ from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice from erpnext.stock.utils import get_incoming_rate +from erpnext.accounts.utils import PaymentEntryUnlinkError class TestSalesInvoice(unittest.TestCase): def make(self): @@ -133,7 +136,7 @@ class TestSalesInvoice(unittest.TestCase): pe.paid_to_account_currency = si.currency pe.source_exchange_rate = 1 pe.target_exchange_rate = 1 - pe.paid_amount = si.grand_total + pe.paid_amount = si.outstanding_amount pe.insert() pe.submit() @@ -142,6 +145,42 @@ class TestSalesInvoice(unittest.TestCase): self.assertRaises(frappe.LinkExistsError, si.cancel) unlink_payment_on_cancel_of_invoice() + def test_payment_entry_unlink_against_standalone_credit_note(self): + from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry + 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', { + 'reference_doctype': 'Sales Invoice', + 'reference_name': si2.name, + 'total_amount': si2.grand_total, + 'outstanding_amount': si2.outstanding_amount, + 'allocated_amount': si2.outstanding_amount + }) + + pe.append('references', { + 'reference_doctype': 'Sales Invoice', + 'reference_name': si3.name, + 'total_amount': si3.grand_total, + 'outstanding_amount': si3.outstanding_amount, + 'allocated_amount': si3.outstanding_amount + }) + + pe.reference_no = 'Test001' + pe.reference_date = nowdate() + pe.save() + pe.submit() + + si2.load_from_db() + si2.cancel() + + si1.load_from_db() + self.assertRaises(PaymentEntryUnlinkError, si1.cancel) + + def test_sales_invoice_calculation_export_currency(self): si = frappe.copy_doc(test_records[2]) si.currency = "USD" @@ -1069,6 +1108,57 @@ class TestSalesInvoice(unittest.TestCase): self.assertFalse(si1.outstanding_amount) self.assertEqual(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"), 1500) + def test_gle_made_when_asset_is_returned(self): + create_asset_data() + + pi = frappe.new_doc('Purchase Invoice') + pi.supplier = '_Test Supplier' + pi.append('items', { + 'item_code': 'Macbook Pro', + 'qty': 1 + }) + pi.set_missing_values() + + asset = create_asset(item_code="Macbook Pro") + + si = create_sales_invoice(item_code="Macbook Pro", asset=asset.name, qty=1, rate=90000) + return_si = create_sales_invoice(is_return=1, return_against=si.name, item_code="Macbook Pro", asset=asset.name, qty=-1, rate=90000) + + disposal_account = frappe.get_cached_value("Company", "_Test Company", "disposal_account") + + # Asset value is 100,000 but it was sold for 90,000, so there should be a loss of 10,000 + loss_for_si = frappe.get_all( + "GL Entry", + filters = { + "voucher_no": si.name, + "account": disposal_account + }, + fields = ["credit", "debit"] + )[0] + + loss_for_return_si = frappe.get_all( + "GL Entry", + filters = { + "voucher_no": return_si.name, + "account": disposal_account + }, + fields = ["credit", "debit"] + )[0] + + 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]) @@ -1746,23 +1836,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({ @@ -1805,8 +1885,43 @@ class TestSalesInvoice(unittest.TestCase): self.assertEqual(target_doc.company, "_Test Company 1") self.assertEqual(target_doc.supplier, "_Test Internal Supplier") + 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") @@ -1898,7 +2013,7 @@ class TestSalesInvoice(unittest.TestCase): data = get_ewb_data("Sales Invoice", [si.name]) - self.assertEqual(data['version'], '1.0.1118') + self.assertEqual(data['version'], '1.0.0421') self.assertEqual(data['billLists'][0]['fromGstin'], '27AAECE4835E1ZR') self.assertEqual(data['billLists'][0]['fromTrdName'], '_Test Company') self.assertEqual(data['billLists'][0]['toTrdName'], '_Test Customer') @@ -1908,6 +2023,8 @@ class TestSalesInvoice(unittest.TestCase): self.assertEqual(data['billLists'][0]['sgstValue'], 5400) self.assertEqual(data['billLists'][0]['vehicleNo'], 'KA12KA1234') self.assertEqual(data['billLists'][0]['itemList'][0]['taxableAmount'], 60000) + self.assertEqual(data['billLists'][0]['actualFromStateCode'],7) + self.assertEqual(data['billLists'][0]['fromStateCode'],27) def test_einvoice_submission_without_irn(self): # init @@ -1984,6 +2101,122 @@ class TestSalesInvoice(unittest.TestCase): sales_invoice.save() self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 10 - _TC") + def test_sales_invoice_with_discount_accounting_enabled(self): + from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import enable_discount_accounting + + enable_discount_accounting() + + discount_account = create_account(account_name="Discount Account", + parent_account="Indirect Expenses - _TC", company="_Test Company") + si = create_sales_invoice(discount_account=discount_account, discount_percentage=10, rate=90) + + expected_gle = [ + ["Debtors - _TC", 90.0, 0.0, nowdate()], + ["Discount Account - _TC", 10.0, 0.0, nowdate()], + ["Sales - _TC", 0.0, 100.0, nowdate()] + ] + + check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1)) + enable_discount_accounting(enable=0) + + def test_additional_discount_for_sales_invoice_with_discount_accounting_enabled(self): + from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import enable_discount_accounting + + enable_discount_accounting() + additional_discount_account = create_account(account_name="Discount Account", + parent_account="Indirect Expenses - _TC", company="_Test Company") + + si = create_sales_invoice(parent_cost_center='Main - _TC', do_not_save=1) + si.apply_discount_on = "Grand Total" + si.additional_discount_account = additional_discount_account + si.additional_discount_percentage = 20 + si.append("taxes", { + "charge_type": "On Net Total", + "account_head": "_Test Account VAT - _TC", + "cost_center": "Main - _TC", + "description": "Test", + "rate": 10 + }) + si.submit() + + expected_gle = [ + ["_Test Account VAT - _TC", 0.0, 10.0, nowdate()], + ["Debtors - _TC", 88, 0.0, nowdate()], + ["Discount Account - _TC", 22.0, 0.0, nowdate()], + ["Sales - _TC", 0.0, 100.0, nowdate()] + ] + + check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1)) + enable_discount_accounting(enable=0) + + def test_asset_depreciation_on_sale(self): + """ + Tests if an Asset set to depreciate yearly on June 30, that gets sold on Sept 30, creates an additional depreciation entry on Sept 30. + """ + + create_asset_data() + asset = create_asset(item_code="Macbook Pro", calculate_depreciation=1, submit=1) + post_depreciation_entries(getdate("2021-09-30")) + + create_sales_invoice(item_code="Macbook Pro", asset=asset.name, qty=1, rate=90000, posting_date=getdate("2021-09-30")) + asset.load_from_db() + + expected_values = [ + ["2020-06-30", 1311.48, 1311.48], + ["2021-06-30", 20000.0, 21311.48], + ["2021-09-30", 3966.76, 25278.24] + ] + + for i, schedule in enumerate(asset.schedules): + self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date) + self.assertEqual(expected_values[i][1], schedule.depreciation_amount) + 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-.#####' @@ -2062,6 +2295,30 @@ def make_test_address_for_ewaybill(): address.save() + if not frappe.db.exists('Address', '_Test Dispatch-Address for Eway bill-Shipping'): + address = frappe.get_doc({ + "address_line1": "_Test Dispatch Address Line 1", + "address_title": "_Test Dispatch-Address for Eway bill", + "address_type": "Shipping", + "city": "_Test City", + "state": "Test State", + "country": "India", + "doctype": "Address", + "is_primary_address": 0, + "phone": "+910000000000", + "gstin": "07AAACC1206D1ZI", + "gst_state": "Delhi", + "gst_state_number": "07", + "pincode": "1100101" + }).insert() + + address.append("links", { + "link_doctype": "Company", + "link_name": "_Test Company" + }) + + address.save() + def make_test_transporter_for_ewaybill(): if not frappe.db.exists('Supplier', '_Test Transporter'): frappe.get_doc({ @@ -2100,6 +2357,7 @@ def make_sales_invoice_for_ewaybill(): si.distance = 2000 si.company_address = "_Test Address for Eway bill-Billing" si.customer_address = "_Test Customer-Address for Eway bill-Shipping" + si.dispatch_address_name = "_Test Dispatch-Address for Eway bill-Shipping" si.vehicle_no = "KA12KA1234" si.gst_category = "Registered Regular" si.mode_of_transport = 'Road' @@ -2152,6 +2410,7 @@ def create_sales_invoice(**args): si.currency=args.currency or "INR" si.conversion_rate = args.conversion_rate or 1 si.naming_series = args.naming_series or "T-SINV-" + si.cost_center = args.parent_cost_center si.append("items", { "item_code": args.item or args.item_code or "_Test Item", @@ -2163,11 +2422,16 @@ def create_sales_invoice(**args): "uom": args.uom or "Nos", "stock_uom": args.uom or "Nos", "rate": args.rate if args.get("rate") is not None else 100, + "price_list_rate": args.price_list_rate if args.get("price_list_rate") is not None else 100, "income_account": args.income_account or "Sales - _TC", "expense_account": args.expense_account or "Cost of Goods Sold - _TC", + "asset": args.asset or None, + "discount_account": args.discount_account or None, + "discount_amount": args.discount_amount or 0, "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: @@ -2264,29 +2528,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/tests/test_sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/tests/test_sales_invoice.js index e12ac038500..61d78e1fe4b 100644 --- a/erpnext/accounts/doctype/sales_invoice/tests/test_sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/tests/test_sales_invoice.js @@ -40,4 +40,3 @@ QUnit.test("test sales Invoice", function(assert) { () => done() ]); }); - diff --git a/erpnext/accounts/doctype/sales_invoice/tests/test_sales_invoice_with_margin.js b/erpnext/accounts/doctype/sales_invoice/tests/test_sales_invoice_with_margin.js index f1cb22a4976..cf2d0fbedba 100644 --- a/erpnext/accounts/doctype/sales_invoice/tests/test_sales_invoice_with_margin.js +++ b/erpnext/accounts/doctype/sales_invoice/tests/test_sales_invoice_with_margin.js @@ -33,4 +33,3 @@ QUnit.test("test sales invoice with margin", function(assert) { () => done() ]); }); - diff --git a/erpnext/accounts/doctype/sales_invoice/tests/test_sales_invoice_with_payment.js b/erpnext/accounts/doctype/sales_invoice/tests/test_sales_invoice_with_payment.js index 651bf0aa4ce..45d9a14bffb 100644 --- a/erpnext/accounts/doctype/sales_invoice/tests/test_sales_invoice_with_payment.js +++ b/erpnext/accounts/doctype/sales_invoice/tests/test_sales_invoice_with_payment.js @@ -54,4 +54,3 @@ QUnit.test("test sales Invoice with payment", function(assert) { () => done() ]); }); - diff --git a/erpnext/accounts/doctype/sales_invoice/tests/test_sales_invoice_with_payment_request.js b/erpnext/accounts/doctype/sales_invoice/tests/test_sales_invoice_with_payment_request.js index b959cf961b8..0464e4509f6 100644 --- a/erpnext/accounts/doctype/sales_invoice/tests/test_sales_invoice_with_payment_request.js +++ b/erpnext/accounts/doctype/sales_invoice/tests/test_sales_invoice_with_payment_request.js @@ -49,4 +49,3 @@ QUnit.test("test sales Invoice with payment request", function(assert) { () => done() ]); }); - diff --git a/erpnext/accounts/doctype/sales_invoice/tests/test_sales_invoice_with_serialize_item.js b/erpnext/accounts/doctype/sales_invoice/tests/test_sales_invoice_with_serialize_item.js index 2697758d7a3..af484d7899c 100644 --- a/erpnext/accounts/doctype/sales_invoice/tests/test_sales_invoice_with_serialize_item.js +++ b/erpnext/accounts/doctype/sales_invoice/tests/test_sales_invoice_with_serialize_item.js @@ -42,4 +42,3 @@ QUnit.test("test sales Invoice with serialize item", function(assert) { () => done() ]); }); - diff --git a/erpnext/accounts/doctype/sales_invoice_advance/sales_invoice_advance.json b/erpnext/accounts/doctype/sales_invoice_advance/sales_invoice_advance.json index 14bf4d81330..29422d68cf6 100644 --- a/erpnext/accounts/doctype/sales_invoice_advance/sales_invoice_advance.json +++ b/erpnext/accounts/doctype/sales_invoice_advance/sales_invoice_advance.json @@ -1,235 +1,128 @@ { - "allow_copy": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2013-02-22 01:27:41", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Document", - "editable_grid": 1, + "actions": [], + "creation": "2013-02-22 01:27:41", + "doctype": "DocType", + "document_type": "Document", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "reference_type", + "reference_name", + "remarks", + "reference_row", + "col_break1", + "advance_amount", + "allocated_amount", + "exchange_gain_loss", + "ref_exchange_rate" + ], "fields": [ { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "reference_type", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Reference Type", - "length": 0, - "no_copy": 1, - "oldfieldname": "journal_voucher", - "oldfieldtype": "Link", - "options": "DocType", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "250px", - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0, + "fieldname": "reference_type", + "fieldtype": "Link", + "label": "Reference Type", + "no_copy": 1, + "oldfieldname": "journal_voucher", + "oldfieldtype": "Link", + "options": "DocType", + "print_width": "250px", + "read_only": 1, "width": "250px" - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 3, - "fieldname": "reference_name", - "fieldtype": "Dynamic Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "label": "Reference Name", - "length": 0, - "no_copy": 1, - "options": "reference_type", - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "columns": 3, + "fieldname": "reference_name", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "label": "Reference Name", + "no_copy": 1, + "options": "reference_type", + "print_hide": 1, + "read_only": 1 + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 3, - "fieldname": "remarks", - "fieldtype": "Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "label": "Remarks", - "length": 0, - "no_copy": 1, - "oldfieldname": "remarks", - "oldfieldtype": "Small Text", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "150px", - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0, + "columns": 3, + "fieldname": "remarks", + "fieldtype": "Text", + "in_list_view": 1, + "label": "Remarks", + "no_copy": 1, + "oldfieldname": "remarks", + "oldfieldtype": "Small Text", + "print_width": "150px", + "read_only": 1, "width": "150px" - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "reference_row", - "fieldtype": "Data", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Reference Row", - "length": 0, - "no_copy": 1, - "oldfieldname": "jv_detail_no", - "oldfieldtype": "Data", - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "print_width": "120px", - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0, + "fieldname": "reference_row", + "fieldtype": "Data", + "hidden": 1, + "label": "Reference Row", + "no_copy": 1, + "oldfieldname": "jv_detail_no", + "oldfieldtype": "Data", + "print_hide": 1, + "print_width": "120px", + "read_only": 1, "width": "120px" - }, + }, { - "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_list_view": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "col_break1", + "fieldtype": "Column Break" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 2, - "fieldname": "advance_amount", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "label": "Advance amount", - "length": 0, - "no_copy": 1, - "oldfieldname": "advance_amount", - "oldfieldtype": "Currency", - "options": "party_account_currency", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "120px", - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0, + "columns": 2, + "fieldname": "advance_amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Advance amount", + "no_copy": 1, + "oldfieldname": "advance_amount", + "oldfieldtype": "Currency", + "options": "party_account_currency", + "print_width": "120px", + "read_only": 1, "width": "120px" - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 2, - "fieldname": "allocated_amount", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "label": "Allocated amount", - "length": 0, - "no_copy": 1, - "oldfieldname": "allocated_amount", - "oldfieldtype": "Currency", - "options": "party_account_currency", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "120px", - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0, + "columns": 2, + "fieldname": "allocated_amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Allocated amount", + "no_copy": 1, + "oldfieldname": "allocated_amount", + "oldfieldtype": "Currency", + "options": "party_account_currency", + "print_width": "120px", "width": "120px" + }, + { + "fieldname": "exchange_gain_loss", + "fieldtype": "Currency", + "label": "Exchange Gain/Loss", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "ref_exchange_rate", + "fieldtype": "Float", + "label": "Reference Exchange Rate", + "non_negative": 1, + "read_only": 1 } - ], - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 1, - "image_view": 0, - "in_create": 0, - - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "menu_index": 0, - "modified": "2016-08-26 02:36:10.718057", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Sales Invoice Advance", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "sort_order": "DESC", - "track_seen": 0 + ], + "idx": 1, + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-06-04 20:25:49.832052", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Sales Invoice Advance", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC" } \ No newline at end of file diff --git a/erpnext/accounts/doctype/sales_invoice_advance/sales_invoice_advance.py b/erpnext/accounts/doctype/sales_invoice_advance/sales_invoice_advance.py index 1ec517929ef..28aeef4d5e1 100644 --- a/erpnext/accounts/doctype/sales_invoice_advance/sales_invoice_advance.py +++ b/erpnext/accounts/doctype/sales_invoice_advance/sales_invoice_advance.py @@ -7,4 +7,4 @@ import frappe from frappe.model.document import Document class SalesInvoiceAdvance(Document): - pass \ No newline at end of file + pass 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 8e6952a93c4..d27a3a779ed 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", @@ -63,6 +62,7 @@ "finance_book", "col_break4", "expense_account", + "discount_account", "deferred_revenue", "deferred_revenue_account", "service_stop_date", @@ -80,6 +80,7 @@ "target_warehouse", "quality_inspection", "batch_no", + "incoming_rate", "col_break5", "allow_zero_valuation_rate", "serial_no", @@ -473,6 +474,7 @@ }, { "collapsible": 1, + "collapsible_depends_on": "enable_deferred_revenue", "fieldname": "deferred_revenue", "fieldtype": "Section Break", "label": "Deferred Revenue" @@ -806,12 +808,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", @@ -821,12 +823,18 @@ "no_copy": 1, "options": "currency", "read_only": 1 + }, + { + "fieldname": "discount_account", + "fieldtype": "Link", + "label": "Discount Account", + "options": "Account" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2021-02-23 01:05:22.123527", + "modified": "2021-08-19 13:41:53.435827", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice Item", @@ -834,4 +842,4 @@ "permissions": [], "sort_field": "modified", "sort_order": "DESC" -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/sales_invoice_timesheet/sales_invoice_timesheet.json b/erpnext/accounts/doctype/sales_invoice_timesheet/sales_invoice_timesheet.json index f069e8dd0b8..2cddbbfceee 100644 --- a/erpnext/accounts/doctype/sales_invoice_timesheet/sales_invoice_timesheet.json +++ b/erpnext/accounts/doctype/sales_invoice_timesheet/sales_invoice_timesheet.json @@ -7,10 +7,19 @@ "field_order": [ "activity_type", "description", + "section_break_3", + "from_time", + "column_break_5", + "to_time", + "section_break_7", "billing_hours", + "column_break_9", "billing_amount", + "section_break_11", + "timesheet_detail", + "column_break_5", "time_sheet", - "timesheet_detail" + "project_name" ], "fields": [ { @@ -61,11 +70,51 @@ "in_list_view": 1, "label": "Description", "read_only": 1 + }, + { + "fieldname": "from_time", + "fieldtype": "Datetime", + "label": "From Time" + }, + { + "fieldname": "to_time", + "fieldtype": "Datetime", + "label": "To Time" + }, + { + "fieldname": "section_break_3", + "fieldtype": "Section Break", + "label": "Time" + }, + { + "fieldname": "column_break_5", + "fieldtype": "Column Break" + }, + { + + "fieldname": "section_break_7", + "fieldtype": "Section Break", + "label": "Totals" + }, + { + "fieldname": "column_break_9", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_11", + "fieldtype": "Section Break", + "label": "Reference" + }, + { + "fieldname": "project_name", + "fieldtype": "Data", + "label": "Project Name", + "read_only": 1 } ], "istable": 1, "links": [], - "modified": "2021-05-20 22:33:57.234846", + "modified": "2021-08-15 18:37:08.084930", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice Timesheet", diff --git a/erpnext/accounts/doctype/sales_partner_item/__init__.py b/erpnext/accounts/doctype/sales_partner_item/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/accounts/doctype/sales_partner_item/sales_partner_item.json b/erpnext/accounts/doctype/sales_partner_item/sales_partner_item.json new file mode 100644 index 00000000000..c176e4d173d --- /dev/null +++ b/erpnext/accounts/doctype/sales_partner_item/sales_partner_item.json @@ -0,0 +1,31 @@ +{ + "actions": [], + "creation": "2021-05-06 16:17:44.329943", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "sales_partner" + ], + "fields": [ + { + "fieldname": "sales_partner", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Sales Partner ", + "options": "Sales Partner" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-05-07 10:43:37.532095", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Sales Partner Item", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/sales_partner_item/sales_partner_item.py b/erpnext/accounts/doctype/sales_partner_item/sales_partner_item.py new file mode 100644 index 00000000000..9796c7b0ccf --- /dev/null +++ b/erpnext/accounts/doctype/sales_partner_item/sales_partner_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 SalesPartnerItem(Document): + pass diff --git a/erpnext/accounts/doctype/sales_taxes_and_charges/sales_taxes_and_charges.json b/erpnext/accounts/doctype/sales_taxes_and_charges/sales_taxes_and_charges.json index 1b7a0fe562e..3a871bfcede 100644 --- a/erpnext/accounts/doctype/sales_taxes_and_charges/sales_taxes_and_charges.json +++ b/erpnext/accounts/doctype/sales_taxes_and_charges/sales_taxes_and_charges.json @@ -19,7 +19,7 @@ "section_break_8", "rate", "section_break_9", - "currency", + "account_currency", "tax_amount", "total", "tax_amount_after_discount_amount", @@ -27,7 +27,8 @@ "base_tax_amount", "base_total", "base_tax_amount_after_discount_amount", - "item_wise_tax_detail" + "item_wise_tax_detail", + "dont_recompute_tax" ], "fields": [ { @@ -185,14 +186,6 @@ "fieldname": "dimension_col_break", "fieldtype": "Column Break" }, - { - "fetch_from": "account_head.account_currency", - "fieldname": "currency", - "fieldtype": "Link", - "label": "Account Currency", - "options": "Currency", - "read_only": 1 - }, { "default": "0", "depends_on": "eval:['Sales Taxes and Charges Template', 'Payment Entry'].includes(parent.doctype)", @@ -200,13 +193,30 @@ "fieldname": "included_in_paid_amount", "fieldtype": "Check", "label": "Considered In Paid Amount" + }, + { + "default": "0", + "fieldname": "dont_recompute_tax", + "fieldtype": "Check", + "hidden": 1, + "label": "Dont Recompute tax", + "print_hide": 1, + "read_only": 1 + }, + { + "fetch_from": "account_head.account_currency", + "fieldname": "account_currency", + "fieldtype": "Link", + "label": "Account Currency", + "options": "Currency", + "read_only": 1 } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-06-14 01:44:36.899147", + "modified": "2021-08-05 20:04:01.726867", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Taxes and Charges", diff --git a/erpnext/accounts/doctype/sales_taxes_and_charges/sales_taxes_and_charges.py b/erpnext/accounts/doctype/sales_taxes_and_charges/sales_taxes_and_charges.py index 8d1df5c19a3..b1de9d85fdb 100644 --- a/erpnext/accounts/doctype/sales_taxes_and_charges/sales_taxes_and_charges.py +++ b/erpnext/accounts/doctype/sales_taxes_and_charges/sales_taxes_and_charges.py @@ -7,4 +7,4 @@ import frappe from frappe.model.document import Document class SalesTaxesandCharges(Document): - pass \ No newline at end of file + pass diff --git a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py index 52d19d54a8b..8f9eb6577b8 100644 --- a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py +++ b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py @@ -6,7 +6,7 @@ import frappe from frappe import _ from frappe.utils import flt from frappe.model.document import Document -from erpnext.controllers.accounts_controller import validate_taxes_and_charges, validate_inclusive_tax +from erpnext.controllers.accounts_controller import validate_taxes_and_charges, validate_inclusive_tax, validate_cost_center, validate_account_head class SalesTaxesandChargesTemplate(Document): def validate(self): @@ -39,6 +39,8 @@ def valdiate_taxes_and_charges_template(doc): for tax in doc.get("taxes"): validate_taxes_and_charges(tax) + validate_account_head(tax, doc) + validate_cost_center(tax, doc) validate_inclusive_tax(tax, doc) def validate_disabled(doc): diff --git a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template_dashboard.py b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template_dashboard.py index d825c6fd325..522e282a170 100644 --- a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template_dashboard.py +++ b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template_dashboard.py @@ -21,4 +21,4 @@ def get_data(): 'items': ['POS Profile', 'Subscription', 'Restaurant', 'Tax Rule'] } ] - } \ No newline at end of file + } diff --git a/erpnext/accounts/doctype/sales_taxes_and_charges_template/test_records.json b/erpnext/accounts/doctype/sales_taxes_and_charges_template/test_records.json index 2b737b98048..74db08d5b86 100644 --- a/erpnext/accounts/doctype/sales_taxes_and_charges_template/test_records.json +++ b/erpnext/accounts/doctype/sales_taxes_and_charges_template/test_records.json @@ -8,6 +8,7 @@ "charge_type": "On Net Total", "description": "VAT", "doctype": "Sales Taxes and Charges", + "cost_center": "Main - _TC", "parentfield": "taxes", "rate": 6 }, @@ -16,6 +17,7 @@ "charge_type": "On Net Total", "description": "Service Tax", "doctype": "Sales Taxes and Charges", + "cost_center": "Main - _TC", "parentfield": "taxes", "rate": 6.36 } @@ -114,6 +116,7 @@ "charge_type": "On Net Total", "description": "VAT", "doctype": "Sales Taxes and Charges", + "cost_center": "Main - _TC", "parentfield": "taxes", "rate": 12 }, @@ -122,6 +125,7 @@ "charge_type": "On Net Total", "description": "Service Tax", "doctype": "Sales Taxes and Charges", + "cost_center": "Main - _TC", "parentfield": "taxes", "rate": 4 } @@ -137,6 +141,7 @@ "charge_type": "On Net Total", "description": "VAT", "doctype": "Sales Taxes and Charges", + "cost_center": "Main - _TC", "parentfield": "taxes", "rate": 12 }, @@ -145,6 +150,7 @@ "charge_type": "On Net Total", "description": "Service Tax", "doctype": "Sales Taxes and Charges", + "cost_center": "Main - _TC", "parentfield": "taxes", "rate": 4 } @@ -160,6 +166,7 @@ "charge_type": "On Net Total", "description": "VAT", "doctype": "Sales Taxes and Charges", + "cost_center": "Main - _TC", "parentfield": "taxes", "rate": 12 }, @@ -168,6 +175,7 @@ "charge_type": "On Net Total", "description": "Service Tax", "doctype": "Sales Taxes and Charges", + "cost_center": "Main - _TC", "parentfield": "taxes", "rate": 4 } diff --git a/erpnext/accounts/doctype/sales_taxes_and_charges_template/test_sales_taxes_and_charges_template.js b/erpnext/accounts/doctype/sales_taxes_and_charges_template/test_sales_taxes_and_charges_template.js index d02e70b5419..8cd42f63a41 100644 --- a/erpnext/accounts/doctype/sales_taxes_and_charges_template/test_sales_taxes_and_charges_template.js +++ b/erpnext/accounts/doctype/sales_taxes_and_charges_template/test_sales_taxes_and_charges_template.js @@ -26,4 +26,3 @@ QUnit.test("test sales taxes and charges template", function(assert) { () => done() ]); }); - diff --git a/erpnext/accounts/doctype/share_transfer/share_transfer.js b/erpnext/accounts/doctype/share_transfer/share_transfer.js index 1cad4dfae3d..6317c9c8c0d 100644 --- a/erpnext/accounts/doctype/share_transfer/share_transfer.js +++ b/erpnext/accounts/doctype/share_transfer/share_transfer.js @@ -115,4 +115,4 @@ erpnext.share_transfer.make_jv = function (frm) { frappe.set_route("Form", doc.doctype, doc.name); } }); -}; \ No newline at end of file +}; diff --git a/erpnext/accounts/doctype/share_transfer/share_transfer.py b/erpnext/accounts/doctype/share_transfer/share_transfer.py index 4024b8155a4..3d4543fb051 100644 --- a/erpnext/accounts/doctype/share_transfer/share_transfer.py +++ b/erpnext/accounts/doctype/share_transfer/share_transfer.py @@ -299,4 +299,4 @@ def make_jv_entry( company, account, amount, payment_account,\ "party": credit_applicant, }) journal_entry.set("accounts", account_amt_list) - return journal_entry.as_dict() \ No newline at end of file + return journal_entry.as_dict() 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 e5530fa0aa6..00000000000 --- 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 620afa2ba85..00000000000 --- 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 61c53120ea6..00000000000 --- 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/shipping_rule/test_shipping_rule.js b/erpnext/accounts/doctype/shipping_rule/test_shipping_rule.js index 0201f762b37..63ea1bf35f4 100644 --- a/erpnext/accounts/doctype/shipping_rule/test_shipping_rule.js +++ b/erpnext/accounts/doctype/shipping_rule/test_shipping_rule.js @@ -34,4 +34,3 @@ QUnit.test("test Shipping Rule", function(assert) { () => done() ]); }); - diff --git a/erpnext/accounts/doctype/shipping_rule/tests/test_shipping_rule_for_buying.js b/erpnext/accounts/doctype/shipping_rule/tests/test_shipping_rule_for_buying.js index ab1b77cd5f5..f3668b8b406 100644 --- a/erpnext/accounts/doctype/shipping_rule/tests/test_shipping_rule_for_buying.js +++ b/erpnext/accounts/doctype/shipping_rule/tests/test_shipping_rule_for_buying.js @@ -34,4 +34,3 @@ QUnit.test("test Shipping Rule", function(assert) { () => done() ]); }); - diff --git a/erpnext/accounts/doctype/shipping_rule_condition/shipping_rule_condition.py b/erpnext/accounts/doctype/shipping_rule_condition/shipping_rule_condition.py index dab59db70c3..db6ef117c22 100644 --- a/erpnext/accounts/doctype/shipping_rule_condition/shipping_rule_condition.py +++ b/erpnext/accounts/doctype/shipping_rule_condition/shipping_rule_condition.py @@ -9,4 +9,4 @@ import frappe from frappe.model.document import Document class ShippingRuleCondition(Document): - pass \ No newline at end of file + pass diff --git a/erpnext/accounts/doctype/south_africa_vat_account/__init__.py b/erpnext/accounts/doctype/south_africa_vat_account/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/accounts/doctype/south_africa_vat_account/south_africa_vat_account.json b/erpnext/accounts/doctype/south_africa_vat_account/south_africa_vat_account.json new file mode 100644 index 00000000000..fa1aa7da594 --- /dev/null +++ b/erpnext/accounts/doctype/south_africa_vat_account/south_africa_vat_account.json @@ -0,0 +1,34 @@ +{ + "actions": [], + "autoname": "account", + "creation": "2021-07-08 22:04:24.634967", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "account" + ], + "fields": [ + { + "allow_in_quick_entry": 1, + "fieldname": "account", + "fieldtype": "Link", + "in_list_view": 1, + "in_preview": 1, + "label": "Account", + "options": "Account" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-07-08 22:35:33.202911", + "modified_by": "Administrator", + "module": "Accounts", + "name": "South Africa VAT Account", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/south_africa_vat_account/south_africa_vat_account.py b/erpnext/accounts/doctype/south_africa_vat_account/south_africa_vat_account.py new file mode 100644 index 00000000000..4bd8c65a046 --- /dev/null +++ b/erpnext/accounts/doctype/south_africa_vat_account/south_africa_vat_account.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 SouthAfricaVATAccount(Document): + pass diff --git a/erpnext/accounts/doctype/subscription/subscription_list.js b/erpnext/accounts/doctype/subscription/subscription_list.js index c7325fb9f74..6490ff3776e 100644 --- a/erpnext/accounts/doctype/subscription/subscription_list.js +++ b/erpnext/accounts/doctype/subscription/subscription_list.js @@ -14,4 +14,4 @@ frappe.listview_settings['Subscription'] = { return [__("Cancelled"), "gray"]; } } -}; \ No newline at end of file +}; diff --git a/erpnext/accounts/doctype/subscription/test_subscription.py b/erpnext/accounts/doctype/subscription/test_subscription.py index 7c58e9865fd..4f2cf487a4f 100644 --- a/erpnext/accounts/doctype/subscription/test_subscription.py +++ b/erpnext/accounts/doctype/subscription/test_subscription.py @@ -630,5 +630,3 @@ class TestSubscription(unittest.TestCase): subscription.process() self.assertEqual(len(subscription.invoices), 1) - - 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 15d3df2a633..00000000000 --- 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.js b/erpnext/accounts/doctype/subscription_plan/subscription_plan.js index aaa32cfe7ef..7d6f2aed100 100644 --- a/erpnext/accounts/doctype/subscription_plan/subscription_plan.js +++ b/erpnext/accounts/doctype/subscription_plan/subscription_plan.js @@ -6,4 +6,4 @@ frappe.ui.form.on('Subscription Plan', { frm.toggle_reqd("cost", frm.doc.price_determination === 'Fixed rate'); frm.toggle_reqd("price_list", frm.doc.price_determination === 'Based on price list'); } -}); \ No newline at end of file +}); diff --git a/erpnext/accounts/doctype/subscription_plan/subscription_plan.json b/erpnext/accounts/doctype/subscription_plan/subscription_plan.json index 46ce0939e4f..878ae098891 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", @@ -78,7 +78,7 @@ "label": "Cost" }, { - "depends_on": "eval:doc.price_determination==\"Based on price list\"", + "depends_on": "eval:doc.price_determination==\"Based On Price List\"", "fieldname": "price_list", "fieldtype": "Link", "label": "Price List", @@ -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": "2020-06-25 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/subscription_plan.py b/erpnext/accounts/doctype/subscription_plan/subscription_plan.py index 1ca442a4531..a341c2af6ac 100644 --- a/erpnext/accounts/doctype/subscription_plan/subscription_plan.py +++ b/erpnext/accounts/doctype/subscription_plan/subscription_plan.py @@ -54,4 +54,4 @@ def get_plan_rate(plan, quantity=1, customer=None, start_date=None, end_date=Non cost -= (plan.cost * prorate_factor) - return cost \ No newline at end of file + return cost 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 3ceb9a60507..00000000000 --- 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 5a751ea99c3..00000000000 --- 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/supplier_group_item/__init__.py b/erpnext/accounts/doctype/supplier_group_item/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/accounts/doctype/supplier_group_item/supplier_group_item.json b/erpnext/accounts/doctype/supplier_group_item/supplier_group_item.json new file mode 100644 index 00000000000..67fac458456 --- /dev/null +++ b/erpnext/accounts/doctype/supplier_group_item/supplier_group_item.json @@ -0,0 +1,31 @@ +{ + "actions": [], + "creation": "2021-05-06 16:19:22.040795", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "supplier_group" + ], + "fields": [ + { + "fieldname": "supplier_group", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Supplier Group", + "options": "Supplier Group" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-05-07 10:43:59.877938", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Supplier Group Item", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/supplier_group_item/supplier_group_item.py b/erpnext/accounts/doctype/supplier_group_item/supplier_group_item.py new file mode 100644 index 00000000000..de0444ee193 --- /dev/null +++ b/erpnext/accounts/doctype/supplier_group_item/supplier_group_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 SupplierGroupItem(Document): + pass diff --git a/erpnext/accounts/doctype/supplier_item/__init__.py b/erpnext/accounts/doctype/supplier_item/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/accounts/doctype/supplier_item/supplier_item.json b/erpnext/accounts/doctype/supplier_item/supplier_item.json new file mode 100644 index 00000000000..95c4dc6db36 --- /dev/null +++ b/erpnext/accounts/doctype/supplier_item/supplier_item.json @@ -0,0 +1,31 @@ +{ + "actions": [], + "creation": "2021-05-06 16:18:54.758468", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "supplier" + ], + "fields": [ + { + "fieldname": "supplier", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Supplier", + "options": "Supplier" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-05-07 10:44:09.707778", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Supplier Item", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/supplier_item/supplier_item.py b/erpnext/accounts/doctype/supplier_item/supplier_item.py new file mode 100644 index 00000000000..ad66e230c80 --- /dev/null +++ b/erpnext/accounts/doctype/supplier_item/supplier_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 SupplierItem(Document): + pass 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 5142456d769..00000000000 --- 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/tax_rule.py b/erpnext/accounts/doctype/tax_rule/tax_rule.py index e4ebc6d12f9..58142318177 100644 --- a/erpnext/accounts/doctype/tax_rule/tax_rule.py +++ b/erpnext/accounts/doctype/tax_rule/tax_rule.py @@ -188,4 +188,4 @@ def get_customer_group_condition(customer_group): customer_groups = ["%s"%(frappe.db.escape(d.name)) for d in get_parent_customer_groups(customer_group)] if customer_groups: condition = ",".join(['%s'] * len(customer_groups))%(tuple(customer_groups)) - return condition \ No newline at end of file + return condition 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 72d177deffe..00000000000 --- 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/tax_withholding_category.json b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.json index f9160e281da..153906ffe97 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.json +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.json @@ -1,263 +1,151 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 1, - "allow_rename": 1, - "autoname": "Prompt", - "beta": 0, - "creation": "2018-04-13 18:42:06.431683", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "allow_import": 1, + "allow_rename": 1, + "autoname": "Prompt", + "creation": "2018-04-13 18:42:06.431683", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "category_details_section", + "category_name", + "round_off_tax_amount", + "column_break_2", + "consider_party_ledger_amount", + "tax_on_excess_amount", + "section_break_8", + "rates", + "section_break_7", + "accounts" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "category_name", "fieldtype": "Data", - "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": "Category Name", - "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 - }, + "show_days": 1, + "show_seconds": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "section_break_8", "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": "Tax Withholding Rates", - "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 + "show_days": 1, + "show_seconds": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "rates", "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": "Rates", - "length": 0, - "no_copy": 0, "options": "Tax Withholding Rate", - "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 + "show_days": 1, + "show_seconds": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_7", - "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, + "fieldname": "section_break_7", + "fieldtype": "Section Break", "label": "Account Details", - "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 - }, + "show_days": 1, + "show_seconds": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "accounts", - "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": "Accounts", - "length": 0, - "no_copy": 0, - "options": "Tax Withholding 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 + "fieldname": "accounts", + "fieldtype": "Table", + "label": "Accounts", + "options": "Tax Withholding Account", + "reqd": 1, + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "category_details_section", + "fieldtype": "Section Break", + "label": "Category Details", + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 + }, + { + "default": "0", + "description": "Even invoices with apply tax withholding unchecked will be considered for checking cumulative threshold breach", + "fieldname": "consider_party_ledger_amount", + "fieldtype": "Check", + "label": "Consider Entire Party Ledger Amount", + "show_days": 1, + "show_seconds": 1 + }, + { + "default": "0", + "description": "Tax will be withheld only for amount exceeding the cumulative threshold", + "fieldname": "tax_on_excess_amount", + "fieldtype": "Check", + "label": "Only Deduct Tax On Excess Amount ", + "show_days": 1, + "show_seconds": 1 + }, + { + "description": "Checking this will round off the tax amount to the nearest integer", + "fieldname": "round_off_tax_amount", + "fieldtype": "Check", + "label": "Round Off Tax Amount", + "show_days": 1, + "show_seconds": 1 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-07-17 22:53:26.193179", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Tax Withholding Category", - "name_case": "", - "owner": "Administrator", + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2021-07-27 21:47:34.396071", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Tax Withholding Category", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, "write": 1 - }, + }, { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Accounts Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts Manager", + "share": 1, "write": 1 - }, + }, { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Accounts User", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 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/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py index b9ee4a0963f..0cb872c4b81 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -6,7 +6,7 @@ from __future__ import unicode_literals import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import flt, getdate +from frappe.utils import flt, getdate, cint from erpnext.accounts.utils import get_fiscal_year class TaxWithholdingCategory(Document): @@ -86,7 +86,10 @@ def get_tax_withholding_details(tax_withholding_category, fiscal_year, company): "rate": tax_rate_detail.tax_withholding_rate, "threshold": tax_rate_detail.single_threshold, "cumulative_threshold": tax_rate_detail.cumulative_threshold, - "description": tax_withholding.category_name if tax_withholding.category_name else tax_withholding_category + "description": tax_withholding.category_name if tax_withholding.category_name else tax_withholding_category, + "consider_party_ledger_amount": tax_withholding.consider_party_ledger_amount, + "tax_on_excess_amount": tax_withholding.tax_on_excess_amount, + "round_off_tax_amount": tax_withholding.round_off_tax_amount }) def get_tax_withholding_rates(tax_withholding, fiscal_year): @@ -145,6 +148,7 @@ def get_lower_deduction_certificate(fiscal_year, pan_no): def get_tax_amount(party_type, parties, inv, tax_details, fiscal_year_details, pan_no=None): fiscal_year = fiscal_year_details[0] + vouchers = get_invoice_vouchers(parties, fiscal_year, inv.company, party_type=party_type) advance_vouchers = get_advance_vouchers(parties, fiscal_year, inv.company, party_type=party_type) taxable_vouchers = vouchers + advance_vouchers @@ -235,10 +239,19 @@ def get_deducted_tax(taxable_vouchers, fiscal_year, tax_details): def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, tax_deducted, vouchers): tds_amount = 0 + invoice_filters = { + 'name': ('in', vouchers), + 'docstatus': 1, + 'apply_tds': 1 + } - supp_credit_amt = frappe.db.get_value('Purchase Invoice', { - 'name': ('in', vouchers), 'docstatus': 1, 'apply_tds': 1 - }, 'sum(net_total)') or 0.0 + field = 'sum(net_total)' + + if cint(tax_details.consider_party_ledger_amount): + invoice_filters.pop('apply_tds', None) + field = 'sum(grand_total)' + + supp_credit_amt = frappe.db.get_value('Purchase Invoice', invoice_filters, field) or 0.0 supp_jv_credit_amt = frappe.db.get_value('Journal Entry Account', { 'parent': ('in', vouchers), 'docstatus': 1, @@ -255,6 +268,13 @@ def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, tax_dedu cumulative_threshold = tax_details.get('cumulative_threshold', 0) if ((threshold and inv.net_total >= threshold) or (cumulative_threshold and supp_credit_amt >= cumulative_threshold)): + if (cumulative_threshold and supp_credit_amt >= cumulative_threshold) and cint(tax_details.tax_on_excess_amount): + # Get net total again as TDS is calculated on net total + # Grand is used to just check for threshold breach + net_total = frappe.db.get_value('Purchase Invoice', invoice_filters, 'sum(net_total)') or 0.0 + net_total += inv.net_total + supp_credit_amt = net_total - cumulative_threshold + if ldc and is_valid_certificate( ldc.valid_from, ldc.valid_upto, inv.get('posting_date') or inv.get('transaction_date'), tax_deducted, @@ -264,6 +284,9 @@ def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, tax_dedu else: tds_amount = supp_credit_amt * tax_details.rate / 100 if supp_credit_amt > 0 else 0 + if cint(tax_details.round_off_tax_amount): + tds_amount = round(tds_amount) + return tds_amount def get_tcs_amount(parties, inv, tax_details, fiscal_year_details, vouchers, adv_vouchers): 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 eab98d43892..00000000000 --- 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/doctype/tax_withholding_category/test_tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py index dd26be7c992..0f921db678d 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py @@ -87,6 +87,31 @@ class TestTaxWithholdingCategory(unittest.TestCase): for d in invoices: d.cancel() + def test_tax_withholding_category_checks(self): + invoices = [] + frappe.db.set_value("Supplier", "Test TDS Supplier3", "tax_withholding_category", "New TDS Category") + + # First Invoice with no tds check + pi = create_purchase_invoice(supplier = "Test TDS Supplier3", rate = 20000, do_not_save=True) + pi.apply_tds = 0 + pi.save() + pi.submit() + invoices.append(pi) + + # Second Invoice will apply TDS checked + pi1 = create_purchase_invoice(supplier = "Test TDS Supplier3", rate = 20000) + pi1.submit() + invoices.append(pi1) + + # Cumulative threshold is 30000 + # Threshold calculation should be on both the invoices + # TDS should be applied only on 1000 + self.assertEqual(pi1.taxes[0].tax_amount, 1000) + + for d in invoices: + d.cancel() + + def test_cumulative_threshold_tcs(self): frappe.db.set_value("Customer", "Test TCS Customer", "tax_withholding_category", "Cumulative Threshold TCS") invoices = [] @@ -120,6 +145,36 @@ class TestTaxWithholdingCategory(unittest.TestCase): for d in invoices: d.cancel() + def test_tds_calculation_on_net_total(self): + frappe.db.set_value("Supplier", "Test TDS Supplier4", "tax_withholding_category", "Cumulative Threshold TDS") + invoices = [] + + pi = create_purchase_invoice(supplier = "Test TDS Supplier4", rate = 20000, do_not_save=True) + pi.append('taxes', { + "category": "Total", + "charge_type": "Actual", + "account_head": '_Test Account VAT - _TC', + "cost_center": 'Main - _TC', + "tax_amount": 1000, + "description": "Test", + "add_deduct_tax": "Add" + + }) + pi.save() + pi.submit() + invoices.append(pi) + + # Second Invoice will apply TDS checked + pi1 = create_purchase_invoice(supplier = "Test TDS Supplier4", rate = 20000) + pi1.submit() + invoices.append(pi1) + + self.assertEqual(pi1.taxes[0].tax_amount, 4000) + + #delete invoices to avoid clashing + for d in invoices: + d.cancel() + def cancel_invoices(): purchase_invoices = frappe.get_all("Purchase Invoice", { 'supplier': ['in', ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2']], @@ -195,7 +250,7 @@ def create_sales_invoice(**args): def create_records(): # create a new suppliers - for name in ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2']: + for name in ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2', 'Test TDS Supplier3', 'Test TDS Supplier4']: if frappe.db.exists('Supplier', name): continue @@ -311,3 +366,23 @@ def create_tax_with_holding_category(): 'account': 'TDS - _TC' }] }).insert() + + if not frappe.db.exists("Tax Withholding Category", "New TDS Category"): + frappe.get_doc({ + "doctype": "Tax Withholding Category", + "name": "New TDS Category", + "category_name": "New TDS Category", + "round_off_tax_amount": 1, + "consider_party_ledger_amount": 1, + "tax_on_excess_amount": 1, + "rates": [{ + 'fiscal_year': fiscal_year, + 'tax_withholding_rate': 10, + 'single_threshold': 0, + 'cumulative_threshold': 30000 + }], + "accounts": [{ + 'company': '_Test Company', + 'account': 'TDS - _TC' + }] + }).insert() diff --git a/erpnext/accounts/doctype/territory_item/__init__.py b/erpnext/accounts/doctype/territory_item/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/accounts/doctype/territory_item/territory_item.json b/erpnext/accounts/doctype/territory_item/territory_item.json new file mode 100644 index 00000000000..0f0fdea8c01 --- /dev/null +++ b/erpnext/accounts/doctype/territory_item/territory_item.json @@ -0,0 +1,31 @@ +{ + "actions": [], + "creation": "2021-05-06 16:16:51.885441", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "territory" + ], + "fields": [ + { + "fieldname": "territory", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Territory", + "options": "Territory" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-05-07 10:43:26.641030", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Territory Item", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/territory_item/territory_item.py b/erpnext/accounts/doctype/territory_item/territory_item.py new file mode 100644 index 00000000000..d46edc9dca8 --- /dev/null +++ b/erpnext/accounts/doctype/territory_item/territory_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 TerritoryItem(Document): + pass diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 25d2cf10bd4..31261384080 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -100,8 +100,8 @@ def merge_similar_entries(gl_map, precision=None): return merged_gl_map def check_if_in_list(gle, gl_map, dimensions=None): - account_head_fieldnames = ['party_type', 'party', 'against_voucher', 'against_voucher_type', - 'cost_center', 'project', 'voucher_detail_no'] + account_head_fieldnames = ['voucher_detail_no', 'party', 'against_voucher', + 'cost_center', 'against_voucher_type', 'party_type', 'project', 'finance_book'] if dimensions: account_head_fieldnames = account_head_fieldnames + dimensions @@ -110,10 +110,12 @@ def check_if_in_list(gle, gl_map, dimensions=None): same_head = True if e.account != gle.account: same_head = False + continue for fieldname in account_head_fieldnames: if cstr(e.get(fieldname)) != cstr(gle.get(fieldname)): same_head = False + break if same_head: return e @@ -143,16 +145,19 @@ def make_entry(args, adv_adj, update_outstanding, from_repost=False): validate_expense_against_budget(args) def validate_cwip_accounts(gl_map): - cwip_enabled = any(cint(ac.enable_cwip_accounting) for ac in frappe.db.get_all("Asset Category","enable_cwip_accounting")) + """Validate that CWIP account are not used in Journal Entry""" + if gl_map and gl_map[0].voucher_type != "Journal Entry": + return - if cwip_enabled and gl_map[0].voucher_type == "Journal Entry": - cwip_accounts = [d[0] for d in frappe.db.sql("""select name from tabAccount - where account_type = 'Capital Work in Progress' and is_group=0""")] + cwip_enabled = any(cint(ac.enable_cwip_accounting) for ac in frappe.db.get_all("Asset Category", "enable_cwip_accounting")) + if cwip_enabled: + cwip_accounts = [d[0] for d in frappe.db.sql("""select name from tabAccount + where account_type = 'Capital Work in Progress' and is_group=0""")] - for entry in gl_map: - if entry.account in cwip_accounts: - frappe.throw( - _("Account: {0} is capital Work in progress and can not be updated by Journal Entry").format(entry.account)) + for entry in gl_map: + if entry.account in cwip_accounts: + frappe.throw( + _("Account: {0} is capital Work in progress and can not be updated by Journal Entry").format(entry.account)) def round_off_debit_credit(gl_map): precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index b97dc401e6a..de7dde9dd1f 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -8,7 +8,7 @@ from frappe import _, msgprint, scrub from frappe.core.doctype.user_permission.user_permission import get_permitted_documents from frappe.model.utils import get_fetch_values from frappe.utils import (add_days, getdate, formatdate, date_diff, - add_years, get_timestamp, nowdate, flt, cstr, add_months, get_last_day) + add_years, get_timestamp, nowdate, flt, cstr, add_months, get_last_day, cint) from frappe.contacts.doctype.address.address import (get_address_display, get_default_address, get_company_address) from frappe.contacts.doctype.contact.contact import get_contact_details @@ -58,7 +58,7 @@ def _get_party_details(party=None, account=None, party_type="Customer", company= customer_group=party_details.customer_group, supplier_group=party_details.supplier_group, tax_category=party_details.tax_category, billing_address=party_address, shipping_address=shipping_address) - if fetch_payment_terms_template: + if cint(fetch_payment_terms_template): party_details["payment_terms_template"] = get_payment_terms_template(party.name, party_type, company) if not party_details.get("currency"): @@ -286,6 +286,7 @@ def validate_party_gle_currency(party_type, party, company, party_account_curren .format(frappe.bold(party_type), frappe.bold(party), frappe.bold(existing_gle_currency), frappe.bold(company)), InvalidAccountCurrency) def validate_party_accounts(doc): + companies = [] for account in doc.get("accounts"): @@ -446,6 +447,10 @@ def get_payment_terms_template(party_name, party_type, company=None): return template def validate_party_frozen_disabled(party_type, party_name): + + if frappe.flags.ignore_party_validation: + return + if party_type and party_name: if party_type in ("Customer", "Supplier"): party = frappe.get_cached_value(party_type, party_name, ["is_frozen", "disabled"], as_dict=True) diff --git a/erpnext/accounts/print_format/bank_and_cash_payment_voucher/bank_and_cash_payment_voucher.html b/erpnext/accounts/print_format/bank_and_cash_payment_voucher/bank_and_cash_payment_voucher.html index e588ed6609e..4ac657d1ae6 100644 --- a/erpnext/accounts/print_format/bank_and_cash_payment_voucher/bank_and_cash_payment_voucher.html +++ b/erpnext/accounts/print_format/bank_and_cash_payment_voucher/bank_and_cash_payment_voucher.html @@ -73,4 +73,4 @@
-
\ No newline at end of file + diff --git a/erpnext/accounts/print_format/bank_and_cash_payment_voucher/bank_and_cash_payment_voucher.json b/erpnext/accounts/print_format/bank_and_cash_payment_voucher/bank_and_cash_payment_voucher.json index e3afaec2ad0..9e126bade7b 100644 --- a/erpnext/accounts/print_format/bank_and_cash_payment_voucher/bank_and_cash_payment_voucher.json +++ b/erpnext/accounts/print_format/bank_and_cash_payment_voucher/bank_and_cash_payment_voucher.json @@ -16,7 +16,7 @@ "name": "Bank and Cash Payment Voucher", "owner": "Administrator", "print_format_builder": 0, - "print_format_type": "Server", + "print_format_type": "Jinja", "show_section_headings": 0, "standard": "Yes" } \ No newline at end of file diff --git a/erpnext/accounts/print_format/cheque_printing_format/cheque_printing_format.json b/erpnext/accounts/print_format/cheque_printing_format/cheque_printing_format.json index 8c9c26691e5..08b8ef88900 100755 --- a/erpnext/accounts/print_format/cheque_printing_format/cheque_printing_format.json +++ b/erpnext/accounts/print_format/cheque_printing_format/cheque_printing_format.json @@ -10,6 +10,6 @@ "modified_by": "Administrator", "name": "Cheque Printing Format", "owner": "Administrator", - "print_format_type": "Server", + "print_format_type": "Jinja", "standard": "Yes" } \ No newline at end of file diff --git a/erpnext/accounts/print_format/credit_note/credit_note.json b/erpnext/accounts/print_format/credit_note/credit_note.json index c5a11cf3958..d12b872dd79 100644 --- a/erpnext/accounts/print_format/credit_note/credit_note.json +++ b/erpnext/accounts/print_format/credit_note/credit_note.json @@ -17,7 +17,7 @@ "owner": "Administrator", "parentfield": "__print_formats", "print_format_builder": 0, - "print_format_type": "Server", + "print_format_type": "Jinja", "show_section_headings": 0, "standard": "Yes" } \ No newline at end of file diff --git a/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html b/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html index 71c26e8c55a..7643eca7635 100644 --- a/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html +++ b/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html @@ -159,4 +159,4 @@ - \ No newline at end of file + diff --git a/erpnext/accounts/print_format/gst_purchase_invoice/gst_purchase_invoice.json b/erpnext/accounts/print_format/gst_purchase_invoice/gst_purchase_invoice.json index 6d7c3d31ae4..1467f27e66a 100644 --- a/erpnext/accounts/print_format/gst_purchase_invoice/gst_purchase_invoice.json +++ b/erpnext/accounts/print_format/gst_purchase_invoice/gst_purchase_invoice.json @@ -16,7 +16,7 @@ "name": "GST Purchase Invoice", "owner": "Administrator", "print_format_builder": 1, - "print_format_type": "Server", + "print_format_type": "Jinja", "show_section_headings": 0, "standard": "Yes" } \ No newline at end of file diff --git a/erpnext/accounts/print_format/journal_auditing_voucher/journal_auditing_voucher.html b/erpnext/accounts/print_format/journal_auditing_voucher/journal_auditing_voucher.html index 0ca940f8bd5..c1c611ee3a3 100644 --- a/erpnext/accounts/print_format/journal_auditing_voucher/journal_auditing_voucher.html +++ b/erpnext/accounts/print_format/journal_auditing_voucher/journal_auditing_voucher.html @@ -68,4 +68,4 @@
-
\ No newline at end of file + diff --git a/erpnext/accounts/print_format/journal_auditing_voucher/journal_auditing_voucher.json b/erpnext/accounts/print_format/journal_auditing_voucher/journal_auditing_voucher.json index 927e818e017..e45295d9fea 100644 --- a/erpnext/accounts/print_format/journal_auditing_voucher/journal_auditing_voucher.json +++ b/erpnext/accounts/print_format/journal_auditing_voucher/journal_auditing_voucher.json @@ -16,7 +16,7 @@ "name": "Journal Auditing Voucher", "owner": "Administrator", "print_format_builder": 0, - "print_format_type": "Server", + "print_format_type": "Jinja", "show_section_headings": 0, "standard": "Yes" } \ No newline at end of file diff --git a/erpnext/accounts/print_format/payment_receipt_voucher/payment_receipt_voucher.html b/erpnext/accounts/print_format/payment_receipt_voucher/payment_receipt_voucher.html index 283d505e3be..ae07582704c 100644 --- a/erpnext/accounts/print_format/payment_receipt_voucher/payment_receipt_voucher.html +++ b/erpnext/accounts/print_format/payment_receipt_voucher/payment_receipt_voucher.html @@ -27,4 +27,3 @@ {{ _("Authorized Signatory") }}

- diff --git a/erpnext/accounts/print_format/payment_receipt_voucher/payment_receipt_voucher.json b/erpnext/accounts/print_format/payment_receipt_voucher/payment_receipt_voucher.json index f1de448bb98..c52eeb2dfb9 100755 --- a/erpnext/accounts/print_format/payment_receipt_voucher/payment_receipt_voucher.json +++ b/erpnext/accounts/print_format/payment_receipt_voucher/payment_receipt_voucher.json @@ -12,6 +12,6 @@ "name": "Payment Receipt Voucher", "owner": "Administrator", "print_format_builder": 0, - "print_format_type": "Server", + "print_format_type": "Jinja", "standard": "Yes" } \ No newline at end of file diff --git a/erpnext/accounts/print_format/purchase_auditing_voucher/purchase_auditing_voucher.html b/erpnext/accounts/print_format/purchase_auditing_voucher/purchase_auditing_voucher.html index 043ac254ed3..8696bffbfcb 100644 --- a/erpnext/accounts/print_format/purchase_auditing_voucher/purchase_auditing_voucher.html +++ b/erpnext/accounts/print_format/purchase_auditing_voucher/purchase_auditing_voucher.html @@ -103,4 +103,4 @@ - \ No newline at end of file + diff --git a/erpnext/accounts/print_format/purchase_auditing_voucher/purchase_auditing_voucher.json b/erpnext/accounts/print_format/purchase_auditing_voucher/purchase_auditing_voucher.json index 73779d49aab..33981178220 100644 --- a/erpnext/accounts/print_format/purchase_auditing_voucher/purchase_auditing_voucher.json +++ b/erpnext/accounts/print_format/purchase_auditing_voucher/purchase_auditing_voucher.json @@ -16,7 +16,7 @@ "name": "Purchase Auditing Voucher", "owner": "Administrator", "print_format_builder": 0, - "print_format_type": "Server", + "print_format_type": "Jinja", "show_section_headings": 0, "standard": "Yes" } \ No newline at end of file diff --git a/erpnext/accounts/print_format/sales_auditing_voucher/sales_auditing_voucher.html b/erpnext/accounts/print_format/sales_auditing_voucher/sales_auditing_voucher.html index a53b593a72a..efb2d00f0bf 100644 --- a/erpnext/accounts/print_format/sales_auditing_voucher/sales_auditing_voucher.html +++ b/erpnext/accounts/print_format/sales_auditing_voucher/sales_auditing_voucher.html @@ -93,4 +93,4 @@ - \ No newline at end of file + diff --git a/erpnext/accounts/print_format/sales_auditing_voucher/sales_auditing_voucher.json b/erpnext/accounts/print_format/sales_auditing_voucher/sales_auditing_voucher.json index 0544e0bc9e8..289e1848488 100644 --- a/erpnext/accounts/print_format/sales_auditing_voucher/sales_auditing_voucher.json +++ b/erpnext/accounts/print_format/sales_auditing_voucher/sales_auditing_voucher.json @@ -16,7 +16,7 @@ "name": "Sales Auditing Voucher", "owner": "Administrator", "print_format_builder": 0, - "print_format_type": "Server", + "print_format_type": "Jinja", "show_section_headings": 0, "standard": "Yes" } \ No newline at end of file diff --git a/erpnext/accounts/report/account_balance/test_account_balance.py b/erpnext/accounts/report/account_balance/test_account_balance.py index 14ddf4a30fc..f5c9449e85d 100644 --- a/erpnext/accounts/report/account_balance/test_account_balance.py +++ b/erpnext/accounts/report/account_balance/test_account_balance.py @@ -62,8 +62,3 @@ def make_sales_invoice(): income_account = 'Sales - _TC2', expense_account = 'Cost of Goods Sold - _TC2', cost_center = 'Main - _TC2') - - - - - diff --git a/erpnext/accounts/report/accounts_payable/accounts_payable.js b/erpnext/accounts/report/accounts_payable/accounts_payable.js index 6abd6e5cf77..b6c6689be0b 100644 --- a/erpnext/accounts/report/accounts_payable/accounts_payable.js +++ b/erpnext/accounts/report/accounts_payable/accounts_payable.js @@ -136,4 +136,3 @@ frappe.query_reports["Accounts Payable"] = { } erpnext.utils.add_dimensions('Accounts Payable', 9); - diff --git a/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js b/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js index 9c6b0639c0a..ea200720dff 100644 --- a/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js +++ b/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js @@ -105,4 +105,3 @@ frappe.query_reports["Accounts Payable Summary"] = { } erpnext.utils.add_dimensions('Accounts Payable Summary', 9); - diff --git a/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.py b/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.py index 729eda9492f..c08582b564c 100644 --- a/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.py +++ b/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.py @@ -12,4 +12,3 @@ def execute(filters=None): "naming_by": ["Buying Settings", "supp_master_name"], } return AccountsReceivableSummary(filters).run(args) - diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js index 29c4f7d3941..1a32e2a8e06 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js @@ -200,4 +200,3 @@ frappe.query_reports["Accounts Receivable"] = { } erpnext.utils.add_dimensions('Accounts Receivable', 9); - diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index a11b77a6f64..cedfc0f58b8 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -99,7 +99,6 @@ class ReceivablePayableReport(object): voucher_no = gle.voucher_no, party = gle.party, posting_date = gle.posting_date, - remarks = gle.remarks, account_currency = gle.account_currency, invoiced = 0.0, paid = 0.0, @@ -536,6 +535,8 @@ class ReceivablePayableReport(object): if getdate(entry_date) > getdate(self.filters.report_date): row.range1 = row.range2 = row.range3 = row.range4 = row.range5 = 0.0 + row.total_due = row.range1 + row.range2 + row.range3 + row.range4 + row.range5 + def get_ageing_data(self, entry_date, row): # [0-30, 30-60, 60-90, 90-120, 120-above] row.range1 = row.range2 = row.range3 = row.range4 = row.range5 = 0.0 @@ -579,7 +580,7 @@ class ReceivablePayableReport(object): self.gl_entries = frappe.db.sql(""" select name, posting_date, account, party_type, party, voucher_type, voucher_no, cost_center, - against_voucher_type, against_voucher, account_currency, remarks, {0} + against_voucher_type, against_voucher, account_currency, {0} from `tabGL Entry` where @@ -792,8 +793,6 @@ class ReceivablePayableReport(object): self.add_column(label=_('Supplier Group'), fieldname='supplier_group', fieldtype='Link', options='Supplier Group') - self.add_column(label=_('Remarks'), fieldname='remarks', fieldtype='Text', width=200) - def add_column(self, label, fieldname=None, fieldtype='Currency', options=None, width=120): if not fieldname: fieldname = scrub(label) if fieldtype=='Currency': options='currency' diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py index 2ff5b531c51..cca67608238 100644 --- a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py @@ -93,4 +93,3 @@ def make_credit_note(docname): cost_center = 'Main - _TC2', is_return = 1, return_against = docname) - diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py index 657b3e8f204..4bfb022c4ee 100644 --- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py +++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py @@ -82,6 +82,7 @@ class AccountsReceivableSummary(ReceivablePayableReport): "range3": 0.0, "range4": 0.0, "range5": 0.0, + "total_due": 0.0, "sales_person": [] })) @@ -134,4 +135,7 @@ class AccountsReceivableSummary(ReceivablePayableReport): "{range2}-{range3}".format(range2=cint(self.filters["range2"])+ 1, range3=self.filters["range3"]), "{range3}-{range4}".format(range3=cint(self.filters["range3"])+ 1, range4=self.filters["range4"]), "{range4}-{above}".format(range4=cint(self.filters["range4"])+ 1, above=_("Above"))]): - self.add_column(label=label, fieldname='range' + str(i+1)) \ No newline at end of file + self.add_column(label=label, fieldname='range' + str(i+1)) + + # Add column for total due amount + self.add_column(label="Total Amount Due", fieldname='total_due') diff --git a/erpnext/accounts/report/balance_sheet/balance_sheet.py b/erpnext/accounts/report/balance_sheet/balance_sheet.py index 26bb44f4f7b..7838385dc56 100644 --- a/erpnext/accounts/report/balance_sheet/balance_sheet.py +++ b/erpnext/accounts/report/balance_sheet/balance_sheet.py @@ -209,4 +209,4 @@ def get_chart_data(filters, columns, asset, liability, equity): else: chart["type"] = "line" - return chart \ No newline at end of file + return chart diff --git a/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.js b/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.js index dbee0229737..f0b6c6b20ac 100644 --- a/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.js +++ b/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.js @@ -22,7 +22,7 @@ frappe.query_reports["Bank Clearance Summary"] = { "fieldtype": "Link", "options": "Account", "reqd": 1, - "default": frappe.defaults.get_user_default("Company")? + "default": frappe.defaults.get_user_default("Company")? locals[":Company"][frappe.defaults.get_user_default("Company")]["default_bank_account"]: "", "get_query": function() { return { diff --git a/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py b/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py index 79b0a6f30ec..95f724cc580 100644 --- a/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py +++ b/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py @@ -74,19 +74,19 @@ def get_entries(filters): journal_entries = frappe.db.sql("""SELECT "Journal Entry", jv.name, jv.posting_date, jv.cheque_no, jv.clearance_date, jvd.against_account, jvd.debit - jvd.credit - FROM + FROM `tabJournal Entry Account` jvd, `tabJournal Entry` jv - WHERE + WHERE jvd.parent = jv.name and jv.docstatus=1 and jvd.account = %(account)s {0} order by posting_date DESC, jv.name DESC""".format(conditions), filters, as_list=1) payment_entries = frappe.db.sql("""SELECT - "Payment Entry", name, posting_date, reference_no, clearance_date, party, + "Payment Entry", name, posting_date, reference_no, clearance_date, party, if(paid_from=%(account)s, paid_amount * -1, received_amount) - FROM + FROM `tabPayment Entry` - WHERE + WHERE docstatus=1 and (paid_from = %(account)s or paid_to = %(account)s) {0} order by posting_date DESC, name DESC""".format(conditions), filters, as_list=1) - return sorted(journal_entries + payment_entries, key=lambda k: k[2] or getdate(nowdate())) \ No newline at end of file + return sorted(journal_entries + payment_entries, key=lambda k: k[2] or getdate(nowdate())) diff --git a/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.js b/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.js index 8f028496cd5..9bb6a14c677 100644 --- a/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.js +++ b/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.js @@ -16,7 +16,7 @@ frappe.query_reports["Bank Reconciliation Statement"] = { "label": __("Bank Account"), "fieldtype": "Link", "options": "Account", - "default": frappe.defaults.get_user_default("Company")? + "default": frappe.defaults.get_user_default("Company")? locals[":Company"][frappe.defaults.get_user_default("Company")]["default_bank_account"]: "", "reqd": 1, "get_query": function() { diff --git a/erpnext/accounts/report/billed_items_to_be_received/billed_items_to_be_received.py b/erpnext/accounts/report/billed_items_to_be_received/billed_items_to_be_received.py index 2ce5d50edf4..2dcea22f7e6 100644 --- a/erpnext/accounts/report/billed_items_to_be_received/billed_items_to_be_received.py +++ b/erpnext/accounts/report/billed_items_to_be_received/billed_items_to_be_received.py @@ -104,4 +104,4 @@ def get_columns(): 'fieldtype': 'Currency', 'width': 100 } - ] \ No newline at end of file + ] diff --git a/erpnext/accounts/report/budget_variance_report/budget_variance_report.js b/erpnext/accounts/report/budget_variance_report/budget_variance_report.js index f547ca619bd..718b6e2fcb6 100644 --- a/erpnext/accounts/report/budget_variance_report/budget_variance_report.js +++ b/erpnext/accounts/report/budget_variance_report/budget_variance_report.js @@ -92,4 +92,3 @@ frappe.query_reports["Budget Variance Report"] = { erpnext.dimension_filters.forEach((dimension) => { frappe.query_reports["Budget Variance Report"].filters[4].options.push(dimension["document_type"]); }); - diff --git a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py index 9c9ada871c8..443126e4655 100644 --- a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py +++ b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py @@ -38,8 +38,8 @@ def execute(filters=None): GROUP BY parent''',{'dimension':[dimension]}) if DCC_allocation: filters['budget_against_filter'] = [DCC_allocation[0][0]] - cam_map = get_dimension_account_month_map(filters) - dimension_items = cam_map.get(DCC_allocation[0][0]) + ddc_cam_map = get_dimension_account_month_map(filters) + dimension_items = ddc_cam_map.get(DCC_allocation[0][0]) if dimension_items: data = get_final_data(dimension, dimension_items, filters, period_month_ranges, data, DCC_allocation[0][1]) @@ -48,7 +48,6 @@ def execute(filters=None): return columns, data, None, chart def get_final_data(dimension, dimension_items, filters, period_month_ranges, data, DCC_allocation): - for account, monthwise_data in iteritems(dimension_items): row = [dimension, account] totals = [0, 0, 0] @@ -79,7 +78,7 @@ def get_final_data(dimension, dimension_items, filters, period_month_ranges, dat if filters["period"] != "Yearly" : row += totals data.append(row) - + return data @@ -389,7 +388,7 @@ def get_chart_data(filters, columns, data): budget_values[i] += values[index] actual_values[i] += values[index+1] index += 3 - + return { 'data': { 'labels': labels, @@ -397,6 +396,6 @@ def get_chart_data(filters, columns, data): {'name': 'Budget', 'chartType': 'bar', 'values': budget_values}, {'name': 'Actual Expense', 'chartType': 'bar', 'values': actual_values} ] - } + }, + 'type' : 'bar' } - diff --git a/erpnext/accounts/report/cash_flow/cash_flow.html b/erpnext/accounts/report/cash_flow/cash_flow.html index 40ba20c4ac6..d4ae54d4f38 100644 --- a/erpnext/accounts/report/cash_flow/cash_flow.html +++ b/erpnext/accounts/report/cash_flow/cash_flow.html @@ -1 +1 @@ -{% include "accounts/report/financial_statements.html" %} \ No newline at end of file +{% include "accounts/report/financial_statements.html" %} diff --git a/erpnext/accounts/report/cash_flow/cash_flow.js b/erpnext/accounts/report/cash_flow/cash_flow.js index a984bf46b50..a2c34c6ee26 100644 --- a/erpnext/accounts/report/cash_flow/cash_flow.js +++ b/erpnext/accounts/report/cash_flow/cash_flow.js @@ -21,4 +21,4 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { "default": 1 } ); -}); \ No newline at end of file +}); diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js index 1363b53746a..6a8301a6f91 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js @@ -94,10 +94,10 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { "default": 1 } ], - "formatter": function(value, row, column, data, default_formatter) { + "formatter": function(value, row, column, data, default_formatter) { if (data && column.fieldname=="account") { value = data.account_name || value; - + column.link_onclick = "erpnext.financial_statements.open_general_ledger(" + JSON.stringify(data) + ")"; column.is_tree = true; @@ -126,4 +126,4 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { }); } } -}); \ No newline at end of file +}); diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py index 56a67bb0989..fc4212733a3 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py @@ -210,10 +210,10 @@ def get_data(companies, root_type, balance_must_be, fiscal_year, filters=None, i company_currency = get_company_currency(filters) if filters.filter_based_on == 'Fiscal Year': - start_date = fiscal_year.year_start_date + start_date = fiscal_year.year_start_date if filters.report != 'Balance Sheet' else None end_date = fiscal_year.year_end_date else: - start_date = filters.period_start_date + start_date = filters.period_start_date if filters.report != 'Balance Sheet' else None end_date = filters.period_end_date gl_entries_by_account = {} diff --git a/erpnext/accounts/report/delivered_items_to_be_billed/delivered_items_to_be_billed.py b/erpnext/accounts/report/delivered_items_to_be_billed/delivered_items_to_be_billed.py index 515fd995e66..9953d8fcaf5 100644 --- a/erpnext/accounts/report/delivered_items_to_be_billed/delivered_items_to_be_billed.py +++ b/erpnext/accounts/report/delivered_items_to_be_billed/delivered_items_to_be_billed.py @@ -105,4 +105,4 @@ def get_column(): def get_args(): return {'doctype': 'Delivery Note', 'party': 'customer', - 'date': 'posting_date', 'order': 'name', 'order_by': 'desc'} \ No newline at end of file + 'date': 'posting_date', 'order': 'name', 'order_by': 'desc'} diff --git a/erpnext/accounts/report/general_ledger/general_ledger.js b/erpnext/accounts/report/general_ledger/general_ledger.js index 4a551b80124..095f5eda66a 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.js +++ b/erpnext/accounts/report/general_ledger/general_ledger.js @@ -176,4 +176,3 @@ frappe.query_reports["General Ledger"] = { } erpnext.utils.add_dimensions('General Ledger', 15) - diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index 744ada9e558..3723c8e0d23 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -49,16 +49,17 @@ def validate_filters(filters, account_details): if not filters.get("from_date") and not filters.get("to_date"): frappe.throw(_("{0} and {1} are mandatory").format(frappe.bold(_("From Date")), frappe.bold(_("To Date")))) - for account in filters.account: - if not account_details.get(account): - frappe.throw(_("Account {0} does not exists").format(account)) - if filters.get('account'): filters.account = frappe.parse_json(filters.get('account')) + for account in filters.account: + if not account_details.get(account): + frappe.throw(_("Account {0} does not exists").format(account)) - if (filters.get("account") and filters.get("group_by") == _('Group by Account') - and account_details[filters.account].is_group == 0): - frappe.throw(_("Can not filter based on Account, if grouped by Account")) + if (filters.get("account") and filters.get("group_by") == _('Group by Account')): + filters.account = frappe.parse_json(filters.get('account')) + for account in filters.account: + if account_details[account].is_group == 0: + frappe.throw(_("Can not filter based on Child Account, if grouped by Account")) if (filters.get("voucher_no") and filters.get("group_by") in [_('Group by Voucher')]): @@ -77,13 +78,10 @@ def validate_filters(filters, account_details): def validate_party(filters): party_type, party = filters.get("party_type"), filters.get("party") - if party: - if not party_type: - frappe.throw(_("To filter based on Party, select Party Type first")) - else: - for d in party: - if not frappe.db.exists(party_type, d): - frappe.throw(_("Invalid {0}: {1}").format(party_type, d)) + if party and party_type: + for d in party: + if not frappe.db.exists(party_type, d): + frappe.throw(_("Invalid {0}: {1}").format(party_type, d)) def set_account_currency(filters): if filters.get("account") or (filters.get('party') and len(filters.party) == 1): @@ -91,7 +89,7 @@ def set_account_currency(filters): account_currency = None if filters.get("account"): - if len(filters.get("account")) == 1: + if len(filters.get("account")) == 1: account_currency = get_account_currency(filters.account[0]) else: currency = get_account_currency(filters.account[0]) diff --git a/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.html b/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.html index 40ba20c4ac6..d4ae54d4f38 100644 --- a/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.html +++ b/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.html @@ -1 +1 @@ -{% include "accounts/report/financial_statements.html" %} \ No newline at end of file +{% include "accounts/report/financial_statements.html" %} diff --git a/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py b/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py index 714e48d279b..8e33af7ee8e 100644 --- a/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py +++ b/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py @@ -165,4 +165,4 @@ def get_net_profit(non_gross_income, gross_income, gross_expense, non_gross_expe has_value=True if has_value: - return profit_loss \ No newline at end of file + return profit_loss diff --git a/erpnext/accounts/report/gross_profit/gross_profit.json b/erpnext/accounts/report/gross_profit/gross_profit.json index cd6bac2d77d..5fff3fdba77 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.json +++ b/erpnext/accounts/report/gross_profit/gross_profit.json @@ -1,16 +1,20 @@ { - "add_total_row": 1, + "add_total_row": 0, + "columns": [], "creation": "2013-02-25 17:03:34", + "disable_prepared_report": 0, "disabled": 0, "docstatus": 0, "doctype": "Report", + "filters": [], "idx": 3, "is_standard": "Yes", - "modified": "2020-08-13 11:26:39.112352", + "modified": "2021-08-19 18:57:07.468202", "modified_by": "Administrator", "module": "Accounts", "name": "Gross Profit", "owner": "Administrator", + "prepared_report": 0, "ref_doctype": "Sales Invoice", "report_name": "Gross Profit", "report_type": "Script Report", diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index 84c74543dae..c949d9b74e5 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -41,12 +41,14 @@ def execute(filters=None): columns = get_columns(group_wise_columns, filters) - for src in gross_profit_data.grouped_data: + for idx, src in enumerate(gross_profit_data.grouped_data): row = [] for col in group_wise_columns.get(scrub(filters.group_by)): row.append(src.get(col)) row.append(filters.currency) + if idx == len(gross_profit_data.grouped_data)-1: + row[0] = frappe.bold("Total") data.append(row) return columns, data @@ -154,6 +156,15 @@ class GrossProfitGenerator(object): def get_average_rate_based_on_group_by(self): # sum buying / selling totals for group + self.totals = frappe._dict( + qty=0, + base_amount=0, + buying_amount=0, + gross_profit=0, + gross_profit_percent=0, + base_rate=0, + buying_rate=0 + ) for key in list(self.grouped): if self.filters.get("group_by") != "Invoice": for i, row in enumerate(self.grouped[key]): @@ -165,6 +176,7 @@ class GrossProfitGenerator(object): new_row.base_amount += flt(row.base_amount, self.currency_precision) new_row = self.set_average_rate(new_row) self.grouped_data.append(new_row) + self.add_to_totals(new_row) else: for i, row in enumerate(self.grouped[key]): if row.parent in self.returned_invoices \ @@ -177,15 +189,25 @@ class GrossProfitGenerator(object): if row.qty or row.base_amount: row = self.set_average_rate(row) self.grouped_data.append(row) + self.add_to_totals(row) + self.set_average_gross_profit(self.totals) + self.grouped_data.append(self.totals) def set_average_rate(self, new_row): + self.set_average_gross_profit(new_row) + new_row.buying_rate = flt(new_row.buying_amount / new_row.qty, self.float_precision) if new_row.qty else 0 + new_row.base_rate = flt(new_row.base_amount / new_row.qty, self.float_precision) if new_row.qty else 0 + return new_row + + def set_average_gross_profit(self, new_row): new_row.gross_profit = flt(new_row.base_amount - new_row.buying_amount, self.currency_precision) new_row.gross_profit_percent = flt(((new_row.gross_profit / new_row.base_amount) * 100.0), self.currency_precision) \ if new_row.base_amount else 0 - new_row.buying_rate = flt(new_row.buying_amount / new_row.qty, self.float_precision) if new_row.qty else 0 - new_row.base_rate = flt(new_row.base_amount / new_row.qty, self.float_precision) if new_row.qty else 0 - return new_row + def add_to_totals(self, new_row): + for key in self.totals: + if new_row.get(key): + self.totals[key] += new_row[key] def get_returned_invoice_items(self): returned_invoices = frappe.db.sql(""" @@ -241,6 +263,7 @@ class GrossProfitGenerator(object): sle.voucher_detail_no == row.item_row: previous_stock_value = len(my_sle) > i+1 and \ flt(my_sle[i+1].stock_value) or 0.0 + if previous_stock_value: return (previous_stock_value - flt(sle.stock_value)) * flt(row.qty) / abs(flt(sle.qty)) else: @@ -335,7 +358,7 @@ class GrossProfitGenerator(object): res = frappe.db.sql("""select item_code, voucher_type, voucher_no, voucher_detail_no, stock_value, warehouse, actual_qty as qty from `tabStock Ledger Entry` - where company=%(company)s + where company=%(company)s and is_cancelled = 0 order by item_code desc, warehouse desc, posting_date desc, posting_time desc, creation desc""", self.filters, as_dict=True) diff --git a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py index 2e794da8425..c9c22c246ed 100644 --- a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py +++ b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py @@ -76,7 +76,7 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum 'company': d.company, 'sales_order': d.sales_order, 'delivery_note': d.delivery_note, - 'income_account': d.unrealized_profit_loss_account or d.income_account, + 'income_account': d.unrealized_profit_loss_account if d.is_internal_customer == 1 else d.income_account, 'cost_center': d.cost_center, 'stock_qty': d.stock_qty, 'stock_uom': d.stock_uom @@ -380,6 +380,7 @@ def get_items(filters, additional_query_columns): `tabSales Invoice Item`.name, `tabSales Invoice Item`.parent, `tabSales Invoice`.posting_date, `tabSales Invoice`.debit_to, `tabSales Invoice`.unrealized_profit_loss_account, + `tabSales Invoice`.is_internal_customer, `tabSales Invoice`.project, `tabSales Invoice`.customer, `tabSales Invoice`.remarks, `tabSales Invoice`.territory, `tabSales Invoice`.company, `tabSales Invoice`.base_net_total, `tabSales Invoice Item`.item_code, `tabSales Invoice Item`.description, @@ -625,7 +626,3 @@ def add_sub_total_row(item, total_row_map, group_by_value, tax_columns): for tax in tax_columns: total_row.setdefault(frappe.scrub(tax + ' Amount'), 0.0) total_row[frappe.scrub(tax + ' Amount')] += flt(item[frappe.scrub(tax + ' Amount')]) - - - - diff --git a/erpnext/accounts/report/non_billed_report.py b/erpnext/accounts/report/non_billed_report.py index 2e18ce11ddc..51735056896 100644 --- a/erpnext/accounts/report/non_billed_report.py +++ b/erpnext/accounts/report/non_billed_report.py @@ -44,4 +44,4 @@ def get_ordered_to_be_billed_data(args): def get_project_field(doctype, party): if party == "supplier": doctype = doctype + ' Item' - return "`tab%s`.project"%(doctype) \ No newline at end of file + return "`tab%s`.project"%(doctype) diff --git a/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.py b/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.py index 7195c7e0b8b..556f5ad4f79 100644 --- a/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.py +++ b/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.py @@ -40,7 +40,7 @@ def execute(filters=None): row = [ d.voucher_type, d.voucher_no, d.party_type, d.party, d.posting_date, d.against_voucher, - invoice.posting_date, invoice.due_date, d.debit, d.credit, d.remarks, + invoice.posting_date, invoice.due_date, d.debit, d.credit, d.remarks, d.age, d.range1, d.range2, d.range3, d.range4 ] diff --git a/erpnext/accounts/report/pos_register/pos_register.py b/erpnext/accounts/report/pos_register/pos_register.py index 6a42bb4fb65..b7e112c0c9a 100644 --- a/erpnext/accounts/report/pos_register/pos_register.py +++ b/erpnext/accounts/report/pos_register/pos_register.py @@ -10,7 +10,7 @@ from erpnext.accounts.report.sales_register.sales_register import get_mode_of_pa def execute(filters=None): if not filters: return [], [] - + validate_filters(filters) columns = get_columns(filters) @@ -29,7 +29,7 @@ def execute(filters=None): invoice_map, grouped_data = {}, [] for d in pos_entries: invoice_map.setdefault(d[group_by_field], []).append(d) - + for key in invoice_map: invoices = invoice_map[key] grouped_data += invoices @@ -56,7 +56,7 @@ def get_pos_entries(filters, group_by_field): return frappe.db.sql( """ - SELECT + SELECT p.posting_date, p.name as pos_invoice, p.pos_profile, p.owner, p.base_grand_total as grand_total, p.base_paid_amount as paid_amount, p.customer, p.is_return {select_mop_field} @@ -96,22 +96,22 @@ def add_subtotal_row(data, group_invoices, group_by_field, group_by_value): def validate_filters(filters): if not filters.get("company"): frappe.throw(_("{0} is mandatory").format(_("Company"))) - + if not filters.get("from_date") and not filters.get("to_date"): frappe.throw(_("{0} and {1} are mandatory").format(frappe.bold(_("From Date")), frappe.bold(_("To Date")))) - + if filters.from_date > filters.to_date: frappe.throw(_("From Date must be before To Date")) if (filters.get("pos_profile") and filters.get("group_by") == _('POS Profile')): frappe.throw(_("Can not filter based on POS Profile, if grouped by POS Profile")) - + if (filters.get("customer") and filters.get("group_by") == _('Customer')): frappe.throw(_("Can not filter based on Customer, if grouped by Customer")) - + if (filters.get("owner") and filters.get("group_by") == _('Cashier')): frappe.throw(_("Can not filter based on Cashier, if grouped by Cashier")) - + if (filters.get("mode_of_payment") and filters.get("group_by") == _('Payment Method')): frappe.throw(_("Can not filter based on Payment Method, if grouped by Payment Method")) @@ -120,23 +120,23 @@ def get_conditions(filters): if filters.get("pos_profile"): conditions += " AND pos_profile = %(pos_profile)s" - + if filters.get("owner"): conditions += " AND owner = %(owner)s" - + if filters.get("customer"): conditions += " AND customer = %(customer)s" - + if filters.get("is_return"): conditions += " AND is_return = %(is_return)s" - + if filters.get("mode_of_payment"): conditions += """ AND EXISTS( SELECT name FROM `tabSales Invoice Payment` sip WHERE parent=p.name AND ifnull(sip.mode_of_payment, '') = %(mode_of_payment)s )""" - + return conditions def get_group_by_field(group_by): @@ -150,7 +150,7 @@ def get_group_by_field(group_by): group_by_field = "customer" elif group_by == "Payment Method": group_by_field = "mode_of_payment" - + return group_by_field def get_columns(filters): @@ -217,4 +217,4 @@ def get_columns(filters): }, ] - return columns \ No newline at end of file + return columns diff --git a/erpnext/accounts/report/profitability_analysis/profitability_analysis.html b/erpnext/accounts/report/profitability_analysis/profitability_analysis.html index 40ba20c4ac6..d4ae54d4f38 100644 --- a/erpnext/accounts/report/profitability_analysis/profitability_analysis.html +++ b/erpnext/accounts/report/profitability_analysis/profitability_analysis.html @@ -1 +1 @@ -{% include "accounts/report/financial_statements.html" %} \ No newline at end of file +{% include "accounts/report/financial_statements.html" %} diff --git a/erpnext/accounts/report/purchase_invoice_trends/purchase_invoice_trends.js b/erpnext/accounts/report/purchase_invoice_trends/purchase_invoice_trends.js index a95cfacaeef..feab96f2652 100644 --- a/erpnext/accounts/report/purchase_invoice_trends/purchase_invoice_trends.js +++ b/erpnext/accounts/report/purchase_invoice_trends/purchase_invoice_trends.js @@ -5,4 +5,4 @@ frappe.require("assets/erpnext/js/purchase_trends_filters.js", function() { frappe.query_reports["Purchase Invoice Trends"] = { filters: erpnext.get_purchase_trends_filters() } -}); \ No newline at end of file +}); diff --git a/erpnext/accounts/report/purchase_invoice_trends/purchase_invoice_trends.py b/erpnext/accounts/report/purchase_invoice_trends/purchase_invoice_trends.py index ad3783f0de1..ba236b9969d 100644 --- a/erpnext/accounts/report/purchase_invoice_trends/purchase_invoice_trends.py +++ b/erpnext/accounts/report/purchase_invoice_trends/purchase_invoice_trends.py @@ -11,4 +11,4 @@ def execute(filters=None): conditions = get_columns(filters, "Purchase Invoice") data = get_data(filters, conditions) - return conditions["columns"], data \ No newline at end of file + return conditions["columns"], data diff --git a/erpnext/accounts/report/purchase_register/purchase_register.js b/erpnext/accounts/report/purchase_register/purchase_register.js index f34ea571639..aaf76c42997 100644 --- a/erpnext/accounts/report/purchase_register/purchase_register.js +++ b/erpnext/accounts/report/purchase_register/purchase_register.js @@ -56,4 +56,4 @@ frappe.query_reports["Purchase Register"] = { ] } -erpnext.utils.add_dimensions('Purchase Register', 7); \ No newline at end of file +erpnext.utils.add_dimensions('Purchase Register', 7); diff --git a/erpnext/accounts/report/received_items_to_be_billed/received_items_to_be_billed.py b/erpnext/accounts/report/received_items_to_be_billed/received_items_to_be_billed.py index e9e9c9c4e69..a5eced5f80b 100644 --- a/erpnext/accounts/report/received_items_to_be_billed/received_items_to_be_billed.py +++ b/erpnext/accounts/report/received_items_to_be_billed/received_items_to_be_billed.py @@ -105,4 +105,4 @@ def get_column(): def get_args(): return {'doctype': 'Purchase Receipt', 'party': 'supplier', - 'date': 'posting_date', 'order': 'name', 'order_by': 'desc'} \ No newline at end of file + 'date': 'posting_date', 'order': 'name', 'order_by': 'desc'} diff --git a/erpnext/accounts/report/sales_invoice_trends/sales_invoice_trends.js b/erpnext/accounts/report/sales_invoice_trends/sales_invoice_trends.js index 2d320f52cfa..e3d43a7de1e 100644 --- a/erpnext/accounts/report/sales_invoice_trends/sales_invoice_trends.js +++ b/erpnext/accounts/report/sales_invoice_trends/sales_invoice_trends.js @@ -5,4 +5,4 @@ frappe.require("assets/erpnext/js/sales_trends_filters.js", function() { frappe.query_reports["Sales Invoice Trends"] = { filters: erpnext.get_sales_trends_filters() } -}); \ No newline at end of file +}); diff --git a/erpnext/accounts/report/sales_payment_summary/sales_payment_summary.js b/erpnext/accounts/report/sales_payment_summary/sales_payment_summary.js index 068926b063d..44e20e83c50 100644 --- a/erpnext/accounts/report/sales_payment_summary/sales_payment_summary.js +++ b/erpnext/accounts/report/sales_payment_summary/sales_payment_summary.js @@ -42,4 +42,4 @@ frappe.query_reports["Sales Payment Summary"] = { "fieldtype": "Check" }, ] -}; \ No newline at end of file +}; diff --git a/erpnext/accounts/report/sales_payment_summary/test_sales_payment_summary.py b/erpnext/accounts/report/sales_payment_summary/test_sales_payment_summary.py index a51c4276301..e4a3d3527fd 100644 --- a/erpnext/accounts/report/sales_payment_summary/test_sales_payment_summary.py +++ b/erpnext/accounts/report/sales_payment_summary/test_sales_payment_summary.py @@ -162,4 +162,4 @@ def create_records(): "price_list": "Standard Selling", "item_code": item.item_code, "price_list_rate": 10000 - }).insert() \ No newline at end of file + }).insert() diff --git a/erpnext/accounts/report/sales_register/sales_register.js b/erpnext/accounts/report/sales_register/sales_register.js index 85bbceab827..2c9b01bbaa3 100644 --- a/erpnext/accounts/report/sales_register/sales_register.js +++ b/erpnext/accounts/report/sales_register/sales_register.js @@ -69,4 +69,3 @@ frappe.query_reports["Sales Register"] = { } erpnext.utils.add_dimensions('Sales Register', 7); - diff --git a/erpnext/accounts/report/sales_register/sales_register.py b/erpnext/accounts/report/sales_register/sales_register.py index 909959323f7..f38bd78c0d2 100644 --- a/erpnext/accounts/report/sales_register/sales_register.py +++ b/erpnext/accounts/report/sales_register/sales_register.py @@ -84,7 +84,7 @@ def _execute(filters, additional_table_columns=None, additional_query_columns=No # Add amount in unrealized account for account in unrealized_profit_loss_accounts: row.update({ - frappe.scrub(account): flt(internal_invoice_map.get((inv.name, account))) + frappe.scrub(account+"_unrealized"): flt(internal_invoice_map.get((inv.name, account))) }) # net total @@ -258,6 +258,7 @@ def get_columns(invoice_list, additional_table_columns): unrealized_profit_loss_accounts = frappe.db.sql_list("""SELECT distinct unrealized_profit_loss_account from `tabSales Invoice` where docstatus = 1 and name in (%s) + and is_internal_customer = 1 and ifnull(unrealized_profit_loss_account, '') != '' order by unrealized_profit_loss_account""" % ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list)) @@ -284,7 +285,7 @@ def get_columns(invoice_list, additional_table_columns): for account in unrealized_profit_loss_accounts: unrealized_profit_loss_account_columns.append({ "label": account, - "fieldname": frappe.scrub(account), + "fieldname": frappe.scrub(account+"_unrealized"), "fieldtype": "Currency", "options": "currency", "width": 120 diff --git a/erpnext/accounts/report/supplier_ledger_summary/supplier_ledger_summary.py b/erpnext/accounts/report/supplier_ledger_summary/supplier_ledger_summary.py index d2c23ee4e78..fbd25b13bb5 100644 --- a/erpnext/accounts/report/supplier_ledger_summary/supplier_ledger_summary.py +++ b/erpnext/accounts/report/supplier_ledger_summary/supplier_ledger_summary.py @@ -10,4 +10,4 @@ def execute(filters=None): "party_type": "Supplier", "naming_by": ["Buying Settings", "supp_master_name"], } - return PartyLedgerSummaryReport(filters).run(args) \ No newline at end of file + return PartyLedgerSummaryReport(filters).run(args) diff --git a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py index e15715dccd8..6b9df41f54e 100644 --- a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py +++ b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py @@ -75,7 +75,8 @@ def get_invoice_and_tds_amount(supplier, account, company, from_date, to_date, f select voucher_no, credit from `tabGL Entry` where party in (%s) and credit > 0 - and company=%s and posting_date between %s and %s + and company=%s and is_cancelled = 0 + and posting_date between %s and %s """, (supplier, company, from_date, to_date), as_dict=1) supplier_credit_amount = flt(sum(d.credit for d in entries)) diff --git a/erpnext/accounts/report/trial_balance/trial_balance.js b/erpnext/accounts/report/trial_balance/trial_balance.js index 8645d55d0fe..078b06519f1 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.js +++ b/erpnext/accounts/report/trial_balance/trial_balance.js @@ -110,6 +110,3 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { erpnext.utils.add_dimensions('Trial Balance', 6); }); - - - diff --git a/erpnext/accounts/report/trial_balance/trial_balance.py b/erpnext/accounts/report/trial_balance/trial_balance.py index 33360e2b01e..1fc0faab3a7 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.py +++ b/erpnext/accounts/report/trial_balance/trial_balance.py @@ -321,4 +321,4 @@ def prepare_opening_closing(row): row[reverse_col] = abs(row[valid_col]) row[valid_col] = 0.0 else: - row[reverse_col] = 0.0 \ No newline at end of file + row[reverse_col] = 0.0 diff --git a/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.py b/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.py index 78c7e439d38..f034e7450ee 100644 --- a/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.py +++ b/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.py @@ -242,4 +242,4 @@ def is_party_name_visible(filters): else: show_party_name = True - return show_party_name \ No newline at end of file + return show_party_name diff --git a/erpnext/accounts/report/unpaid_expense_claim/unpaid_expense_claim.py b/erpnext/accounts/report/unpaid_expense_claim/unpaid_expense_claim.py index eee620b7cc8..1250d676a06 100644 --- a/erpnext/accounts/report/unpaid_expense_claim/unpaid_expense_claim.py +++ b/erpnext/accounts/report/unpaid_expense_claim/unpaid_expense_claim.py @@ -20,11 +20,11 @@ def get_unclaimed_expese_claims(filters): if filters.get("employee"): cond = "ec.employee = %(employee)s" - return frappe.db.sql(""" + return frappe.db.sql(""" select ec.employee, ec.employee_name, ec.name, ec.total_sanctioned_amount, ec.total_amount_reimbursed, sum(gle.credit_in_account_currency - gle.debit_in_account_currency) as outstanding_amt - from + from `tabExpense Claim` ec, `tabGL Entry` gle where gle.against_voucher_type = "Expense Claim" and gle.against_voucher = ec.name diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index ed6e28da1e6..9120602adf2 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -19,6 +19,7 @@ from erpnext.stock import get_warehouse_account_map class StockValueAndAccountBalanceOutOfSync(frappe.ValidationError): pass class FiscalYearError(frappe.ValidationError): pass +class PaymentEntryUnlinkError(frappe.ValidationError): pass @frappe.whitelist() def get_fiscal_year(date=None, fiscal_year=None, label="Date", verbose=1, company=None, as_dict=False): @@ -350,6 +351,7 @@ def reconcile_against_document(args): # cancel advance entry doc = frappe.get_doc(d.voucher_type, d.voucher_no) + frappe.flags.ignore_party_validation = True doc.make_gl_entries(cancel=1, adv_adj=1) # update ref in advance entry @@ -361,6 +363,7 @@ def reconcile_against_document(args): # re-submit advance entry doc = frappe.get_doc(d.voucher_type, d.voucher_no) doc.make_gl_entries(cancel = 0, adv_adj =1) + frappe.flags.ignore_party_validation = False if d.voucher_type in ('Payment Entry', 'Journal Entry'): doc.update_expense_claim() @@ -472,7 +475,8 @@ def update_reference_in_payment_entry(d, payment_entry, do_not_save=False): "total_amount": d.grand_total, "outstanding_amount": d.outstanding_amount, "allocated_amount": d.allocated_amount, - "exchange_rate": d.exchange_rate + "exchange_rate": d.exchange_rate if not d.exchange_gain_loss else payment_entry.get_exchange_rate(), + "exchange_gain_loss": d.exchange_gain_loss # only populated from invoice in case of advance allocation } if d.voucher_detail_no: @@ -498,12 +502,15 @@ def update_reference_in_payment_entry(d, payment_entry, do_not_save=False): payment_entry.set_amounts() if d.difference_amount and d.difference_account: - payment_entry.set_gain_or_loss(account_details={ + account_details = { 'account': d.difference_account, 'cost_center': payment_entry.cost_center or frappe.get_cached_value('Company', - payment_entry.company, "cost_center"), - 'amount': d.difference_amount - }) + payment_entry.company, "cost_center") + } + if d.difference_amount: + account_details['amount'] = d.difference_amount + + payment_entry.set_gain_or_loss(account_details=account_details) if not do_not_save: payment_entry.save(ignore_permissions=True) @@ -549,10 +556,16 @@ def remove_ref_doc_link_from_pe(ref_type, ref_no): and docstatus < 2""", (now(), frappe.session.user, ref_type, ref_no)) for pe in linked_pe: - pe_doc = frappe.get_doc("Payment Entry", pe) - pe_doc.set_total_allocated_amount() - pe_doc.set_unallocated_amount() - pe_doc.clear_unallocated_reference_document_rows() + try: + pe_doc = frappe.get_doc("Payment Entry", pe) + pe_doc.set_amounts() + pe_doc.clear_unallocated_reference_document_rows() + pe_doc.validate_payment_type_with_outstanding() + except Exception as e: + msg = _("There were issues unlinking payment entry {0}.").format(pe_doc.name) + msg += '
' + msg += _("Please cancel payment entry manually first") + frappe.throw(msg, exc=PaymentEntryUnlinkError, title=_("Payment Unlink Error")) frappe.db.sql("""update `tabPayment Entry` set total_allocated_amount=%s, base_total_allocated_amount=%s, unallocated_amount=%s, modified=%s, modified_by=%s @@ -916,7 +929,6 @@ def repost_gle_for_stock_vouchers(stock_vouchers, posting_date, company=None, wa _delete_gl_entries(voucher_type, voucher_no) def get_future_stock_vouchers(posting_date, posting_time, for_warehouses=None, for_items=None, company=None): - future_stock_vouchers = [] values = [] condition = "" @@ -932,37 +944,53 @@ def get_future_stock_vouchers(posting_date, posting_time, for_warehouses=None, f condition += " and company = %s" values.append(company) - for d in frappe.db.sql("""select distinct sle.voucher_type, sle.voucher_no + future_stock_vouchers = frappe.db.sql("""select distinct sle.voucher_type, sle.voucher_no from `tabStock Ledger Entry` sle where timestamp(sle.posting_date, sle.posting_time) >= timestamp(%s, %s) and is_cancelled = 0 {condition} order by timestamp(sle.posting_date, sle.posting_time) asc, creation asc for update""".format(condition=condition), - tuple([posting_date, posting_time] + values), as_dict=True): - future_stock_vouchers.append([d.voucher_type, d.voucher_no]) + tuple([posting_date, posting_time] + values), as_dict=True) - return future_stock_vouchers + return [(d.voucher_type, d.voucher_no) for d in future_stock_vouchers] def get_voucherwise_gl_entries(future_stock_vouchers, posting_date): + """ Get voucherwise list of GL entries. + + Only fetches GLE fields required for comparing with new GLE. + Check compare_existing_and_expected_gle function below. + """ gl_entries = {} - if future_stock_vouchers: - for d in frappe.db.sql("""select * from `tabGL Entry` - where posting_date >= %s and voucher_no in (%s)""" % - ('%s', ', '.join(['%s']*len(future_stock_vouchers))), - tuple([posting_date] + [d[1] for d in future_stock_vouchers]), as_dict=1): - gl_entries.setdefault((d.voucher_type, d.voucher_no), []).append(d) + if not future_stock_vouchers: + return gl_entries + + voucher_nos = [d[1] for d in future_stock_vouchers] + + gles = frappe.db.sql(""" + select name, account, credit, debit, cost_center, project + from `tabGL Entry` + where + posting_date >= %s and voucher_no in (%s)""" % + ('%s', ', '.join(['%s'] * len(voucher_nos))), + tuple([posting_date] + voucher_nos), as_dict=1) + + for d in gles: + gl_entries.setdefault((d.voucher_type, d.voucher_no), []).append(d) return gl_entries def compare_existing_and_expected_gle(existing_gle, expected_gle, precision): + if len(existing_gle) != len(expected_gle): + return False + matched = True for entry in expected_gle: account_existed = False for e in existing_gle: if entry.account == e.account: account_existed = True - if (entry.account == e.account and entry.against_account == e.against_account + if (entry.account == e.account and (not entry.cost_center or not e.cost_center or entry.cost_center == e.cost_center) and ( flt(entry.debit, precision) != flt(e.debit, precision) or flt(entry.credit, precision) != flt(e.credit, precision))): @@ -1058,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 10a4001502f..7e3ecaf3ab6 100644 --- a/erpnext/accounts/workspace/accounting/accounting.json +++ b/erpnext/accounts/workspace/accounting/accounting.json @@ -588,7 +588,7 @@ { "hidden": 0, "is_query_report": 0, - "label": "Bank Statement", + "label": "Banking and Payments", "onboard": 0, "type": "Card Break" }, @@ -642,6 +642,24 @@ "onboard": 0, "type": "Link" }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Payment Entry", + "link_to": "Payment Entry", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Payment Reconciliation", + "link_to": "Payment Reconciliation", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, { "hidden": 0, "is_query_report": 0, @@ -1064,7 +1082,7 @@ "type": "Link" } ], - "modified": "2021-06-10 03:17:31.427945", + "modified": "2021-08-23 16:06:34.167267", "modified_by": "Administrator", "module": "Accounts", "name": "Accounting", 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 f70dcd2f322..00000000000 --- 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 a012c4b1ade..00000000000 --- 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/crop/crop.js b/erpnext/agriculture/doctype/crop/crop.js index afd84fd9f66..550824636b6 100644 --- a/erpnext/agriculture/doctype/crop/crop.js +++ b/erpnext/agriculture/doctype/crop/crop.js @@ -52,4 +52,4 @@ erpnext.crop.update_item_qty_amount = function(frm, cdt, cdn) { } }); }); -}; \ No newline at end of file +}; diff --git a/erpnext/agriculture/doctype/crop/crop_dashboard.py b/erpnext/agriculture/doctype/crop/crop_dashboard.py index 9a8f26fe90c..8f37735c812 100644 --- a/erpnext/agriculture/doctype/crop/crop_dashboard.py +++ b/erpnext/agriculture/doctype/crop/crop_dashboard.py @@ -9,4 +9,4 @@ def get_data(): 'items': ['Crop Cycle'] } ] - } \ No newline at end of file + } diff --git a/erpnext/agriculture/doctype/crop/test_crop.js b/erpnext/agriculture/doctype/crop/test_crop.js index 138acbf85a3..40555634a2c 100644 --- a/erpnext/agriculture/doctype/crop/test_crop.js +++ b/erpnext/agriculture/doctype/crop/test_crop.js @@ -105,7 +105,7 @@ QUnit.test("test: Crop", function (assert) { ] ]} ]), - // agriculture task list + // agriculture task list () => { assert.equal(cur_frm.doc.name, 'Basil from seed'); assert.equal(cur_frm.doc.period, 15); diff --git a/erpnext/agriculture/doctype/crop/test_crop.py b/erpnext/agriculture/doctype/crop/test_crop.py index c2e49174047..b3079837c35 100644 --- a/erpnext/agriculture/doctype/crop/test_crop.py +++ b/erpnext/agriculture/doctype/crop/test_crop.py @@ -11,4 +11,4 @@ test_dependencies = ["Fertilizer"] class TestCrop(unittest.TestCase): def test_crop_period(self): basil = frappe.get_doc('Crop', 'Basil from seed') - self.assertEqual(basil.period, 15) \ No newline at end of file + self.assertEqual(basil.period, 15) diff --git a/erpnext/agriculture/doctype/crop_cycle/test_crop_cycle.js b/erpnext/agriculture/doctype/crop_cycle/test_crop_cycle.js index 464a3680baa..87184daedc9 100644 --- a/erpnext/agriculture/doctype/crop_cycle/test_crop_cycle.js +++ b/erpnext/agriculture/doctype/crop_cycle/test_crop_cycle.js @@ -19,7 +19,7 @@ QUnit.test("test: Crop Cycle", function (assert) { {disease: 'Aphids'} ] ]}, - {linked_land_unit: [ + {linked_land_unit: [ [ {land_unit: 'Basil Farm'} ] diff --git a/erpnext/agriculture/doctype/disease/disease.py b/erpnext/agriculture/doctype/disease/disease.py index c7707a54652..affa57046e5 100644 --- a/erpnext/agriculture/doctype/disease/disease.py +++ b/erpnext/agriculture/doctype/disease/disease.py @@ -17,4 +17,4 @@ class Disease(Document): frappe.throw(_("Start day is greater than end day in task '{0}'").format(task.task_name)) # to calculate the period of the Crop Cycle if task.end_day > max_period: max_period = task.end_day - self.treatment_period = max_period \ No newline at end of file + self.treatment_period = max_period diff --git a/erpnext/agriculture/doctype/disease/test_disease.js b/erpnext/agriculture/doctype/disease/test_disease.js index 57d62c16c25..33f60c4e152 100644 --- a/erpnext/agriculture/doctype/disease/test_disease.js +++ b/erpnext/agriculture/doctype/disease/test_disease.js @@ -36,4 +36,3 @@ QUnit.test("test: Disease", function (assert) { ]); }); - diff --git a/erpnext/agriculture/doctype/disease/test_disease.py b/erpnext/agriculture/doctype/disease/test_disease.py index 54788a2c817..80861770b0d 100644 --- a/erpnext/agriculture/doctype/disease/test_disease.py +++ b/erpnext/agriculture/doctype/disease/test_disease.py @@ -9,4 +9,4 @@ import unittest class TestDisease(unittest.TestCase): def test_treatment_period(self): disease = frappe.get_doc('Disease', 'Aphids') - self.assertEqual(disease.treatment_period, 3) \ No newline at end of file + self.assertEqual(disease.treatment_period, 3) diff --git a/erpnext/agriculture/doctype/fertilizer/fertilizer.py b/erpnext/agriculture/doctype/fertilizer/fertilizer.py index 9cb492aff1e..c475f002981 100644 --- a/erpnext/agriculture/doctype/fertilizer/fertilizer.py +++ b/erpnext/agriculture/doctype/fertilizer/fertilizer.py @@ -11,4 +11,4 @@ class Fertilizer(Document): def load_contents(self): docs = frappe.get_all("Agriculture Analysis Criteria", filters={'linked_doctype':'Fertilizer'}) for doc in docs: - self.append('fertilizer_contents', {'title': str(doc.name)}) \ No newline at end of file + self.append('fertilizer_contents', {'title': str(doc.name)}) diff --git a/erpnext/agriculture/doctype/fertilizer/test_fertilizer.py b/erpnext/agriculture/doctype/fertilizer/test_fertilizer.py index 3a25b3f0a7a..4c71d33fe80 100644 --- a/erpnext/agriculture/doctype/fertilizer/test_fertilizer.py +++ b/erpnext/agriculture/doctype/fertilizer/test_fertilizer.py @@ -8,4 +8,4 @@ import unittest class TestFertilizer(unittest.TestCase): def test_fertilizer_creation(self): - self.assertEqual(frappe.db.exists('Fertilizer', 'Urea'), 'Urea') \ No newline at end of file + self.assertEqual(frappe.db.exists('Fertilizer', 'Urea'), 'Urea') diff --git a/erpnext/agriculture/doctype/plant_analysis/plant_analysis.py b/erpnext/agriculture/doctype/plant_analysis/plant_analysis.py index 2806cc6523e..b65f93de0a0 100644 --- a/erpnext/agriculture/doctype/plant_analysis/plant_analysis.py +++ b/erpnext/agriculture/doctype/plant_analysis/plant_analysis.py @@ -12,4 +12,4 @@ class PlantAnalysis(Document): def load_contents(self): docs = frappe.get_all("Agriculture Analysis Criteria", filters={'linked_doctype':'Plant Analysis'}) for doc in docs: - self.append('plant_analysis_criteria', {'title': str(doc.name)}) \ No newline at end of file + self.append('plant_analysis_criteria', {'title': str(doc.name)}) 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 786c0471a47..00000000000 --- 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/soil_analysis.py b/erpnext/agriculture/doctype/soil_analysis/soil_analysis.py index 37835f8c7b1..234d0d4b011 100644 --- a/erpnext/agriculture/doctype/soil_analysis/soil_analysis.py +++ b/erpnext/agriculture/doctype/soil_analysis/soil_analysis.py @@ -11,4 +11,4 @@ class SoilAnalysis(Document): def load_contents(self): docs = frappe.get_all("Agriculture Analysis Criteria", filters={'linked_doctype':'Soil Analysis'}) for doc in docs: - self.append('soil_analysis_criteria', {'title': str(doc.name)}) \ No newline at end of file + self.append('soil_analysis_criteria', {'title': str(doc.name)}) 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 29128eba27d..00000000000 --- 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/soil_texture/test_soil_texture.py b/erpnext/agriculture/doctype/soil_texture/test_soil_texture.py index 937c06ccadf..16d105c9c58 100644 --- a/erpnext/agriculture/doctype/soil_texture/test_soil_texture.py +++ b/erpnext/agriculture/doctype/soil_texture/test_soil_texture.py @@ -11,4 +11,4 @@ class TestSoilTexture(unittest.TestCase): soil_tex = frappe.get_all('Soil Texture', fields=['name'], filters={'collection_datetime': '2017-11-08'}) doc = frappe.get_doc('Soil Texture', soil_tex[0].name) self.assertEqual(doc.silt_composition, 50) - self.assertEqual(doc.soil_type, 'Silt Loam') \ No newline at end of file + self.assertEqual(doc.soil_type, 'Silt Loam') diff --git a/erpnext/agriculture/doctype/water_analysis/water_analysis.py b/erpnext/agriculture/doctype/water_analysis/water_analysis.py index d9f007cea13..cb2691d4555 100644 --- a/erpnext/agriculture/doctype/water_analysis/water_analysis.py +++ b/erpnext/agriculture/doctype/water_analysis/water_analysis.py @@ -24,4 +24,4 @@ class WaterAnalysis(Document): if self.collection_datetime > self.laboratory_testing_datetime: frappe.throw(_('Lab testing datetime cannot be before collection datetime')) if self.laboratory_testing_datetime > self.result_datetime: - frappe.throw(_('Lab result datetime cannot be before testing datetime')) \ No newline at end of file + frappe.throw(_('Lab result datetime cannot be before testing datetime')) diff --git a/erpnext/agriculture/doctype/weather/test_weather.js b/erpnext/agriculture/doctype/weather/test_weather.js deleted file mode 100644 index b5009a4ccd3..00000000000 --- 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/agriculture/setup.py b/erpnext/agriculture/setup.py index ab91343d5d1..75f07be5de2 100644 --- a/erpnext/agriculture/setup.py +++ b/erpnext/agriculture/setup.py @@ -426,5 +426,5 @@ def create_agriculture_data(): title='Degree Days', standard=1, linked_doctype='Weather') - ] + ] insert_record(records) diff --git a/erpnext/assets/dashboard_fixtures.py b/erpnext/assets/dashboard_fixtures.py index 7f3c1de406a..2c701796072 100644 --- a/erpnext/assets/dashboard_fixtures.py +++ b/erpnext/assets/dashboard_fixtures.py @@ -176,4 +176,4 @@ def get_number_cards(fiscal_year, year_start_date, year_end_date): "filters_json": "[]", "doctype": "Number Card" } - ] \ No newline at end of file + ] diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index 6f1bb28f370..da5778ea3d5 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -82,24 +82,46 @@ frappe.ui.form.on('Asset', { if (in_list(["Submitted", "Partially Depreciated", "Fully Depreciated"], frm.doc.status)) { frm.add_custom_button("Transfer Asset", function() { erpnext.asset.transfer_asset(frm); - }); + }, __("Manage")); frm.add_custom_button("Scrap Asset", function() { erpnext.asset.scrap_asset(frm); - }); + }, __("Manage")); frm.add_custom_button("Sell Asset", function() { frm.trigger("make_sales_invoice"); - }); + }, __("Manage")); } else if (frm.doc.status=='Scrapped') { frm.add_custom_button("Restore Asset", function() { erpnext.asset.restore_asset(frm); - }); + }, __("Manage")); + } + + if (frm.doc.maintenance_required && !frm.doc.maintenance_schedule) { + frm.add_custom_button(__("Maintain Asset"), function() { + frm.trigger("create_asset_maintenance"); + }, __("Manage")); + } + + frm.add_custom_button(__("Repair Asset"), function() { + frm.trigger("create_asset_repair"); + }, __("Manage")); + + if (frm.doc.status != 'Fully Depreciated') { + frm.add_custom_button(__("Adjust Asset Value"), function() { + frm.trigger("create_asset_adjustment"); + }, __("Manage")); + } + + if (!frm.doc.calculate_depreciation) { + frm.add_custom_button(__("Create Depreciation Entry"), function() { + frm.trigger("make_journal_entry"); + }, __("Manage")); } if (frm.doc.purchase_receipt || !frm.doc.is_existing_asset) { - frm.add_custom_button("General Ledger", function() { + frm.add_custom_button("View General Ledger", function() { frappe.route_options = { "voucher_no": frm.doc.name, "from_date": frm.doc.available_for_use_date, @@ -107,27 +129,9 @@ frappe.ui.form.on('Asset', { "company": frm.doc.company }; frappe.set_route("query-report", "General Ledger"); - }); + }, __("Manage")); } - if (frm.doc.maintenance_required && !frm.doc.maintenance_schedule) { - frm.add_custom_button(__("Asset Maintenance"), function() { - frm.trigger("create_asset_maintenance"); - }, __('Create')); - } - if (frm.doc.status != 'Fully Depreciated') { - frm.add_custom_button(__("Asset Value Adjustment"), function() { - frm.trigger("create_asset_adjustment"); - }, __('Create')); - } - - if (!frm.doc.calculate_depreciation) { - frm.add_custom_button(__("Depreciation Entry"), function() { - frm.trigger("make_journal_entry"); - }, __('Create')); - } - - frm.page.set_inner_btn_group_as_primary(__('Create')); frm.trigger("setup_chart"); } @@ -304,6 +308,20 @@ frappe.ui.form.on('Asset', { }) }, + create_asset_repair: function(frm) { + frappe.call({ + args: { + "asset": frm.doc.name, + "asset_name": frm.doc.asset_name + }, + method: "erpnext.assets.doctype.asset.asset.create_asset_repair", + callback: function(r) { + var doclist = frappe.model.sync(r.message); + frappe.set_route("Form", doclist[0].doctype, doclist[0].name); + } + }); + }, + create_asset_adjustment: function(frm) { frappe.call({ args: { diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json index 421b9a6c378..de060757e2e 100644 --- a/erpnext/assets/doctype/asset/asset.json +++ b/erpnext/assets/doctype/asset/asset.json @@ -502,7 +502,7 @@ "link_fieldname": "asset" } ], - "modified": "2021-01-22 12:38:59.091510", + "modified": "2021-06-24 14:58:51.097908", "modified_by": "Administrator", "module": "Assets", "name": "Asset", diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 8799275fc4e..6828c58960b 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -56,12 +56,12 @@ class Asset(AccountsController): if self.is_existing_asset and self.purchase_invoice: frappe.throw(_("Purchase Invoice cannot be made against an existing asset {0}").format(self.name)) - def prepare_depreciation_data(self): + def prepare_depreciation_data(self, date_of_sale=None): if self.calculate_depreciation: self.value_after_depreciation = 0 self.set_depreciation_rate() - self.make_depreciation_schedule() - self.set_accumulated_depreciation() + self.make_depreciation_schedule(date_of_sale) + self.set_accumulated_depreciation(date_of_sale) else: self.finance_books = [] self.value_after_depreciation = (flt(self.gross_purchase_amount) - @@ -167,18 +167,25 @@ class Asset(AccountsController): d.rate_of_depreciation = flt(self.get_depreciation_rate(d, on_validate=True), d.precision("rate_of_depreciation")) - def make_depreciation_schedule(self): - if 'Manual' not in [d.depreciation_method for d in self.finance_books]: + def make_depreciation_schedule(self, date_of_sale): + if 'Manual' not in [d.depreciation_method for d in self.finance_books] and not self.schedules: self.schedules = [] - if self.get("schedules") or not self.available_for_use_date: + if not self.available_for_use_date: return for d in self.get('finance_books'): self.validate_asset_finance_books(d) - value_after_depreciation = (flt(self.gross_purchase_amount) - - flt(self.opening_accumulated_depreciation)) + start = self.clear_depreciation_schedule() + + # value_after_depreciation - current Asset value + if d.value_after_depreciation: + value_after_depreciation = (flt(d.value_after_depreciation) - + flt(self.opening_accumulated_depreciation)) + else: + value_after_depreciation = (flt(self.gross_purchase_amount) - + flt(self.opening_accumulated_depreciation)) d.value_after_depreciation = value_after_depreciation @@ -191,7 +198,7 @@ class Asset(AccountsController): number_of_pending_depreciations += 1 skip_row = False - for n in range(number_of_pending_depreciations): + for n in range(start, number_of_pending_depreciations): # If depreciation is already completed (for double declining balance) if skip_row: continue @@ -205,6 +212,21 @@ class Asset(AccountsController): # so monthly schedule date is calculated by removing 11 months from it monthly_schedule_date = add_months(schedule_date, - d.frequency_of_depreciation + 1) + # if asset is being sold + if date_of_sale: + from_date = self.get_from_date(d.finance_book) + depreciation_amount, days, months = self.get_pro_rata_amt(d, depreciation_amount, + from_date, date_of_sale) + + self.append("schedules", { + "schedule_date": date_of_sale, + "depreciation_amount": depreciation_amount, + "depreciation_method": d.depreciation_method, + "finance_book": d.finance_book, + "finance_book_id": d.idx + }) + break + # For first row if has_pro_rata and n==0: depreciation_amount, days, months = self.get_pro_rata_amt(d, depreciation_amount, @@ -216,11 +238,13 @@ class Asset(AccountsController): # For last row elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1: - to_date = add_months(self.available_for_use_date, - n * cint(d.frequency_of_depreciation)) + if not self.flags.increase_in_asset_life: + # In case of increase_in_asset_life, the self.to_date is already set on asset_repair submission + self.to_date = add_months(self.available_for_use_date, + n * cint(d.frequency_of_depreciation)) depreciation_amount, days, months = self.get_pro_rata_amt(d, - depreciation_amount, schedule_date, to_date) + depreciation_amount, schedule_date, self.to_date) monthly_schedule_date = add_months(schedule_date, 1) @@ -284,10 +308,38 @@ class Asset(AccountsController): "finance_book_id": d.idx }) + # used when depreciation schedule needs to be modified due to increase in asset life + def clear_depreciation_schedule(self): + start = 0 + for n in range(len(self.schedules)): + if not self.schedules[n].journal_entry: + del self.schedules[n:] + start = n + break + return start + + def get_from_date(self, finance_book): + if not self.get('schedules'): + return self.available_for_use_date + + if len(self.finance_books) == 1: + return self.schedules[-1].schedule_date + + from_date = "" + for schedule in self.get('schedules'): + if schedule.finance_book == finance_book: + from_date = schedule.schedule_date + + if from_date: + return from_date + return self.available_for_use_date + + # if it returns True, depreciation_amount will not be equal for the first and last rows def check_is_pro_rata(self, row): has_pro_rata = False - days = date_diff(row.depreciation_start_date, self.available_for_use_date) + 1 + + # if frequency_of_depreciation is 12 months, total_days = 365 total_days = get_total_days(row.depreciation_start_date, row.frequency_of_depreciation) if days < total_days: @@ -335,7 +387,7 @@ class Asset(AccountsController): frappe.throw(_("Depreciation Row {0}: Next Depreciation Date cannot be before Available-for-use Date") .format(row.idx)) - def set_accumulated_depreciation(self, ignore_booked_entry = False): + def set_accumulated_depreciation(self, date_of_sale=None, ignore_booked_entry = False): straight_line_idx = [d.idx for d in self.get("schedules") if d.depreciation_method == 'Straight Line'] finance_books = [] @@ -343,15 +395,16 @@ class Asset(AccountsController): if ignore_booked_entry and d.journal_entry: continue - if d.finance_book_id not in finance_books: + if int(d.finance_book_id) not in finance_books: accumulated_depreciation = flt(self.opening_accumulated_depreciation) value_after_depreciation = flt(self.get_value_after_depreciation(d.finance_book_id)) - finance_books.append(d.finance_book_id) + finance_books.append(int(d.finance_book_id)) depreciation_amount = flt(d.depreciation_amount, d.precision("depreciation_amount")) value_after_depreciation -= flt(depreciation_amount) - if straight_line_idx and i == max(straight_line_idx) - 1: + # for the last row, if depreciation method = Straight Line + if straight_line_idx and i == max(straight_line_idx) - 1 and not date_of_sale: book = self.get('finance_books')[cint(d.finance_book_id) - 1] depreciation_amount += flt(value_after_depreciation - flt(book.expected_value_after_useful_life), d.precision("depreciation_amount")) @@ -625,9 +678,18 @@ def create_asset_maintenance(asset, item_code, item_name, asset_category, compan }) return asset_maintenance +@frappe.whitelist() +def create_asset_repair(asset, asset_name): + asset_repair = frappe.new_doc("Asset Repair") + asset_repair.update({ + "asset": asset, + "asset_name": asset_name + }) + return asset_repair + @frappe.whitelist() def create_asset_adjustment(asset, asset_category, company): - asset_maintenance = frappe.new_doc("Asset Value Adjustment") + asset_maintenance = frappe.get_doc("Asset Value Adjustment") asset_maintenance.update({ "asset": asset, "company": company, @@ -757,9 +819,16 @@ def get_depreciation_amount(asset, depreciable_value, row): depreciation_left = flt(row.total_number_of_depreciations) - flt(asset.number_of_depreciations_booked) if row.depreciation_method in ("Straight Line", "Manual"): - depreciation_amount = (flt(row.value_after_depreciation) - - flt(row.expected_value_after_useful_life)) / depreciation_left + # if the Depreciation Schedule is being prepared for the first time + if not asset.flags.increase_in_asset_life: + depreciation_amount = (flt(row.value_after_depreciation) - + flt(row.expected_value_after_useful_life)) / depreciation_left + + # if the Depreciation Schedule is being modified after Asset Repair + else: + depreciation_amount = (flt(row.value_after_depreciation) - + flt(row.expected_value_after_useful_life)) / (date_diff(asset.to_date, asset.available_for_use_date) / 365) else: depreciation_amount = flt(depreciable_value * (flt(row.rate_of_depreciation) / 100)) - return depreciation_amount \ No newline at end of file + return depreciation_amount diff --git a/erpnext/assets/doctype/asset/asset_dashboard.py b/erpnext/assets/doctype/asset/asset_dashboard.py index a5cf23803d2..62bb4be53aa 100644 --- a/erpnext/assets/doctype/asset/asset_dashboard.py +++ b/erpnext/assets/doctype/asset/asset_dashboard.py @@ -11,4 +11,4 @@ def get_data(): 'items': ['Asset Movement'] } ] - } \ No newline at end of file + } diff --git a/erpnext/assets/doctype/asset/asset_list.js b/erpnext/assets/doctype/asset/asset_list.js index 02f39e0e7f4..4302cb2c518 100644 --- a/erpnext/assets/doctype/asset/asset_list.js +++ b/erpnext/assets/doctype/asset/asset_list.js @@ -50,4 +50,4 @@ frappe.listview_settings['Asset'] = { }); }); }, -} \ No newline at end of file +} diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index 8f0afb42b2c..9a61b79ed35 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -59,7 +59,7 @@ def make_depreciation_entry(asset_name, date=None): "credit_in_account_currency": d.depreciation_amount, "reference_type": "Asset", "reference_name": asset.name, - "cost_center": "" + "cost_center": depreciation_cost_center } debit_entry = { @@ -176,22 +176,34 @@ def restore_asset(asset_name): asset.set_status() -@frappe.whitelist() +def get_gl_entries_on_asset_regain(asset, selling_amount=0, finance_book=None): + fixed_asset_account, asset, depreciation_cost_center, accumulated_depr_account, accumulated_depr_amount, disposal_account, value_after_depreciation = \ + get_asset_details(asset, finance_book) + + gl_entries = [ + { + "account": fixed_asset_account, + "debit_in_account_currency": asset.gross_purchase_amount, + "debit": asset.gross_purchase_amount, + "cost_center": depreciation_cost_center + }, + { + "account": accumulated_depr_account, + "credit_in_account_currency": accumulated_depr_amount, + "credit": accumulated_depr_amount, + "cost_center": depreciation_cost_center + } + ] + + profit_amount = abs(flt(value_after_depreciation)) - abs(flt(selling_amount)) + if profit_amount: + get_profit_gl_entries(profit_amount, gl_entries, disposal_account, depreciation_cost_center) + + return gl_entries + def get_gl_entries_on_asset_disposal(asset, selling_amount=0, finance_book=None): - fixed_asset_account, accumulated_depr_account, depr_expense_account = get_depreciation_accounts(asset) - disposal_account, depreciation_cost_center = get_disposal_account_and_cost_center(asset.company) - depreciation_cost_center = asset.cost_center or depreciation_cost_center - - idx = 1 - if finance_book: - for d in asset.finance_books: - if d.finance_book == finance_book: - idx = d.idx - break - - value_after_depreciation = (asset.finance_books[idx - 1].value_after_depreciation - if asset.calculate_depreciation else asset.value_after_depreciation) - accumulated_depr_amount = flt(asset.gross_purchase_amount) - flt(value_after_depreciation) + fixed_asset_account, asset, depreciation_cost_center, accumulated_depr_account, accumulated_depr_amount, disposal_account, value_after_depreciation = \ + get_asset_details(asset, finance_book) gl_entries = [ { @@ -210,16 +222,37 @@ def get_gl_entries_on_asset_disposal(asset, selling_amount=0, finance_book=None) profit_amount = flt(selling_amount) - flt(value_after_depreciation) if profit_amount: - debit_or_credit = "debit" if profit_amount < 0 else "credit" - gl_entries.append({ - "account": disposal_account, - "cost_center": depreciation_cost_center, - debit_or_credit: abs(profit_amount), - debit_or_credit + "_in_account_currency": abs(profit_amount) - }) + get_profit_gl_entries(profit_amount, gl_entries, disposal_account, depreciation_cost_center) return gl_entries +def get_asset_details(asset, finance_book=None): + fixed_asset_account, accumulated_depr_account, depr_expense_account = get_depreciation_accounts(asset) + disposal_account, depreciation_cost_center = get_disposal_account_and_cost_center(asset.company) + depreciation_cost_center = asset.cost_center or depreciation_cost_center + + idx = 1 + if finance_book: + for d in asset.finance_books: + if d.finance_book == finance_book: + idx = d.idx + break + + value_after_depreciation = (asset.finance_books[idx - 1].value_after_depreciation + if asset.calculate_depreciation else asset.value_after_depreciation) + accumulated_depr_amount = flt(asset.gross_purchase_amount) - flt(value_after_depreciation) + + return fixed_asset_account, asset, depreciation_cost_center, accumulated_depr_account, accumulated_depr_amount, disposal_account, value_after_depreciation + +def get_profit_gl_entries(profit_amount, gl_entries, disposal_account, depreciation_cost_center): + debit_or_credit = "debit" if profit_amount < 0 else "credit" + gl_entries.append({ + "account": disposal_account, + "cost_center": depreciation_cost_center, + debit_or_credit: abs(profit_amount), + debit_or_credit + "_in_account_currency": abs(profit_amount) + }) + @frappe.whitelist() def get_disposal_account_and_cost_center(company): disposal_account, depreciation_cost_center = frappe.get_cached_value('Company', company, diff --git a/erpnext/assets/doctype/asset/test_asset.js b/erpnext/assets/doctype/asset/test_asset.js deleted file mode 100644 index 6119e382174..00000000000 --- 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/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index 8845f24d104..605ce2e2503 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -125,7 +125,6 @@ class TestAsset(unittest.TestCase): "frequency_of_depreciation": 12, "depreciation_start_date": "2030-12-31" }) - asset.insert() self.assertEqual(asset.status, "Draft") asset.save() expected_schedules = [ @@ -154,9 +153,8 @@ class TestAsset(unittest.TestCase): "frequency_of_depreciation": 12, "depreciation_start_date": '2030-12-31' }) - asset.insert() - self.assertEqual(asset.status, "Draft") asset.save() + self.assertEqual(asset.status, "Draft") expected_schedules = [ ['2030-12-31', 66667.00, 66667.00], @@ -185,7 +183,7 @@ class TestAsset(unittest.TestCase): "frequency_of_depreciation": 12, "depreciation_start_date": "2030-12-31" }) - asset.insert() + asset.save() self.assertEqual(asset.status, "Draft") expected_schedules = [ @@ -216,7 +214,6 @@ class TestAsset(unittest.TestCase): "depreciation_start_date": "2030-12-31" }) - asset.insert() asset.save() expected_schedules = [ @@ -247,7 +244,6 @@ class TestAsset(unittest.TestCase): "frequency_of_depreciation": 10, "depreciation_start_date": "2020-12-31" }) - asset.insert() asset.submit() asset.load_from_db() self.assertEqual(asset.status, "Submitted") @@ -350,7 +346,6 @@ class TestAsset(unittest.TestCase): "frequency_of_depreciation": 10, "depreciation_start_date": "2020-12-31" }) - asset.insert() asset.submit() post_depreciation_entries(date="2021-01-01") @@ -380,7 +375,6 @@ class TestAsset(unittest.TestCase): "total_number_of_depreciations": 10, "frequency_of_depreciation": 1 }) - asset.insert() asset.submit() post_depreciation_entries(date=add_months('2020-01-01', 4)) @@ -424,7 +418,6 @@ class TestAsset(unittest.TestCase): "frequency_of_depreciation": 10, "depreciation_start_date": "2020-12-31" }) - asset.insert() asset.submit() post_depreciation_entries(date="2021-01-01") @@ -468,7 +461,7 @@ class TestAsset(unittest.TestCase): "total_number_of_depreciations": 3, "frequency_of_depreciation": 10 }) - asset.insert() + asset.save() accumulated_depreciation_after_full_schedule = \ max(d.accumulated_depreciation_amount for d in asset.get("schedules")) @@ -646,7 +639,7 @@ class TestAsset(unittest.TestCase): asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') asset = frappe.get_doc('Asset', asset_name) asset.calculate_depreciation = 1 - asset.available_for_use_date = '2030-06-12' + asset.available_for_use_date = '2030-07-12' asset.purchase_date = '2030-01-01' asset.append("finance_books", { "expected_value_after_useful_life": 1000, @@ -660,10 +653,10 @@ class TestAsset(unittest.TestCase): self.assertEqual(asset.finance_books[0].rate_of_depreciation, 50.0) expected_schedules = [ - ["2030-12-31", 1106.85, 1106.85], - ["2031-12-31", 3446.58, 4553.43], - ["2032-12-31", 1723.29, 6276.72], - ["2033-06-12", 723.28, 7000.00] + ["2030-12-31", 942.47, 942.47], + ["2031-12-31", 3528.77, 4471.24], + ["2032-12-31", 1764.38, 6235.62], + ["2033-07-12", 764.38, 7000.00] ] schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2)] @@ -699,7 +692,7 @@ def create_asset(**args): "item_code": args.item_code or "Macbook Pro", "company": args.company or"_Test Company", "purchase_date": "2015-01-01", - "calculate_depreciation": 0, + "calculate_depreciation": args.calculate_depreciation or 0, "gross_purchase_amount": 100000, "purchase_receipt_amount": 100000, "expected_value_after_useful_life": 10000, @@ -707,9 +700,16 @@ def create_asset(**args): "available_for_use_date": "2020-06-06", "location": "Test Location", "asset_owner": "Company", - "is_existing_asset": args.is_existing_asset or 0 + "is_existing_asset": 1 }) + if asset.calculate_depreciation: + asset.append("finance_books", { + "depreciation_method": "Straight Line", + "frequency_of_depreciation": 12, + "total_number_of_depreciations": 5 + }) + try: asset.save() except frappe.DuplicateEntryError: @@ -763,4 +763,4 @@ def set_depreciation_settings_in_company(): company.save() # Enable booking asset depreciation entry automatically - frappe.db.set_value("Accounts Settings", None, "book_asset_depreciation_entry_automatically", 1) \ No newline at end of file + frappe.db.set_value("Accounts Settings", None, "book_asset_depreciation_entry_automatically", 1) diff --git a/erpnext/assets/doctype/asset_category/asset_category.py b/erpnext/assets/doctype/asset_category/asset_category.py index 46620d56e98..39032d637b5 100644 --- a/erpnext/assets/doctype/asset_category/asset_category.py +++ b/erpnext/assets/doctype/asset_category/asset_category.py @@ -20,7 +20,7 @@ class AssetCategory(Document): for field in ("Total Number of Depreciations", "Frequency of Depreciation"): if cint(d.get(frappe.scrub(field)))<1: frappe.throw(_("Row {0}: {1} must be greater than 0").format(d.idx, field), frappe.MandatoryError) - + def validate_account_currency(self): account_types = [ 'fixed_asset_account', 'accumulated_depreciation_account', 'depreciation_expense_account', 'capital_work_in_progress_account' @@ -33,13 +33,13 @@ class AssetCategory(Document): account_currency = frappe.get_value("Account", d.get(type_of_account), "account_currency") if account_currency != company_currency: invalid_accounts.append(frappe._dict({ 'type': type_of_account, 'idx': d.idx, 'account': d.get(type_of_account) })) - + for d in invalid_accounts: frappe.throw(_("Row #{}: Currency of {} - {} doesn't matches company currency.") .format(d.idx, frappe.bold(frappe.unscrub(d.type)), frappe.bold(d.account)), title=_("Invalid Account")) - + def validate_account_types(self): account_type_map = { 'fixed_asset_account': { 'account_type': 'Fixed Asset' }, @@ -59,12 +59,12 @@ class AssetCategory(Document): frappe.throw(_("Row #{}: {} of {} should be {}. Please modify the account or select a different account.") .format(d.idx, frappe.unscrub(key_to_match), frappe.bold(selected_account), frappe.bold(expected_key_type)), title=_("Invalid Account")) - + def valide_cwip_account(self): if self.enable_cwip_accounting: missing_cwip_accounts_for_company = [] for d in self.accounts: - if (not d.capital_work_in_progress_account and + if (not d.capital_work_in_progress_account and not frappe.db.get_value("Company", d.company_name, "capital_work_in_progress_account")): missing_cwip_accounts_for_company.append(get_link_to_form("Company", d.company_name)) @@ -93,4 +93,4 @@ def get_asset_category_account(fieldname, item=None, asset=None, account=None, a account = frappe.db.get_value("Asset Category Account", filters={"parent": asset_category, "company_name": company}, fieldname=fieldname) - return account \ No newline at end of file + return account 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 7e343b7519e..00000000000 --- 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_category/test_asset_category.py b/erpnext/assets/doctype/asset_category/test_asset_category.py index 39b79d6c507..9f7ada65d82 100644 --- a/erpnext/assets/doctype/asset_category/test_asset_category.py +++ b/erpnext/assets/doctype/asset_category/test_asset_category.py @@ -10,9 +10,9 @@ class TestAssetCategory(unittest.TestCase): def test_mandatory_fields(self): asset_category = frappe.new_doc("Asset Category") asset_category.asset_category_name = "Computers" - + self.assertRaises(frappe.MandatoryError, asset_category.insert) - + asset_category.total_number_of_depreciations = 3 asset_category.frequency_of_depreciation = 3 asset_category.append("accounts", { @@ -21,7 +21,7 @@ class TestAssetCategory(unittest.TestCase): "accumulated_depreciation_account": "_Test Accumulated Depreciations - _TC", "depreciation_expense_account": "_Test Depreciations - _TC" }) - + try: asset_category.insert() except frappe.DuplicateEntryError: @@ -44,4 +44,4 @@ class TestAssetCategory(unittest.TestCase): "depreciation_expense_account": "_Test Depreciations - _TC" }) - self.assertRaises(frappe.ValidationError, asset_category.insert) \ No newline at end of file + self.assertRaises(frappe.ValidationError, asset_category.insert) diff --git a/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json b/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json index d9b7b695f7f..e5a5f194c1b 100644 --- a/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json +++ b/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json @@ -67,7 +67,6 @@ { "fieldname": "value_after_depreciation", "fieldtype": "Currency", - "hidden": 1, "label": "Value After Depreciation", "no_copy": 1, "options": "Company:company:default_currency", @@ -85,7 +84,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-11-05 16:30:09.213479", + "modified": "2021-06-17 12:59:05.743683", "modified_by": "Administrator", "module": "Assets", "name": "Asset Finance Book", diff --git a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.js b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.js index 70b8654509f..52996e93475 100644 --- a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.js +++ b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.js @@ -97,4 +97,4 @@ var get_next_due_date = function (frm, cdt, cdn) { } }); } -}; \ No newline at end of file +}; diff --git a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py index a506deec93e..e14f1d88dcb 100644 --- a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py +++ b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py @@ -116,4 +116,4 @@ def get_maintenance_log(asset_name): select maintenance_status, count(asset_name) as count, asset_name from `tabAsset Maintenance Log` where asset_name=%s group by maintenance_status""", - (asset_name), as_dict=1) \ No newline at end of file + (asset_name), as_dict=1) 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 f9b38a1020e..00000000000 --- 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/test_asset_maintenance.py b/erpnext/assets/doctype/asset_maintenance/test_asset_maintenance.py index 392fbdd2af7..7610152039d 100644 --- a/erpnext/assets/doctype/asset_maintenance/test_asset_maintenance.py +++ b/erpnext/assets/doctype/asset_maintenance/test_asset_maintenance.py @@ -73,7 +73,7 @@ def create_asset_data(): 'doctype': 'Location', 'location_name': 'Test Location' }).insert() - + if not frappe.db.exists("Item", "Photocopier"): meta = frappe.get_meta('Asset') naming_series = meta.get_field("naming_series").options @@ -157,6 +157,6 @@ def set_depreciation_settings_in_company(): company.disposal_account = "_Test Gain/Loss on Asset Disposal - _TC" company.depreciation_cost_center = "_Test Cost Center - _TC" company.save() - + # Enable booking asset depreciation entry automatically - frappe.db.set_value("Accounts Settings", None, "book_asset_depreciation_entry_automatically", 1) \ No newline at end of file + frappe.db.set_value("Accounts Settings", None, "book_asset_depreciation_entry_automatically", 1) diff --git a/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log.js b/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log.js index c5db90ad370..bcdc3acf0ac 100644 --- a/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log.js +++ b/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log.js @@ -12,4 +12,4 @@ frappe.ui.form.on('Asset Maintenance Log', { }; }); } -}); \ No newline at end of file +}); 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 4e80184ea7b..00000000000 --- 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 41bf69623ec..00000000000 --- 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/asset_movement.js b/erpnext/assets/doctype/asset_movement/asset_movement.js index 06d8879091c..2df7db97446 100644 --- a/erpnext/assets/doctype/asset_movement/asset_movement.js +++ b/erpnext/assets/doctype/asset_movement/asset_movement.js @@ -99,4 +99,4 @@ frappe.ui.form.on('Asset Movement Item', { }); } } -}); \ No newline at end of file +}); diff --git a/erpnext/assets/doctype/asset_movement/asset_movement.py b/erpnext/assets/doctype/asset_movement/asset_movement.py index b2de250b168..1771e27ddfe 100644 --- a/erpnext/assets/doctype/asset_movement/asset_movement.py +++ b/erpnext/assets/doctype/asset_movement/asset_movement.py @@ -40,14 +40,14 @@ class AssetMovement(Document): if current_location != d.source_location: frappe.throw(_("Asset {0} does not belongs to the location {1}"). format(d.asset, d.source_location)) - + if self.purpose == 'Issue': if d.target_location: frappe.throw(_("Issuing cannot be done to a location. \ Please enter employee who has issued Asset {0}").format(d.asset), title="Incorrect Movement Purpose") if not d.to_employee: frappe.throw(_("Employee is required while issuing Asset {0}").format(d.asset)) - + if self.purpose == 'Transfer': if d.to_employee: frappe.throw(_("Transferring cannot be done to an Employee. \ @@ -57,7 +57,7 @@ class AssetMovement(Document): frappe.throw(_("Target Location is required while transferring Asset {0}").format(d.asset)) if d.source_location == d.target_location: frappe.throw(_("Source and Target Location cannot be same")) - + if self.purpose == 'Receipt': # only when asset is bought and first entry is made if not d.source_location and not (d.target_location or d.to_employee): @@ -80,14 +80,14 @@ class AssetMovement(Document): if current_custodian != d.from_employee: frappe.throw(_("Asset {0} does not belongs to the custodian {1}"). format(d.asset, d.from_employee)) - + if d.to_employee and frappe.db.get_value("Employee", d.to_employee, "company") != self.company: frappe.throw(_("Employee {0} does not belongs to the company {1}"). format(d.to_employee, self.company)) def on_submit(self): self.set_latest_location_in_asset() - + def on_cancel(self): self.set_latest_location_in_asset() @@ -105,12 +105,12 @@ class AssetMovement(Document): # In case of cancellation it corresponds to previous latest document's location, employee latest_movement_entry = frappe.db.sql( """ - SELECT asm_item.target_location, asm_item.to_employee + SELECT asm_item.target_location, asm_item.to_employee FROM `tabAsset Movement Item` asm_item, `tabAsset Movement` asm - WHERE + WHERE asm_item.parent=asm.name and asm_item.asset=%(asset)s and - asm.company=%(company)s and + asm.company=%(company)s and asm.docstatus=1 and {0} ORDER BY asm.transaction_date desc limit 1 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 b9515763c42..00000000000 --- 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_movement/test_asset_movement.py b/erpnext/assets/doctype/asset_movement/test_asset_movement.py index cddee5fa0f1..2b2d2b44004 100644 --- a/erpnext/assets/doctype/asset_movement/test_asset_movement.py +++ b/erpnext/assets/doctype/asset_movement/test_asset_movement.py @@ -15,6 +15,7 @@ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_pu class TestAssetMovement(unittest.TestCase): def setUp(self): + frappe.db.set_value("Company", "_Test Company", "capital_work_in_progress_account", "CWIP Account - _TC") create_asset_data() make_location() @@ -45,12 +46,12 @@ class TestAssetMovement(unittest.TestCase): 'location_name': 'Test Location 2' }).insert() - movement1 = create_asset_movement(purpose = 'Transfer', company = asset.company, + movement1 = create_asset_movement(purpose = 'Transfer', company = asset.company, assets = [{ 'asset': asset.name , 'source_location': 'Test Location', 'target_location': 'Test Location 2'}], reference_doctype = 'Purchase Receipt', reference_name = pr.name) self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location 2") - movement2 = create_asset_movement(purpose = 'Transfer', company = asset.company, + create_asset_movement(purpose = 'Transfer', company = asset.company, assets = [{ 'asset': asset.name , 'source_location': 'Test Location 2', 'target_location': 'Test Location'}], reference_doctype = 'Purchase Receipt', reference_name = pr.name) self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location") @@ -59,18 +60,18 @@ class TestAssetMovement(unittest.TestCase): self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location") employee = make_employee("testassetmovemp@example.com", company="_Test Company") - movement3 = create_asset_movement(purpose = 'Issue', company = asset.company, + create_asset_movement(purpose = 'Issue', company = asset.company, assets = [{ 'asset': asset.name , 'source_location': 'Test Location', 'to_employee': employee}], reference_doctype = 'Purchase Receipt', reference_name = pr.name) - + # after issuing asset should belong to an employee not at a location self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), None) self.assertEqual(frappe.db.get_value("Asset", asset.name, "custodian"), employee) - + def test_last_movement_cancellation(self): pr = make_purchase_receipt(item_code="Macbook Pro", qty=1, rate=100000.0, location="Test Location") - + asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') asset = frappe.get_doc('Asset', asset_name) asset.calculate_depreciation = 1 @@ -85,17 +86,17 @@ class TestAssetMovement(unittest.TestCase): }) if asset.docstatus == 0: asset.submit() - + if not frappe.db.exists("Location", "Test Location 2"): frappe.get_doc({ 'doctype': 'Location', 'location_name': 'Test Location 2' }).insert() - + movement = frappe.get_doc({'doctype': 'Asset Movement', 'reference_name': pr.name }) self.assertRaises(frappe.ValidationError, movement.cancel) - movement1 = create_asset_movement(purpose = 'Transfer', company = asset.company, + movement1 = create_asset_movement(purpose = 'Transfer', company = asset.company, assets = [{ 'asset': asset.name , 'source_location': 'Test Location', 'target_location': 'Test Location 2'}], reference_doctype = 'Purchase Receipt', reference_name = pr.name) self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location 2") diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js index 4ba2b4474a9..18a56d33e6d 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.js +++ b/erpnext/assets/doctype/asset_repair/asset_repair.js @@ -2,6 +2,45 @@ // For license information, please see license.txt frappe.ui.form.on('Asset Repair', { + setup: function(frm) { + frm.fields_dict.cost_center.get_query = function(doc) { + return { + filters: { + 'is_group': 0, + 'company': doc.company + } + }; + }; + + frm.fields_dict.project.get_query = function(doc) { + return { + filters: { + 'company': doc.company + } + }; + }; + + frm.fields_dict.warehouse.get_query = function(doc) { + return { + filters: { + 'is_group': 0, + 'company': doc.company + } + }; + }; + }, + + refresh: function(frm) { + if (frm.doc.docstatus) { + frm.add_custom_button("View General Ledger", function() { + frappe.route_options = { + "voucher_no": frm.doc.name + }; + frappe.set_route("query-report", "General Ledger"); + }); + } + }, + repair_status: (frm) => { if (frm.doc.completion_date && frm.doc.repair_status == "Completed") { frappe.call ({ @@ -17,5 +56,16 @@ frappe.ui.form.on('Asset Repair', { } }); } + + if (frm.doc.repair_status == "Completed") { + frm.set_value('completion_date', frappe.datetime.now_datetime()); + } } }); + +frappe.ui.form.on('Asset Repair Consumed Item', { + consumed_quantity: function(frm, cdt, cdn) { + var row = locals[cdt][cdn]; + frappe.model.set_value(cdt, cdn, 'total_value', row.consumed_quantity * row.valuation_rate); + }, +}); diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index d338fc0fb79..19528a26ccd 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -7,38 +7,43 @@ "editable_grid": 1, "engine": "InnoDB", "field_order": [ - "naming_series", - "asset_name", + "asset", + "company", "column_break_2", - "item_code", - "item_name", + "asset_name", + "naming_series", "section_break_5", "failure_date", - "assign_to", - "assign_to_name", + "repair_status", "column_break_6", "completion_date", - "repair_status", + "accounting_dimensions_section", + "cost_center", + "column_break_14", + "project", + "accounting_details", "repair_cost", + "capitalize_repair_cost", + "stock_consumption", + "column_break_8", + "purchase_invoice", + "stock_consumption_details_section", + "warehouse", + "stock_items", + "total_repair_cost", + "stock_entry", + "asset_depreciation_details_section", + "increase_in_asset_life", "section_break_9", "description", "column_break_9", "actions_performed", - "section_break_17", + "section_break_23", "downtime", "column_break_19", "amended_from" ], "fields": [ - { - "columns": 1, - "fieldname": "asset_name", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Asset", - "options": "Asset", - "reqd": 1 - }, { "fieldname": "naming_series", "fieldtype": "Select", @@ -50,18 +55,6 @@ "fieldname": "column_break_2", "fieldtype": "Column Break" }, - { - "fetch_from": "asset_name.item_code", - "fieldname": "item_code", - "fieldtype": "Read Only", - "label": "Item Code" - }, - { - "fetch_from": "asset_name.item_name", - "fieldname": "item_name", - "fieldtype": "Read Only", - "label": "Item Name" - }, { "fieldname": "section_break_5", "fieldtype": "Section Break", @@ -74,33 +67,20 @@ "label": "Failure Date", "reqd": 1 }, - { - "allow_on_submit": 1, - "fieldname": "assign_to", - "fieldtype": "Link", - "label": "Assign To", - "options": "User" - }, - { - "allow_on_submit": 1, - "fetch_from": "assign_to.full_name", - "fieldname": "assign_to_name", - "fieldtype": "Read Only", - "label": "Assign To Name" - }, { "fieldname": "column_break_6", "fieldtype": "Column Break" }, { - "allow_on_submit": 1, + "depends_on": "eval:!doc.__islocal", "fieldname": "completion_date", "fieldtype": "Datetime", - "label": "Completion Date" + "label": "Completion Date", + "no_copy": 1 }, { - "allow_on_submit": 1, "default": "Pending", + "depends_on": "eval:!doc.__islocal", "fieldname": "repair_status", "fieldtype": "Select", "label": "Repair Status", @@ -116,25 +96,18 @@ { "fieldname": "description", "fieldtype": "Long Text", - "label": "Error Description", - "reqd": 1 + "label": "Error Description" }, { "fieldname": "column_break_9", "fieldtype": "Column Break" }, { - "allow_on_submit": 1, "fieldname": "actions_performed", "fieldtype": "Long Text", "label": "Actions performed" }, { - "fieldname": "section_break_17", - "fieldtype": "Section Break" - }, - { - "allow_on_submit": 1, "fieldname": "downtime", "fieldtype": "Data", "in_list_view": 1, @@ -146,7 +119,7 @@ "fieldtype": "Column Break" }, { - "allow_on_submit": 1, + "default": "0", "fieldname": "repair_cost", "fieldtype": "Currency", "label": "Repair Cost" @@ -159,12 +132,138 @@ "options": "Asset Repair", "print_hide": 1, "read_only": 1 + }, + { + "columns": 1, + "fieldname": "asset", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Asset", + "options": "Asset", + "reqd": 1 + }, + { + "fetch_from": "asset.asset_name", + "fieldname": "asset_name", + "fieldtype": "Read Only", + "label": "Asset Name" + }, + { + "fieldname": "column_break_8", + "fieldtype": "Column Break" + }, + { + "default": "0", + "depends_on": "eval:!doc.__islocal", + "fieldname": "capitalize_repair_cost", + "fieldtype": "Check", + "label": "Capitalize Repair Cost" + }, + { + "fieldname": "accounting_details", + "fieldtype": "Section Break", + "label": "Accounting Details" + }, + { + "fieldname": "stock_items", + "fieldtype": "Table", + "label": "Stock Items", + "mandatory_depends_on": "stock_consumption", + "options": "Asset Repair Consumed Item" + }, + { + "fieldname": "section_break_23", + "fieldtype": "Section Break" + }, + { + "fieldname": "accounting_dimensions_section", + "fieldtype": "Section Break", + "label": "Accounting Dimensions" + }, + { + "fieldname": "cost_center", + "fieldtype": "Link", + "label": "Cost Center", + "options": "Cost Center" + }, + { + "fieldname": "project", + "fieldtype": "Link", + "label": "Project", + "options": "Project" + }, + { + "fieldname": "column_break_14", + "fieldtype": "Column Break" + }, + { + "default": "0", + "depends_on": "eval:!doc.__islocal", + "fieldname": "stock_consumption", + "fieldtype": "Check", + "label": "Stock Consumed During Repair" + }, + { + "depends_on": "stock_consumption", + "fieldname": "stock_consumption_details_section", + "fieldtype": "Section Break", + "label": "Stock Consumption Details" + }, + { + "depends_on": "eval: doc.stock_consumption && doc.total_repair_cost > 0", + "description": "Sum of Repair Cost and Value of Consumed Stock Items.", + "fieldname": "total_repair_cost", + "fieldtype": "Currency", + "label": "Total Repair Cost", + "read_only": 1 + }, + { + "depends_on": "stock_consumption", + "fieldname": "warehouse", + "fieldtype": "Link", + "label": "Warehouse", + "options": "Warehouse" + }, + { + "depends_on": "capitalize_repair_cost", + "fieldname": "asset_depreciation_details_section", + "fieldtype": "Section Break", + "label": "Asset Depreciation Details" + }, + { + "fieldname": "increase_in_asset_life", + "fieldtype": "Int", + "label": "Increase In Asset Life(Months)", + "no_copy": 1 + }, + { + "depends_on": "eval:!doc.__islocal", + "fieldname": "purchase_invoice", + "fieldtype": "Link", + "label": "Purchase Invoice", + "mandatory_depends_on": "eval: doc.repair_status == 'Completed' && doc.repair_cost > 0", + "no_copy": 1, + "options": "Purchase Invoice" + }, + { + "fetch_from": "asset.company", + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company" + }, + { + "fieldname": "stock_entry", + "fieldtype": "Link", + "label": "Stock Entry", + "options": "Stock Entry", + "read_only": 1 } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-01-22 15:08:12.495850", + "modified": "2021-06-25 13:14:38.307723", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", @@ -203,6 +302,7 @@ ], "sort_field": "modified", "sort_order": "DESC", + "title_field": "asset_name", "track_changes": 1, "track_seen": 1 } \ No newline at end of file diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 049b931b5e8..746f582fdcd 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -5,16 +5,252 @@ from __future__ import unicode_literals import frappe from frappe import _ -from frappe.utils import time_diff_in_hours -from frappe.model.document import Document +from frappe.utils import time_diff_in_hours, getdate, add_months, flt, cint +from erpnext.accounts.general_ledger import make_gl_entries +from erpnext.assets.doctype.asset.asset import get_asset_account +from erpnext.controllers.accounts_controller import AccountsController -class AssetRepair(Document): +class AssetRepair(AccountsController): def validate(self): - if self.repair_status == "Completed" and not self.completion_date: - frappe.throw(_("Please select Completion Date for Completed Repair")) + self.asset_doc = frappe.get_doc('Asset', self.asset) + self.update_status() + if self.get('stock_items'): + self.set_total_value() + self.calculate_total_repair_cost() + + def update_status(self): + if self.repair_status == 'Pending': + frappe.db.set_value('Asset', self.asset, 'status', 'Out of Order') + else: + self.asset_doc.set_status() + + def set_total_value(self): + for item in self.get('stock_items'): + item.total_value = flt(item.valuation_rate) * flt(item.consumed_quantity) + + def calculate_total_repair_cost(self): + self.total_repair_cost = flt(self.repair_cost) + + total_value_of_stock_consumed = self.get_total_value_of_stock_consumed() + self.total_repair_cost += total_value_of_stock_consumed + + def before_submit(self): + self.check_repair_status() + + if self.get('stock_consumption') or self.get('capitalize_repair_cost'): + self.increase_asset_value() + if self.get('stock_consumption'): + self.check_for_stock_items_and_warehouse() + self.decrease_stock_quantity() + if self.get('capitalize_repair_cost'): + self.make_gl_entries() + if frappe.db.get_value('Asset', self.asset, 'calculate_depreciation') and self.increase_in_asset_life: + self.modify_depreciation_schedule() + + self.asset_doc.flags.ignore_validate_update_after_submit = True + self.asset_doc.prepare_depreciation_data() + self.asset_doc.save() + + def before_cancel(self): + self.asset_doc = frappe.get_doc('Asset', self.asset) + + if self.get('stock_consumption') or self.get('capitalize_repair_cost'): + self.decrease_asset_value() + if self.get('stock_consumption'): + self.increase_stock_quantity() + if self.get('capitalize_repair_cost'): + self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry') + self.make_gl_entries(cancel=True) + if frappe.db.get_value('Asset', self.asset, 'calculate_depreciation') and self.increase_in_asset_life: + self.revert_depreciation_schedule_on_cancellation() + + self.asset_doc.flags.ignore_validate_update_after_submit = True + self.asset_doc.prepare_depreciation_data() + self.asset_doc.save() + + def check_repair_status(self): + if self.repair_status == "Pending": + frappe.throw(_("Please update Repair Status.")) + + def check_for_stock_items_and_warehouse(self): + if not self.get('stock_items'): + frappe.throw(_("Please enter Stock Items consumed during the Repair."), title=_("Missing Items")) + if not self.warehouse: + frappe.throw(_("Please enter Warehouse from which Stock Items consumed during the Repair were taken."), title=_("Missing Warehouse")) + + def increase_asset_value(self): + total_value_of_stock_consumed = self.get_total_value_of_stock_consumed() + + if self.asset_doc.calculate_depreciation: + for row in self.asset_doc.finance_books: + row.value_after_depreciation += total_value_of_stock_consumed + + if self.capitalize_repair_cost: + row.value_after_depreciation += self.repair_cost + + def decrease_asset_value(self): + total_value_of_stock_consumed = self.get_total_value_of_stock_consumed() + + if self.asset_doc.calculate_depreciation: + for row in self.asset_doc.finance_books: + row.value_after_depreciation -= total_value_of_stock_consumed + + if self.capitalize_repair_cost: + row.value_after_depreciation -= self.repair_cost + + def get_total_value_of_stock_consumed(self): + total_value_of_stock_consumed = 0 + if self.get('stock_consumption'): + for item in self.get('stock_items'): + total_value_of_stock_consumed += item.total_value + + return total_value_of_stock_consumed + + def decrease_stock_quantity(self): + stock_entry = frappe.get_doc({ + "doctype": "Stock Entry", + "stock_entry_type": "Material Issue", + "company": self.company + }) + + for stock_item in self.get('stock_items'): + stock_entry.append('items', { + "s_warehouse": self.warehouse, + "item_code": stock_item.item, + "qty": stock_item.consumed_quantity, + "basic_rate": stock_item.valuation_rate + }) + + stock_entry.insert() + stock_entry.submit() + + self.db_set('stock_entry', stock_entry.name) + + def increase_stock_quantity(self): + stock_entry = frappe.get_doc('Stock Entry', self.stock_entry) + stock_entry.flags.ignore_links = True + stock_entry.cancel() + + def make_gl_entries(self, cancel=False): + if flt(self.repair_cost) > 0: + gl_entries = self.get_gl_entries() + make_gl_entries(gl_entries, cancel) + + def get_gl_entries(self): + gl_entries = [] + repair_and_maintenance_account = frappe.db.get_value('Company', self.company, 'repair_and_maintenance_account') + fixed_asset_account = get_asset_account("fixed_asset_account", asset=self.asset, company=self.company) + expense_account = frappe.get_doc('Purchase Invoice', self.purchase_invoice).items[0].expense_account + + gl_entries.append( + self.get_gl_dict({ + "account": expense_account, + "credit": self.repair_cost, + "credit_in_account_currency": self.repair_cost, + "against": repair_and_maintenance_account, + "voucher_type": self.doctype, + "voucher_no": self.name, + "cost_center": self.cost_center, + "posting_date": getdate(), + "company": self.company + }, item=self) + ) + + if self.get('stock_consumption'): + # creating GL Entries for each row in Stock Items based on the Stock Entry created for it + stock_entry = frappe.get_doc('Stock Entry', self.stock_entry) + for item in stock_entry.items: + gl_entries.append( + self.get_gl_dict({ + "account": item.expense_account, + "credit": item.amount, + "credit_in_account_currency": item.amount, + "against": repair_and_maintenance_account, + "voucher_type": self.doctype, + "voucher_no": self.name, + "cost_center": self.cost_center, + "posting_date": getdate(), + "company": self.company + }, item=self) + ) + + gl_entries.append( + self.get_gl_dict({ + "account": fixed_asset_account, + "debit": self.total_repair_cost, + "debit_in_account_currency": self.total_repair_cost, + "against": expense_account, + "voucher_type": self.doctype, + "voucher_no": self.name, + "cost_center": self.cost_center, + "posting_date": getdate(), + "against_voucher_type": "Purchase Invoice", + "against_voucher": self.purchase_invoice, + "company": self.company + }, item=self) + ) + + return gl_entries + + def modify_depreciation_schedule(self): + for row in self.asset_doc.finance_books: + row.total_number_of_depreciations += self.increase_in_asset_life/row.frequency_of_depreciation + + self.asset_doc.flags.increase_in_asset_life = False + extra_months = self.increase_in_asset_life % row.frequency_of_depreciation + if extra_months != 0: + self.calculate_last_schedule_date(self.asset_doc, row, extra_months) + + # to help modify depreciation schedule when increase_in_asset_life is not a multiple of frequency_of_depreciation + def calculate_last_schedule_date(self, asset, row, extra_months): + asset.flags.increase_in_asset_life = True + number_of_pending_depreciations = cint(row.total_number_of_depreciations) - \ + cint(asset.number_of_depreciations_booked) + + # the Schedule Date in the final row of the old Depreciation Schedule + last_schedule_date = asset.schedules[len(asset.schedules)-1].schedule_date + + # the Schedule Date in the final row of the new Depreciation Schedule + asset.to_date = add_months(last_schedule_date, extra_months) + + # the latest possible date at which the depreciation can occur, without increasing the Total Number of Depreciations + # if depreciations happen yearly and the Depreciation Posting Date is 01-01-2020, this could be 01-01-2021, 01-01-2022... + schedule_date = add_months(row.depreciation_start_date, + number_of_pending_depreciations * cint(row.frequency_of_depreciation)) + + if asset.to_date > schedule_date: + row.total_number_of_depreciations += 1 + + def revert_depreciation_schedule_on_cancellation(self): + for row in self.asset_doc.finance_books: + row.total_number_of_depreciations -= self.increase_in_asset_life/row.frequency_of_depreciation + + self.asset_doc.flags.increase_in_asset_life = False + extra_months = self.increase_in_asset_life % row.frequency_of_depreciation + if extra_months != 0: + self.calculate_last_schedule_date_before_modification(self.asset_doc, row, extra_months) + + def calculate_last_schedule_date_before_modification(self, asset, row, extra_months): + asset.flags.increase_in_asset_life = True + number_of_pending_depreciations = cint(row.total_number_of_depreciations) - \ + cint(asset.number_of_depreciations_booked) + + # the Schedule Date in the final row of the modified Depreciation Schedule + last_schedule_date = asset.schedules[len(asset.schedules)-1].schedule_date + + # the Schedule Date in the final row of the original Depreciation Schedule + asset.to_date = add_months(last_schedule_date, -extra_months) + + # the latest possible date at which the depreciation can occur, without decreasing the Total Number of Depreciations + # if depreciations happen yearly and the Depreciation Posting Date is 01-01-2020, this could be 01-01-2021, 01-01-2022... + schedule_date = add_months(row.depreciation_start_date, + (number_of_pending_depreciations - 1) * cint(row.frequency_of_depreciation)) + + if asset.to_date < schedule_date: + row.total_number_of_depreciations -= 1 @frappe.whitelist() def get_downtime(failure_date, completion_date): downtime = time_diff_in_hours(completion_date, failure_date) - return round(downtime, 2) \ No newline at end of file + return round(downtime, 2) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair_list.js b/erpnext/assets/doctype/asset_repair/asset_repair_list.js index f36fd2f8dcb..86376f40046 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair_list.js +++ b/erpnext/assets/doctype/asset_repair/asset_repair_list.js @@ -10,4 +10,3 @@ frappe.listview_settings['Asset Repair'] = { } } }; - 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 7424ffe2b80..00000000000 --- 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_repair/test_asset_repair.py b/erpnext/assets/doctype/asset_repair/test_asset_repair.py index 3d325a9683c..5e727d007a9 100644 --- a/erpnext/assets/doctype/asset_repair/test_asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/test_asset_repair.py @@ -2,8 +2,167 @@ # Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt from __future__ import unicode_literals - +import frappe +from frappe.utils import nowdate, flt import unittest +from erpnext.assets.doctype.asset.test_asset import create_asset_data, create_asset, set_depreciation_settings_in_company class TestAssetRepair(unittest.TestCase): - pass + def setUp(self): + set_depreciation_settings_in_company() + create_asset_data() + frappe.db.sql("delete from `tabTax Rule`") + + def test_update_status(self): + asset = create_asset() + initial_status = asset.status + asset_repair = create_asset_repair(asset = asset) + + if asset_repair.repair_status == "Pending": + asset.reload() + self.assertEqual(asset.status, "Out of Order") + + asset_repair.repair_status = "Completed" + asset_repair.save() + asset_status = frappe.db.get_value("Asset", asset_repair.asset, "status") + self.assertEqual(asset_status, initial_status) + + def test_stock_item_total_value(self): + asset_repair = create_asset_repair(stock_consumption = 1) + + for item in asset_repair.stock_items: + total_value = flt(item.valuation_rate) * flt(item.consumed_quantity) + self.assertEqual(item.total_value, total_value) + + def test_total_repair_cost(self): + asset_repair = create_asset_repair(stock_consumption = 1) + + total_repair_cost = asset_repair.repair_cost + self.assertEqual(total_repair_cost, asset_repair.repair_cost) + for item in asset_repair.stock_items: + total_repair_cost += item.total_value + + self.assertEqual(total_repair_cost, asset_repair.total_repair_cost) + + def test_repair_status_after_submit(self): + asset_repair = create_asset_repair(submit = 1) + self.assertNotEqual(asset_repair.repair_status, "Pending") + + def test_stock_items(self): + asset_repair = create_asset_repair(stock_consumption = 1) + self.assertTrue(asset_repair.stock_consumption) + self.assertTrue(asset_repair.stock_items) + + def test_warehouse(self): + asset_repair = create_asset_repair(stock_consumption = 1) + self.assertTrue(asset_repair.stock_consumption) + self.assertTrue(asset_repair.warehouse) + + def test_decrease_stock_quantity(self): + asset_repair = create_asset_repair(stock_consumption = 1, submit = 1) + stock_entry = frappe.get_last_doc('Stock Entry') + + self.assertEqual(stock_entry.stock_entry_type, "Material Issue") + self.assertEqual(stock_entry.items[0].s_warehouse, asset_repair.warehouse) + self.assertEqual(stock_entry.items[0].item_code, asset_repair.stock_items[0].item) + self.assertEqual(stock_entry.items[0].qty, asset_repair.stock_items[0].consumed_quantity) + + def test_increase_in_asset_value_due_to_stock_consumption(self): + asset = create_asset(calculate_depreciation = 1) + initial_asset_value = get_asset_value(asset) + asset_repair = create_asset_repair(asset= asset, stock_consumption = 1, submit = 1) + asset.reload() + + increase_in_asset_value = get_asset_value(asset) - initial_asset_value + self.assertEqual(asset_repair.stock_items[0].total_value, increase_in_asset_value) + + def test_increase_in_asset_value_due_to_repair_cost_capitalisation(self): + asset = create_asset(calculate_depreciation = 1) + initial_asset_value = get_asset_value(asset) + asset_repair = create_asset_repair(asset= asset, capitalize_repair_cost = 1, submit = 1) + asset.reload() + + increase_in_asset_value = get_asset_value(asset) - initial_asset_value + self.assertEqual(asset_repair.repair_cost, increase_in_asset_value) + + def test_purchase_invoice(self): + asset_repair = create_asset_repair(capitalize_repair_cost = 1, submit = 1) + self.assertTrue(asset_repair.purchase_invoice) + + def test_gl_entries(self): + asset_repair = create_asset_repair(capitalize_repair_cost = 1, submit = 1) + gl_entry = frappe.get_last_doc('GL Entry') + self.assertEqual(asset_repair.name, gl_entry.voucher_no) + + def test_increase_in_asset_life(self): + asset = create_asset(calculate_depreciation = 1) + initial_num_of_depreciations = num_of_depreciations(asset) + create_asset_repair(asset= asset, capitalize_repair_cost = 1, submit = 1) + asset.reload() + + self.assertEqual((initial_num_of_depreciations + 1), num_of_depreciations(asset)) + self.assertEqual(asset.schedules[-1].accumulated_depreciation_amount, asset.finance_books[0].value_after_depreciation) + +def get_asset_value(asset): + return asset.finance_books[0].value_after_depreciation + +def num_of_depreciations(asset): + return asset.finance_books[0].total_number_of_depreciations + +def create_asset_repair(**args): + from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice + + args = frappe._dict(args) + + if args.asset: + asset = args.asset + else: + asset = create_asset(is_existing_asset = 1) + asset_repair = frappe.new_doc("Asset Repair") + asset_repair.update({ + "asset": asset.name, + "asset_name": asset.asset_name, + "failure_date": nowdate(), + "description": "Test Description", + "repair_cost": 0, + "company": asset.company + }) + + if args.stock_consumption: + asset_repair.stock_consumption = 1 + asset_repair.warehouse = create_warehouse("Test Warehouse", company = asset.company) + asset_repair.append("stock_items", { + "item": args.item or args.item_code or "_Test Item", + "valuation_rate": args.rate if args.get("rate") is not None else 100, + "consumed_quantity": args.qty or 1 + }) + + asset_repair.insert(ignore_if_duplicate=True) + + if args.submit: + asset_repair.repair_status = "Completed" + asset_repair.cost_center = "_Test Cost Center - _TC" + + if args.stock_consumption: + stock_entry = frappe.get_doc({ + "doctype": "Stock Entry", + "stock_entry_type": "Material Receipt", + "company": asset.company + }) + stock_entry.append('items', { + "t_warehouse": asset_repair.warehouse, + "item_code": asset_repair.stock_items[0].item, + "qty": asset_repair.stock_items[0].consumed_quantity + }) + stock_entry.submit() + + if args.capitalize_repair_cost: + asset_repair.capitalize_repair_cost = 1 + asset_repair.repair_cost = 1000 + if asset.calculate_depreciation: + asset_repair.increase_in_asset_life = 12 + asset_repair.purchase_invoice = make_purchase_invoice().name + + asset_repair.submit() + return asset_repair diff --git a/erpnext/assets/doctype/asset_repair_consumed_item/__init__.py b/erpnext/assets/doctype/asset_repair_consumed_item/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json b/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json new file mode 100644 index 00000000000..528f0ec986a --- /dev/null +++ b/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json @@ -0,0 +1,55 @@ +{ + "actions": [], + "creation": "2021-05-12 02:41:54.161024", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "item", + "valuation_rate", + "consumed_quantity", + "total_value" + ], + "fields": [ + { + "fieldname": "item", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Item", + "options": "Item" + }, + { + "fetch_from": "item.valuation_rate", + "fieldname": "valuation_rate", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Valuation Rate", + "read_only": 1 + }, + { + "fieldname": "consumed_quantity", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Consumed Quantity" + }, + { + "fieldname": "total_value", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Total Value", + "read_only": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-05-12 03:19:55.006300", + "modified_by": "Administrator", + "module": "Assets", + "name": "Asset Repair Consumed Item", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.py b/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.py new file mode 100644 index 00000000000..fa22a5712f4 --- /dev/null +++ b/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_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 AssetRepairConsumedItem(Document): + pass 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 32831c61d2e..00000000000 --- 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/asset_value_adjustment/test_asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py index 03dc47b0bba..a9dc9795ee3 100644 --- a/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py +++ b/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py @@ -91,4 +91,4 @@ def make_asset_value_adjustment(**args): "cost_center": args.cost_center or "Main - _TC" }).insert() - return doc \ No newline at end of file + return doc diff --git a/erpnext/assets/doctype/location/location_tree.js b/erpnext/assets/doctype/location/location_tree.js index b405afd1ddd..3e105f6ca49 100644 --- a/erpnext/assets/doctype/location/location_tree.js +++ b/erpnext/assets/doctype/location/location_tree.js @@ -30,4 +30,4 @@ frappe.treeview_settings["Location"] = { onload: function (treeview) { treeview.make_tree(); } -}; \ No newline at end of file +}; diff --git a/erpnext/assets/doctype/location/test_location.js b/erpnext/assets/doctype/location/test_location.js deleted file mode 100644 index 3c06b63e820..00000000000 --- 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 d942e2a1566..00000000000 --- 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/report/fixed_asset_register/fixed_asset_register.js b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.js index 1a6ef54a830..75f42a9f783 100644 --- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.js +++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.js @@ -76,7 +76,7 @@ frappe.query_reports["Fixed Asset Register"] = { fieldtype: "Link", options: "Asset Category" }, - { + { fieldname:"finance_book", label: __("Finance Book"), fieldtype: "Link", diff --git a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py index d1457b9b85a..7d07397944b 100644 --- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py +++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py @@ -99,7 +99,7 @@ def prepare_chart_data(data, filters): labels_values_map = {} date_field = frappe.scrub(filters.date_based_on) - period_list = get_period_list(filters.from_fiscal_year, filters.to_fiscal_year, + period_list = get_period_list(filters.from_fiscal_year, filters.to_fiscal_year, filters.from_date, filters.to_date, filters.filter_based_on, "Monthly", company=filters.company) for d in period_list: @@ -293,4 +293,4 @@ def get_columns(filters): "options": "Location", "width": 100 }, - ] \ No newline at end of file + ] diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.js b/erpnext/buying/doctype/buying_settings/buying_settings.js index e496e9628d1..944bb61cfeb 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.js +++ b/erpnext/buying/doctype/buying_settings/buying_settings.js @@ -28,4 +28,4 @@ frappe.tour['Buying Settings'] = [ title: "Purchase Receipt Required for Purchase Invoice Creation", description: __("If this option is configured 'Yes', ERPNext will prevent you from creating a Purchase Invoice without creating a Purchase Receipt first. This configuration can be overridden for a particular supplier by enabling the 'Allow Purchase Invoice Creation Without Purchase Receipt' checkbox in the Supplier master.") } -]; \ No newline at end of file +]; diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index bb0ad60cabc..a55a0b7f9fc 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -565,6 +565,7 @@ "fieldname": "scan_barcode", "fieldtype": "Data", "label": "Scan Barcode", + "options": "Barcode", "show_days": 1, "show_seconds": 1 }, @@ -1378,7 +1379,7 @@ "idx": 105, "is_submittable": 1, "links": [], - "modified": "2021-05-30 15:17:53.663648", + "modified": "2021-08-17 20:16:12.737743", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order", diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index eaa502ff7f0..ca3bd90960c 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -447,10 +447,11 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions target.flags.ignore_permissions = ignore_permissions set_missing_values(source, target) #Get the advance paid Journal Entries in Purchase Invoice Advance - if target.get("allocate_advances_automatically"): target.set_advances() + target.set_payment_schedule() + def update_item(obj, target, source_parent): target.amount = flt(obj.amount) - flt(obj.billed_amt) target.base_amount = target.amount * flt(source_parent.conversion_rate) @@ -470,6 +471,7 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions "party_account_currency": "party_account_currency", "supplier_warehouse":"supplier_warehouse" }, + "field_no_map" : ["payment_terms_template"], "validation": { "docstatus": ["=", 1], } @@ -489,12 +491,6 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions }, } - if frappe.get_single("Accounts Settings").automatically_fetch_payment_terms == 1: - fields["Payment Schedule"] = { - "doctype": "Payment Schedule", - "add_if_empty": True - } - doc = get_mapped_doc("Purchase Order", source_name, fields, target_doc, postprocess, ignore_permissions=ignore_permissions) @@ -639,4 +635,4 @@ def add_items_in_ste(ste_doc, row, qty, po_details, batch_no=None): 'item_code': row.item_details['rm_item_code'], 'subcontracted_item': row.item_details['main_item_code'], 'serial_no': '\n'.join(row.serial_no) if row.serial_no else '' - }) \ No newline at end of file + }) diff --git a/erpnext/buying/doctype/purchase_order/regional/india.js b/erpnext/buying/doctype/purchase_order/regional/india.js index 42d3995907f..ef83f203e73 100644 --- a/erpnext/buying/doctype/purchase_order/regional/india.js +++ b/erpnext/buying/doctype/purchase_order/regional/india.js @@ -1,3 +1,3 @@ {% include "erpnext/regional/india/taxes.js" %} -erpnext.setup_auto_gst_taxation('Purchase Order'); \ No newline at end of file +erpnext.setup_auto_gst_taxation('Purchase Order'); diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index 8563b97ab74..fa174ba8fa8 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -484,6 +484,9 @@ class TestPurchaseOrder(unittest.TestCase): def test_make_purchase_invoice_with_terms(self): + from erpnext.selling.doctype.sales_order.test_sales_order import automatically_fetch_payment_terms, compare_payment_schedules + + automatically_fetch_payment_terms() po = create_purchase_order(do_not_save=True) self.assertRaises(frappe.ValidationError, make_pi_from_po, po.name) @@ -509,6 +512,7 @@ class TestPurchaseOrder(unittest.TestCase): self.assertEqual(getdate(pi.payment_schedule[0].due_date), getdate(po.transaction_date)) self.assertEqual(pi.payment_schedule[1].payment_amount, 2500.0) self.assertEqual(getdate(pi.payment_schedule[1].due_date), add_days(getdate(po.transaction_date), 30)) + automatically_fetch_payment_terms(enable=0) def test_subcontracting(self): po = create_purchase_order(item_code="_Test FG Item", is_subcontracted="Yes") @@ -632,14 +636,18 @@ class TestPurchaseOrder(unittest.TestCase): else: raise Exception - def test_terms_does_not_copy(self): - po = create_purchase_order() - - self.assertTrue(po.get('payment_schedule')) + def test_terms_are_not_copied_if_automatically_fetch_payment_terms_is_unchecked(self): + po = create_purchase_order(do_not_save=1) + po.payment_terms_template = '_Test Payment Term Template' + po.save() + po.submit() + frappe.db.set_value('Company', '_Test Company', 'payment_terms', '_Test Payment Term Template 1') pi = make_pi_from_po(po.name) + pi.save() - self.assertFalse(pi.get('payment_schedule')) + self.assertEqual(pi.get('payment_terms_template'), '_Test Payment Term Template 1') + frappe.db.set_value('Company', '_Test Company', 'payment_terms', '') def test_terms_copied(self): po = create_purchase_order(do_not_save=1) @@ -968,8 +976,27 @@ class TestPurchaseOrder(unittest.TestCase): # To test if the PO does NOT have a Blanket Order self.assertEqual(po_doc.items[0].blanket_order, None) + def test_payment_terms_are_fetched_when_creating_purchase_invoice(self): + from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_terms_template + from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice + from erpnext.selling.doctype.sales_order.test_sales_order import automatically_fetch_payment_terms, compare_payment_schedules + automatically_fetch_payment_terms() + po = create_purchase_order(qty=10, rate=100, do_not_save=1) + create_payment_terms_template() + po.payment_terms_template = 'Test Receivable Template' + po.submit() + + pi = make_purchase_invoice(qty=10, rate=100, do_not_save=1) + pi.items[0].purchase_order = po.name + pi.items[0].po_detail = po.items[0].name + pi.insert() + + # self.assertEqual(po.payment_terms_template, pi.payment_terms_template) + compare_payment_schedules(self, po, pi) + + automatically_fetch_payment_terms(enable=0) def make_pr_against_po(po, received_qty=0): pr = make_purchase_receipt(po) diff --git a/erpnext/buying/doctype/purchase_order/tests/test_purchase_order.js b/erpnext/buying/doctype/purchase_order/tests/test_purchase_order.js index 5d196874c98..012b0619cc9 100644 --- a/erpnext/buying/doctype/purchase_order/tests/test_purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/tests/test_purchase_order.js @@ -77,4 +77,4 @@ QUnit.test("test: purchase order", function(assert) { () => done() ]); -}); \ No newline at end of file +}); diff --git a/erpnext/buying/doctype/purchase_order/tests/test_purchase_order_get_items.js b/erpnext/buying/doctype/purchase_order/tests/test_purchase_order_get_items.js index 8c0c1443144..bc3d767f95d 100644 --- a/erpnext/buying/doctype/purchase_order/tests/test_purchase_order_get_items.js +++ b/erpnext/buying/doctype/purchase_order/tests/test_purchase_order_get_items.js @@ -58,4 +58,4 @@ QUnit.test("test: purchase order with get items", function(assert) { () => done() ]); -}); \ No newline at end of file +}); diff --git a/erpnext/buying/doctype/purchase_order/tests/test_purchase_order_with_discount_on_grand_total.js b/erpnext/buying/doctype/purchase_order/tests/test_purchase_order_with_discount_on_grand_total.js index 4e73ab8ef4f..83eb295010a 100644 --- a/erpnext/buying/doctype/purchase_order/tests/test_purchase_order_with_discount_on_grand_total.js +++ b/erpnext/buying/doctype/purchase_order/tests/test_purchase_order_with_discount_on_grand_total.js @@ -44,4 +44,4 @@ QUnit.test("test: purchase order with discount on grand total", function(assert) () => done() ]); -}); \ No newline at end of file +}); diff --git a/erpnext/buying/doctype/purchase_order/tests/test_purchase_order_with_item_wise_discount.js b/erpnext/buying/doctype/purchase_order/tests/test_purchase_order_with_item_wise_discount.js index 1e54e50dda9..a729dd9839f 100644 --- a/erpnext/buying/doctype/purchase_order/tests/test_purchase_order_with_item_wise_discount.js +++ b/erpnext/buying/doctype/purchase_order/tests/test_purchase_order_with_item_wise_discount.js @@ -41,4 +41,4 @@ QUnit.test("test: purchase order with item wise discount", function(assert) { () => done() ]); -}); \ No newline at end of file +}); diff --git a/erpnext/buying/doctype/purchase_order/tests/test_purchase_order_with_multi_uom.js b/erpnext/buying/doctype/purchase_order/tests/test_purchase_order_with_multi_uom.js index bf2dfeb37b7..b605e76ddf4 100644 --- a/erpnext/buying/doctype/purchase_order/tests/test_purchase_order_with_multi_uom.js +++ b/erpnext/buying/doctype/purchase_order/tests/test_purchase_order_with_multi_uom.js @@ -36,4 +36,4 @@ QUnit.test("test: purchase order with multi UOM", function(assert) { () => done() ]); -}); \ No newline at end of file +}); diff --git a/erpnext/buying/doctype/purchase_order/tests/test_purchase_order_with_shipping_rule.js b/erpnext/buying/doctype/purchase_order/tests/test_purchase_order_with_shipping_rule.js index 96775eb0075..c258756b2a1 100644 --- a/erpnext/buying/doctype/purchase_order/tests/test_purchase_order_with_shipping_rule.js +++ b/erpnext/buying/doctype/purchase_order/tests/test_purchase_order_with_shipping_rule.js @@ -40,4 +40,4 @@ QUnit.test("test: purchase order with shipping rule", function(assert) { () => frappe.timeout(0.3), () => done() ]); -}); \ No newline at end of file +}); diff --git a/erpnext/buying/doctype/purchase_order/tests/test_purchase_order_with_taxes_and_charges.js b/erpnext/buying/doctype/purchase_order/tests/test_purchase_order_with_taxes_and_charges.js index 39716ed560c..ccc383fd74e 100644 --- a/erpnext/buying/doctype/purchase_order/tests/test_purchase_order_with_taxes_and_charges.js +++ b/erpnext/buying/doctype/purchase_order/tests/test_purchase_order_with_taxes_and_charges.js @@ -41,4 +41,4 @@ QUnit.test("test: purchase order with taxes and charges", function(assert) { () => frappe.timeout(0.3), () => done() ]); -}); \ No newline at end of file +}); diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.py b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.py index 8bdcd47e028..b6e28b6c674 100644 --- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.py +++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.py @@ -10,4 +10,4 @@ class PurchaseOrderItem(Document): pass def on_doctype_update(): - frappe.db.add_index("Purchase Order Item", ["item_code", "warehouse"]) \ No newline at end of file + frappe.db.add_index("Purchase Order Item", ["item_code", "warehouse"]) diff --git a/erpnext/buying/doctype/purchase_order_item_supplied/purchase_order_item_supplied.py b/erpnext/buying/doctype/purchase_order_item_supplied/purchase_order_item_supplied.py index 6caffbda1f3..c85ca2fbafc 100644 --- a/erpnext/buying/doctype/purchase_order_item_supplied/purchase_order_item_supplied.py +++ b/erpnext/buying/doctype/purchase_order_item_supplied/purchase_order_item_supplied.py @@ -7,4 +7,4 @@ import frappe from frappe.model.document import Document class PurchaseOrderItemSupplied(Document): - pass \ No newline at end of file + pass diff --git a/erpnext/buying/doctype/purchase_receipt_item_supplied/purchase_receipt_item_supplied.py b/erpnext/buying/doctype/purchase_receipt_item_supplied/purchase_receipt_item_supplied.py index 1a76f0ee7db..00c93ed1ea3 100644 --- a/erpnext/buying/doctype/purchase_receipt_item_supplied/purchase_receipt_item_supplied.py +++ b/erpnext/buying/doctype/purchase_receipt_item_supplied/purchase_receipt_item_supplied.py @@ -7,4 +7,4 @@ import frappe from frappe.model.document import Document class PurchaseReceiptItemSupplied(Document): - pass \ No newline at end of file + pass diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py index a4ce84e1cf9..8ed6c9e2a6d 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py @@ -425,4 +425,4 @@ def get_rfq_containing_supplier(doctype, txt, searchfield, start, page_len, filt .format(filters.get("supplier"), filters.get("company"), conditions), {"page_len": page_len, "start": start}, as_dict=1) - return rfq_data \ No newline at end of file + return rfq_data diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation_dashboard.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation_dashboard.py index 6efbc782252..751336dc4c6 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation_dashboard.py +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation_dashboard.py @@ -10,4 +10,4 @@ def get_data(): 'items': ['Supplier Quotation'] }, ] - } \ No newline at end of file + } diff --git a/erpnext/buying/doctype/request_for_quotation/tests/test_request_for_quotation.js b/erpnext/buying/doctype/request_for_quotation/tests/test_request_for_quotation.js index 1fcfe75bb03..75f85f86d1d 100644 --- a/erpnext/buying/doctype/request_for_quotation/tests/test_request_for_quotation.js +++ b/erpnext/buying/doctype/request_for_quotation/tests/test_request_for_quotation.js @@ -73,4 +73,4 @@ QUnit.test("test: request_for_quotation", function(assert) { () => frappe.click_button('Close'), () => done() ]); -}); \ No newline at end of file +}); diff --git a/erpnext/buying/doctype/request_for_quotation/tests/test_request_for_quotation_for_status.js b/erpnext/buying/doctype/request_for_quotation/tests/test_request_for_quotation_for_status.js index 2e1652de733..f06c3f34c44 100644 --- a/erpnext/buying/doctype/request_for_quotation/tests/test_request_for_quotation_for_status.js +++ b/erpnext/buying/doctype/request_for_quotation/tests/test_request_for_quotation_for_status.js @@ -125,4 +125,4 @@ QUnit.test("Test: Request for Quotation", function (assert) { }, () => done() ]); -}); \ No newline at end of file +}); diff --git a/erpnext/buying/doctype/supplier/regional/india.js b/erpnext/buying/doctype/supplier/regional/india.js index bd710e0df71..5f49a47e75e 100644 --- a/erpnext/buying/doctype/supplier/regional/india.js +++ b/erpnext/buying/doctype/supplier/regional/india.js @@ -1,3 +1,3 @@ {% include "erpnext/regional/india/party.js" %} -erpnext.setup_gst_reminder_button('Supplier'); \ No newline at end of file +erpnext.setup_gst_reminder_button('Supplier'); diff --git a/erpnext/buying/doctype/supplier/supplier.js b/erpnext/buying/doctype/supplier/supplier.js index 4ddc458175b..7ee91961ca5 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' } @@ -60,10 +79,47 @@ frappe.ui.form.on("Supplier", { erpnext.utils.make_pricing_rule(frm.doc.doctype, frm.doc.name); }, __('Create')); + frm.add_custom_button(__('Get Supplier Group Details'), function () { + frm.trigger("get_supplier_group_details"); + }, __('Actions')); + // indicators erpnext.utils.set_party_dashboard_indicators(frm); } }, + get_supplier_group_details: function(frm) { + frappe.call({ + method: "get_supplier_group_details", + doc: frm.doc, + callback: function() { + frm.refresh(); + } + }); + }, + + 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) { diff --git a/erpnext/buying/doctype/supplier/supplier.json b/erpnext/buying/doctype/supplier/supplier.json index 38b8dfdf48d..c7a5db59941 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 edeb135d951..c9750caa65a 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: @@ -51,6 +56,23 @@ class Supplier(TransactionBase): validate_party_accounts(self) self.validate_internal_supplier() + @frappe.whitelist() + def get_supplier_group_details(self): + doc = frappe.get_doc('Supplier Group', self.supplier_group) + self.payment_terms = "" + self.accounts = [] + + if doc.accounts: + for account in doc.accounts: + child = self.append('accounts') + child.company = account.company + child.account = account.account + + if doc.payment_terms: + self.payment_terms = doc.payment_terms + + self.save() + def validate_internal_supplier(self): internal_supplier = frappe.db.get_value("Supplier", {"is_internal_supplier": 1, "represents_company": self.represents_company, "name": ("!=", self.name)}, "name") @@ -59,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): @@ -86,4 +140,22 @@ class Supplier(TransactionBase): create_contact(supplier, 'Supplier', doc.name, args.get('supplier_email_' + str(i))) except frappe.NameError: - pass \ No newline at end of file + 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/buying/doctype/supplier/test_supplier.js b/erpnext/buying/doctype/supplier/test_supplier.js index bf7c192c91c..eaa4d0989d2 100644 --- a/erpnext/buying/doctype/supplier/test_supplier.js +++ b/erpnext/buying/doctype/supplier/test_supplier.js @@ -74,4 +74,4 @@ QUnit.test("test: supplier", function(assert) { }, () => done() ]); -}); \ No newline at end of file +}); diff --git a/erpnext/buying/doctype/supplier/test_supplier.py b/erpnext/buying/doctype/supplier/test_supplier.py index f9c8d35518d..89804662700 100644 --- a/erpnext/buying/doctype/supplier/test_supplier.py +++ b/erpnext/buying/doctype/supplier/test_supplier.py @@ -13,6 +13,30 @@ test_records = frappe.get_test_records('Supplier') class TestSupplier(unittest.TestCase): + def test_get_supplier_group_details(self): + doc = frappe.new_doc("Supplier Group") + doc.supplier_group_name = "_Testing Supplier Group" + doc.payment_terms = "_Test Payment Term Template 3" + doc.accounts = [] + test_account_details = { + "company": "_Test Company", + "account": "Creditors - _TC", + } + doc.append("accounts", test_account_details) + doc.save() + s_doc = frappe.new_doc("Supplier") + s_doc.supplier_name = "Testing Supplier" + s_doc.supplier_group = "_Testing Supplier Group" + s_doc.payment_terms = "" + s_doc.accounts = [] + s_doc.insert() + s_doc.get_supplier_group_details() + self.assertEqual(s_doc.payment_terms, "_Test Payment Term Template 3") + self.assertEqual(s_doc.accounts[0].company, "_Test Company") + self.assertEqual(s_doc.accounts[0].account, "Creditors - _TC") + s_doc.delete() + doc.delete() + def test_supplier_default_payment_terms(self): # Payment Term based on Days after invoice date frappe.db.set_value( @@ -136,4 +160,4 @@ def create_supplier(**args): return doc except frappe.DuplicateEntryError: - return frappe.get_doc("Supplier", args.supplier_name) \ No newline at end of file + return frappe.get_doc("Supplier", args.supplier_name) diff --git a/erpnext/buying/doctype/supplier_item_group/supplier_item_group.py b/erpnext/buying/doctype/supplier_item_group/supplier_item_group.py index 3a2e5d6dcef..4473ddea28e 100644 --- a/erpnext/buying/doctype/supplier_item_group/supplier_item_group.py +++ b/erpnext/buying/doctype/supplier_item_group/supplier_item_group.py @@ -15,4 +15,4 @@ class SupplierItemGroup(Document): 'item_group': self.item_group }) if exists: - frappe.throw(_("Item Group has already been linked to this supplier.")) \ No newline at end of file + frappe.throw(_("Item Group has already been linked to this supplier.")) diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py index 6a4c02c075c..25e4e2a4dcf 100644 --- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py +++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py @@ -166,4 +166,4 @@ def set_expired_status(): `tabSupplier Quotation` SET `status` = 'Expired' WHERE `status` not in ('Cancelled', 'Stopped') AND `valid_till` < %s - """, (nowdate())) \ No newline at end of file + """, (nowdate())) diff --git a/erpnext/buying/doctype/supplier_quotation/tests/test_supplier_quotation.js b/erpnext/buying/doctype/supplier_quotation/tests/test_supplier_quotation.js index 2d2b29cb916..20fb43026ab 100644 --- a/erpnext/buying/doctype/supplier_quotation/tests/test_supplier_quotation.js +++ b/erpnext/buying/doctype/supplier_quotation/tests/test_supplier_quotation.js @@ -71,4 +71,4 @@ QUnit.test("test: supplier quotation", function(assert) { () => done() ]); -}); \ No newline at end of file +}); diff --git a/erpnext/buying/doctype/supplier_quotation/tests/test_supplier_quotation_for_item_wise_discount.js b/erpnext/buying/doctype/supplier_quotation/tests/test_supplier_quotation_for_item_wise_discount.js index b151824ba68..0a51565b08e 100644 --- a/erpnext/buying/doctype/supplier_quotation/tests/test_supplier_quotation_for_item_wise_discount.js +++ b/erpnext/buying/doctype/supplier_quotation/tests/test_supplier_quotation_for_item_wise_discount.js @@ -31,4 +31,4 @@ QUnit.test("test: supplier quotation with item wise discount", function(assert){ () => frappe.timeout(0.3), () => done() ]); -}); \ No newline at end of file +}); diff --git a/erpnext/buying/doctype/supplier_quotation/tests/test_supplier_quotation_for_taxes_and_charges.js b/erpnext/buying/doctype/supplier_quotation/tests/test_supplier_quotation_for_taxes_and_charges.js index e37731eb579..7ea3e6079cd 100644 --- a/erpnext/buying/doctype/supplier_quotation/tests/test_supplier_quotation_for_taxes_and_charges.js +++ b/erpnext/buying/doctype/supplier_quotation/tests/test_supplier_quotation_for_taxes_and_charges.js @@ -34,4 +34,4 @@ QUnit.test("test: supplier quotation with taxes and charges", function(assert) { () => frappe.timeout(0.3), () => done() ]); -}); \ No newline at end of file +}); diff --git a/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.js b/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.js index 5f5f54b79f5..b4cd852c32f 100644 --- a/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.js +++ b/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.js @@ -93,5 +93,3 @@ var loadAllStandings = function(frm) { } }); }; - - diff --git a/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard_dashboard.py b/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard_dashboard.py index 3d2305e2853..8e5cce5696b 100644 --- a/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard_dashboard.py +++ b/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard_dashboard.py @@ -13,4 +13,4 @@ def get_data(): 'items': ['Supplier Scorecard Period'] } ] - } \ No newline at end of file + } diff --git a/erpnext/buying/doctype/supplier_scorecard/test_supplier_scorecard.py b/erpnext/buying/doctype/supplier_scorecard/test_supplier_scorecard.py index 25282405492..a5f05ea5258 100644 --- a/erpnext/buying/doctype/supplier_scorecard/test_supplier_scorecard.py +++ b/erpnext/buying/doctype/supplier_scorecard/test_supplier_scorecard.py @@ -128,4 +128,3 @@ valid_scorecard = [ "weighting_function":"{total_score} * max( 0, min ( 1 , (12 - {period_number}) / 12) )" } ] - diff --git a/erpnext/buying/doctype/supplier_scorecard_criteria/test_supplier_scorecard_criteria.py b/erpnext/buying/doctype/supplier_scorecard_criteria/test_supplier_scorecard_criteria.py index 4eef4b4e03e..3babfc8cab3 100644 --- a/erpnext/buying/doctype/supplier_scorecard_criteria/test_supplier_scorecard_criteria.py +++ b/erpnext/buying/doctype/supplier_scorecard_criteria/test_supplier_scorecard_criteria.py @@ -72,4 +72,4 @@ test_bad_criteria = [ "criteria_name":"Fake Criteria 3", "max_score":100.0 }, -] \ No newline at end of file +] diff --git a/erpnext/buying/doctype/supplier_scorecard_period/supplier_scorecard_period.py b/erpnext/buying/doctype/supplier_scorecard_period/supplier_scorecard_period.py index 9938710e6e6..cc345e96bb8 100644 --- a/erpnext/buying/doctype/supplier_scorecard_period/supplier_scorecard_period.py +++ b/erpnext/buying/doctype/supplier_scorecard_period/supplier_scorecard_period.py @@ -109,4 +109,3 @@ def make_supplier_scorecard(source_name, target_doc=None): }, target_doc, post_process, ignore_permissions=True) return doc - diff --git a/erpnext/buying/doctype/supplier_scorecard_standing/supplier_scorecard_standing.py b/erpnext/buying/doctype/supplier_scorecard_standing/supplier_scorecard_standing.py index 1ba5d06c536..678855a457b 100644 --- a/erpnext/buying/doctype/supplier_scorecard_standing/supplier_scorecard_standing.py +++ b/erpnext/buying/doctype/supplier_scorecard_standing/supplier_scorecard_standing.py @@ -26,4 +26,4 @@ def get_standings_list(): `tabSupplier Scorecard Standing` scs""", {}, as_dict=1) - return standings \ No newline at end of file + return standings diff --git a/erpnext/buying/doctype/supplier_scorecard_variable/supplier_scorecard_variable.py b/erpnext/buying/doctype/supplier_scorecard_variable/supplier_scorecard_variable.py index 37fdc5724f5..89a6459bbab 100644 --- a/erpnext/buying/doctype/supplier_scorecard_variable/supplier_scorecard_variable.py +++ b/erpnext/buying/doctype/supplier_scorecard_variable/supplier_scorecard_variable.py @@ -493,4 +493,4 @@ def get_rfq_response_days(scorecard): total_sq_days = 0 - return total_sq_days \ No newline at end of file + return total_sq_days diff --git a/erpnext/buying/doctype/supplier_scorecard_variable/test_supplier_scorecard_variable.py b/erpnext/buying/doctype/supplier_scorecard_variable/test_supplier_scorecard_variable.py index fe6dde50489..14b87105e66 100644 --- a/erpnext/buying/doctype/supplier_scorecard_variable/test_supplier_scorecard_variable.py +++ b/erpnext/buying/doctype/supplier_scorecard_variable/test_supplier_scorecard_variable.py @@ -54,4 +54,4 @@ test_bad_variables = [ "variable_label":"Fake Variable 1", "path":"get_fake_variable1" }, -] \ No newline at end of file +] diff --git a/erpnext/buying/print_format/drop_shipping_format/drop_shipping_format.json b/erpnext/buying/print_format/drop_shipping_format/drop_shipping_format.json index 0c7cad6e942..2e2a04826a3 100644 --- a/erpnext/buying/print_format/drop_shipping_format/drop_shipping_format.json +++ b/erpnext/buying/print_format/drop_shipping_format/drop_shipping_format.json @@ -13,6 +13,6 @@ "name": "Drop Shipping Format", "owner": "Administrator", "print_format_builder": 0, - "print_format_type": "Server", + "print_format_type": "Jinja", "standard": "Yes" } \ No newline at end of file diff --git a/erpnext/buying/report/procurement_tracker/procurement_tracker.py b/erpnext/buying/report/procurement_tracker/procurement_tracker.py index beeca091c8a..99bcbe633cc 100644 --- a/erpnext/buying/report/procurement_tracker/procurement_tracker.py +++ b/erpnext/buying/report/procurement_tracker/procurement_tracker.py @@ -296,4 +296,4 @@ def get_po_entries(conditions): {conditions} GROUP BY parent.name, child.item_code - """.format(conditions=conditions), as_dict=1) #nosec \ No newline at end of file + """.format(conditions=conditions), as_dict=1) #nosec diff --git a/erpnext/buying/report/procurement_tracker/test_procurement_tracker.py b/erpnext/buying/report/procurement_tracker/test_procurement_tracker.py index 44ab767c0a9..c36083f2aff 100644 --- a/erpnext/buying/report/procurement_tracker/test_procurement_tracker.py +++ b/erpnext/buying/report/procurement_tracker/test_procurement_tracker.py @@ -68,4 +68,4 @@ class TestProcurementTracker(unittest.TestCase): "actual_delivery_date": date_obj } - return expected_data \ No newline at end of file + return expected_data diff --git a/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py index 89be62231b9..bda172769a9 100644 --- a/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py +++ b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py @@ -268,4 +268,3 @@ def get_columns(filters): ]) return columns - diff --git a/erpnext/buying/report/purchase_order_trends/purchase_order_trends.js b/erpnext/buying/report/purchase_order_trends/purchase_order_trends.js index 83d25d80ba2..90919dcc6a3 100644 --- a/erpnext/buying/report/purchase_order_trends/purchase_order_trends.js +++ b/erpnext/buying/report/purchase_order_trends/purchase_order_trends.js @@ -5,4 +5,4 @@ frappe.require("assets/erpnext/js/purchase_trends_filters.js", function() { frappe.query_reports["Purchase Order Trends"] = { filters: erpnext.get_purchase_trends_filters() } -}); \ No newline at end of file +}); diff --git a/erpnext/buying/report/purchase_order_trends/purchase_order_trends.py b/erpnext/buying/report/purchase_order_trends/purchase_order_trends.py index 1ed6cad6b46..095a44319d6 100644 --- a/erpnext/buying/report/purchase_order_trends/purchase_order_trends.py +++ b/erpnext/buying/report/purchase_order_trends/purchase_order_trends.py @@ -55,4 +55,4 @@ def get_chart_data(data, conditions, filters): "lineOptions": { "regionFill": 1 } - } \ No newline at end of file + } diff --git a/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py index 0c0d4f0531d..9a45972837b 100644 --- a/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py +++ b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py @@ -149,4 +149,4 @@ def get_columns(): "fieldtype": "Float", "width": 110 } - ] \ No newline at end of file + ] diff --git a/erpnext/buying/report/subcontracted_item_to_be_received/test_subcontracted_item_to_be_received.py b/erpnext/buying/report/subcontracted_item_to_be_received/test_subcontracted_item_to_be_received.py index d8de701bf6e..cb304a1fdab 100644 --- a/erpnext/buying/report/subcontracted_item_to_be_received/test_subcontracted_item_to_be_received.py +++ b/erpnext/buying/report/subcontracted_item_to_be_received/test_subcontracted_item_to_be_received.py @@ -33,4 +33,4 @@ def make_purchase_receipt_against_po(po, quantity=5): pr.items[0].qty = quantity pr.supplier_warehouse = '_Test Warehouse 1 - _TC' pr.insert() - pr.submit() \ No newline at end of file + pr.submit() diff --git a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.py b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.py index 68426abbb04..96cacb6f1b5 100644 --- a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.py +++ b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.py @@ -94,4 +94,4 @@ def get_po_items_to_supply(filters): ["Purchase Order", "transaction_date", ">=", filters.from_date], ["Purchase Order", "docstatus", "=", 1] ] - ) \ No newline at end of file + ) diff --git a/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.html b/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.html index 098214d741c..015b31c2064 100644 --- a/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.html +++ b/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.html @@ -129,4 +129,4 @@ -

Printed On {%= frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string()) %}

\ No newline at end of file +

Printed On {%= frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string()) %}

diff --git a/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.js b/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.js index 80e521a8bfa..7a8d08dd22d 100644 --- a/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.js +++ b/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.js @@ -174,4 +174,4 @@ frappe.query_reports["Supplier Quotation Comparison"] = { }); dialog.show(); } -} \ No newline at end of file +} diff --git a/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.py b/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.py index 2b371915f32..a5a3105a847 100644 --- a/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.py +++ b/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.py @@ -263,4 +263,4 @@ def get_message():    Expires today / Already Expired - """ \ No newline at end of file + """ diff --git a/erpnext/buying/utils.py b/erpnext/buying/utils.py index a73cb0d62ec..17928634e78 100644 --- a/erpnext/buying/utils.py +++ b/erpnext/buying/utils.py @@ -102,4 +102,3 @@ def get_linked_material_requests(items): mr_list.append(material_request) return mr_list - diff --git a/erpnext/commands/__init__.py b/erpnext/commands/__init__.py index a991cf9881e..2276c738fbe 100644 --- a/erpnext/commands/__init__.py +++ b/erpnext/commands/__init__.py @@ -46,4 +46,4 @@ def make_demo(context, site, domain='Manufacturing', days=100, commands = [ make_demo -] \ No newline at end of file +] diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 1c086e9edcd..fbe060d67c2 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 @@ -124,6 +124,8 @@ class AccountsController(TransactionBase): if cint(self.allocate_advances_automatically) and not cint(self.get(pos_check_field)): self.set_advances() + self.set_advance_gain_or_loss() + if self.is_return: self.validate_qty() else: @@ -162,7 +164,8 @@ class AccountsController(TransactionBase): self.set_due_date() self.set_payment_schedule() self.validate_payment_schedule_amount() - self.validate_due_date() + if not self.get('ignore_default_payment_terms_template'): + self.validate_due_date() self.validate_advance_entries() def validate_non_invoice_documents_schedule(self): @@ -584,15 +587,18 @@ class AccountsController(TransactionBase): allocated_amount = min(amount - advance_allocated, d.amount) advance_allocated += flt(allocated_amount) - self.append("advances", { + advance_row = { "doctype": self.doctype + " Advance", "reference_type": d.reference_type, "reference_name": d.reference_name, "reference_row": d.reference_row, "remarks": d.remarks, "advance_amount": flt(d.amount), - "allocated_amount": allocated_amount - }) + "allocated_amount": allocated_amount, + "ref_exchange_rate": flt(d.exchange_rate) # exchange_rate of advance entry + } + + self.append("advances", advance_row) def get_advance_entries(self, include_unallocated=True): if self.doctype == "Sales Invoice": @@ -650,6 +656,71 @@ class AccountsController(TransactionBase): "Payment Entry {0} is linked against Order {1}, check if it should be pulled as advance in this invoice.") .format(d.reference_name, d.against_order)) + def set_advance_gain_or_loss(self): + if not self.get("advances"): + return + + for d in self.get("advances"): + advance_exchange_rate = d.ref_exchange_rate + if (d.allocated_amount and self.conversion_rate != 1 + and self.conversion_rate != advance_exchange_rate): + + base_allocated_amount_in_ref_rate = advance_exchange_rate * d.allocated_amount + base_allocated_amount_in_inv_rate = self.conversion_rate * d.allocated_amount + difference = base_allocated_amount_in_ref_rate - base_allocated_amount_in_inv_rate + + d.exchange_gain_loss = difference + + def make_exchange_gain_loss_gl_entries(self, gl_entries): + if self.get('doctype') in ['Purchase Invoice', 'Sales Invoice']: + for d in self.get("advances"): + if d.exchange_gain_loss: + is_purchase_invoice = self.get('doctype') == 'Purchase Invoice' + party = self.supplier if is_purchase_invoice else self.customer + party_account = self.credit_to if is_purchase_invoice else self.debit_to + party_type = "Supplier" if is_purchase_invoice else "Customer" + + gain_loss_account = frappe.db.get_value('Company', self.company, 'exchange_gain_loss_account') + if not gain_loss_account: + frappe.throw(_("Please set Default Exchange Gain/Loss Account in Company {}") + .format(self.get('company'))) + account_currency = get_account_currency(gain_loss_account) + if account_currency != self.company_currency: + frappe.throw(_("Currency for {0} must be {1}").format(gain_loss_account, self.company_currency)) + + # for purchase + dr_or_cr = 'debit' if d.exchange_gain_loss > 0 else 'credit' + if not is_purchase_invoice: + # just reverse for sales? + dr_or_cr = 'debit' if dr_or_cr == 'credit' else 'credit' + + gl_entries.append( + self.get_gl_dict({ + "account": gain_loss_account, + "account_currency": account_currency, + "against": party, + dr_or_cr + "_in_account_currency": abs(d.exchange_gain_loss), + dr_or_cr: abs(d.exchange_gain_loss), + "cost_center": self.cost_center, + "project": self.project + }, item=d) + ) + + dr_or_cr = 'debit' if dr_or_cr == 'credit' else 'credit' + + gl_entries.append( + self.get_gl_dict({ + "account": party_account, + "party_type": party_type, + "party": party, + "against": gain_loss_account, + dr_or_cr + "_in_account_currency": flt(abs(d.exchange_gain_loss) / self.conversion_rate), + dr_or_cr: abs(d.exchange_gain_loss), + "cost_center": self.cost_center, + "project": self.project + }, self.party_account_currency, item=self) + ) + def update_against_document_in_jv(self): """ Links invoice and advance voucher: @@ -690,7 +761,9 @@ class AccountsController(TransactionBase): if self.party_account_currency != self.company_currency else 1), 'grand_total': (self.base_grand_total if self.party_account_currency == self.company_currency else self.grand_total), - 'outstanding_amount': self.outstanding_amount + 'outstanding_amount': self.outstanding_amount, + 'difference_account': frappe.db.get_value('Company', self.company, 'exchange_gain_loss_account'), + 'exchange_gain_loss': flt(d.get('exchange_gain_loss')) }) lst.append(args) @@ -741,6 +814,89 @@ class AccountsController(TransactionBase): tax_map[tax.account_head] -= allocated_amount allocated_tax_map[tax.account_head] -= allocated_amount + def get_amount_and_base_amount(self, item, enable_discount_accounting): + amount = item.net_amount + base_amount = item.base_net_amount + + if enable_discount_accounting and self.get('discount_amount') and self.get('additional_discount_account'): + amount = item.amount + base_amount = item.base_amount + + return amount, base_amount + + def get_tax_amounts(self, tax, enable_discount_accounting): + amount = tax.tax_amount_after_discount_amount + base_amount = tax.base_tax_amount_after_discount_amount + + if enable_discount_accounting and self.get('discount_amount') and self.get('additional_discount_account') \ + and self.get('apply_discount_on') == 'Grand Total': + amount = tax.tax_amount + base_amount = tax.base_tax_amount + + return amount, base_amount + + def make_discount_gl_entries(self, gl_entries): + enable_discount_accounting = cint(frappe.db.get_single_value('Accounts Settings', 'enable_discount_accounting')) + + if enable_discount_accounting: + if self.doctype == "Purchase Invoice": + dr_or_cr = "credit" + rev_dr_cr = "debit" + supplier_or_customer = self.supplier + + else: + dr_or_cr = "debit" + rev_dr_cr = "credit" + supplier_or_customer = self.customer + + for item in self.get("items"): + if item.get('discount_amount') and item.get('discount_account'): + discount_amount = item.discount_amount * item.qty + if self.doctype == "Purchase Invoice": + income_or_expense_account = (item.expense_account + if (not item.enable_deferred_expense or self.is_return) + else item.deferred_expense_account) + else: + income_or_expense_account = (item.income_account + if (not item.enable_deferred_revenue or self.is_return) + else item.deferred_revenue_account) + + account_currency = get_account_currency(item.discount_account) + gl_entries.append( + self.get_gl_dict({ + "account": item.discount_account, + "against": supplier_or_customer, + dr_or_cr: flt(discount_amount, item.precision('discount_amount')), + dr_or_cr + "_in_account_currency": flt(discount_amount * self.get('conversion_rate'), + item.precision('discount_amount')), + "cost_center": item.cost_center, + "project": item.project + }, account_currency, item=item) + ) + + account_currency = get_account_currency(income_or_expense_account) + gl_entries.append( + self.get_gl_dict({ + "account": income_or_expense_account, + "against": supplier_or_customer, + rev_dr_cr: flt(discount_amount, item.precision('discount_amount')), + rev_dr_cr + "_in_account_currency": flt(discount_amount * self.get('conversion_rate'), + item.precision('discount_amount')), + "cost_center": item.cost_center, + "project": item.project or self.project + }, account_currency, item=item) + ) + + if self.get('discount_amount') and self.get('additional_discount_account'): + gl_entries.append( + self.get_gl_dict({ + "account": self.additional_discount_account, + "against": supplier_or_customer, + dr_or_cr: self.discount_amount, + "cost_center": self.cost_center + }, item=self) + ) + def allocate_advance_taxes(self, gl_entries): tax_map = self.get_tax_map() for pe in self.get("advances"): @@ -751,11 +907,11 @@ class AccountsController(TransactionBase): account_currency = get_account_currency(tax.account_head) if self.doctype == "Purchase Invoice": - dr_or_cr = "credit" if tax.add_deduct_tax == "Add" else "debit" - rev_dr_cr = "debit" if tax.add_deduct_tax == "Add" else "credit" - else: dr_or_cr = "debit" if tax.add_deduct_tax == "Add" else "credit" rev_dr_cr = "credit" if tax.add_deduct_tax == "Add" else "debit" + else: + dr_or_cr = "credit" if tax.add_deduct_tax == "Add" else "debit" + rev_dr_cr = "debit" if tax.add_deduct_tax == "Add" else "credit" party = self.supplier if self.doctype == "Purchase Invoice" else self.customer unallocated_amount = tax.tax_amount - tax.allocated_amount @@ -1024,6 +1180,8 @@ class AccountsController(TransactionBase): if self.doctype in ("Sales Invoice", "Purchase Invoice"): base_grand_total = base_grand_total - flt(self.base_write_off_amount) grand_total = grand_total - flt(self.write_off_amount) + po_or_so, doctype, fieldname = self.get_order_details() + automatically_fetch_payment_terms = cint(frappe.db.get_single_value('Accounts Settings', 'automatically_fetch_payment_terms')) if self.get("total_advance"): if party_account_currency == self.company_currency: @@ -1034,19 +1192,86 @@ class AccountsController(TransactionBase): base_grand_total = flt(grand_total * self.get("conversion_rate"), self.precision("base_grand_total")) if not self.get("payment_schedule"): - if self.get("payment_terms_template"): + if self.doctype in ["Sales Invoice", "Purchase Invoice"] and automatically_fetch_payment_terms \ + and self.linked_order_has_payment_terms(po_or_so, fieldname, doctype): + self.fetch_payment_terms_from_order(po_or_so, doctype) + if self.get('payment_terms_template'): + self.ignore_default_payment_terms_template = 1 + elif self.get("payment_terms_template"): data = get_payment_terms(self.payment_terms_template, posting_date, grand_total, base_grand_total) for item in data: self.append("payment_schedule", item) - else: + elif self.doctype not in ["Purchase Receipt"]: data = dict(due_date=due_date, invoice_portion=100, payment_amount=grand_total, base_payment_amount=base_grand_total) self.append("payment_schedule", data) + + for d in self.get("payment_schedule"): + if d.invoice_portion: + d.payment_amount = flt(grand_total * flt(d.invoice_portion / 100), d.precision('payment_amount')) + 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(d.payment_amount * self.get("conversion_rate"), d.precision('base_payment_amount')) + + + def get_order_details(self): + if self.doctype == "Sales Invoice": + po_or_so = self.get('items')[0].get('sales_order') + po_or_so_doctype = "Sales Order" + po_or_so_doctype_name = "sales_order" + else: - for d in self.get("payment_schedule"): - if d.invoice_portion: - d.payment_amount = flt(grand_total * flt(d.invoice_portion / 100), d.precision('payment_amount')) - d.base_payment_amount = flt(base_grand_total * flt(d.invoice_portion / 100), d.precision('payment_amount')) - d.outstanding = d.payment_amount + po_or_so = self.get('items')[0].get('purchase_order') + po_or_so_doctype = "Purchase Order" + po_or_so_doctype_name = "purchase_order" + + return po_or_so, po_or_so_doctype, po_or_so_doctype_name + + def linked_order_has_payment_terms(self, po_or_so, fieldname, doctype): + if po_or_so and self.all_items_have_same_po_or_so(po_or_so, fieldname): + if self.linked_order_has_payment_terms_template(po_or_so, doctype): + return True + elif self.linked_order_has_payment_schedule(po_or_so): + return True + + return False + + def all_items_have_same_po_or_so(self, po_or_so, fieldname): + for item in self.get('items'): + if item.get(fieldname) != po_or_so: + return False + + return True + + def linked_order_has_payment_terms_template(self, po_or_so, doctype): + return frappe.get_value(doctype, po_or_so, 'payment_terms_template') + + def linked_order_has_payment_schedule(self, po_or_so): + return frappe.get_all('Payment Schedule', filters={'parent': po_or_so}) + + def fetch_payment_terms_from_order(self, po_or_so, po_or_so_doctype): + """ + Fetch Payment Terms from Purchase/Sales Order on creating a new Purchase/Sales Invoice. + """ + po_or_so = frappe.get_cached_doc(po_or_so_doctype, po_or_so) + + self.payment_schedule = [] + self.payment_terms_template = po_or_so.payment_terms_template + + for schedule in po_or_so.payment_schedule: + payment_schedule = { + 'payment_term': schedule.payment_term, + 'due_date': schedule.due_date, + 'invoice_portion': schedule.invoice_portion, + 'mode_of_payment': schedule.mode_of_payment, + 'description': schedule.description + } + + if schedule.discount_type == 'Percentage': + payment_schedule['discount_type'] = schedule.discount_type + payment_schedule['discount'] = schedule.discount + + self.append("payment_schedule", payment_schedule) def set_due_date(self): due_dates = [d.due_date for d in self.get("payment_schedule") if d.due_date] @@ -1143,6 +1368,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) @@ -1213,6 +1499,27 @@ def validate_taxes_and_charges(tax): tax.rate = None +def validate_account_head(tax, doc): + company = frappe.get_cached_value('Account', + tax.account_head, 'company') + + if company != doc.company: + frappe.throw(_('Row {0}: Account {1} does not belong to Company {2}') + .format(tax.idx, frappe.bold(tax.account_head), frappe.bold(doc.company)), title=_('Invalid Account')) + + +def validate_cost_center(tax, doc): + if not tax.cost_center: + return + + company = frappe.get_cached_value('Cost Center', + tax.cost_center, 'company') + + if company != doc.company: + frappe.throw(_('Row {0}: Cost Center {1} does not belong to Company {2}') + .format(tax.idx, frappe.bold(tax.cost_center), frappe.bold(doc.company)), title=_('Invalid Cost Center')) + + def validate_inclusive_tax(tax, doc): def _on_previous_row_error(row_range): throw(_("To include tax in row {0} in Item rate, taxes in rows {1} must also be included").format(tax.idx, row_range)) @@ -1289,6 +1596,8 @@ def get_advance_payment_entries(party_type, party, party_account, order_doctype, 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" + exchange_rate_field = "source_exchange_rate" if payment_type == "Receive" else "target_exchange_rate" + payment_entries_against_order, unallocated_payment_entries = [], [] limit_cond = "limit %s" % limit if limit else "" @@ -1305,27 +1614,28 @@ def get_advance_payment_entries(party_type, party, party_account, order_doctype, "Payment Entry" as reference_type, t1.name as reference_name, t1.remarks, t2.allocated_amount as amount, t2.name as reference_row, t2.reference_name as against_order, t1.posting_date, - t1.{0} as currency + t1.{0} as currency, t1.{4} as exchange_rate from `tabPayment Entry` t1, `tabPayment Entry Reference` t2 where t1.name = t2.parent and t1.{1} = %s and t1.payment_type = %s and t1.party_type = %s and t1.party = %s and t1.docstatus = 1 and t2.reference_doctype = %s {2} order by t1.posting_date {3} - """.format(currency_field, party_account_field, reference_condition, limit_cond), + """.format(currency_field, party_account_field, reference_condition, limit_cond, exchange_rate_field), [party_account, payment_type, party_type, party, order_doctype] + order_list, as_dict=1) if include_unallocated: unallocated_payment_entries = frappe.db.sql(""" select "Payment Entry" as reference_type, name as reference_name, - remarks, unallocated_amount as amount + remarks, unallocated_amount as amount, {2} as exchange_rate from `tabPayment Entry` where {0} = %s and party_type = %s and party = %s and payment_type = %s and docstatus = 1 and unallocated_amount > 0 order by posting_date {1} - """.format(party_account_field, limit_cond), (party_account, party_type, party, payment_type), as_dict=1) + """.format(party_account_field, limit_cond, exchange_rate_field), + (party_account, party_type, party, payment_type), as_dict=1) return list(payment_entries_against_order) + list(unallocated_payment_entries) @@ -1429,7 +1739,7 @@ def set_child_tax_template_and_map(item, child_item, parent_doc): if child_item.get("item_tax_template"): child_item.item_tax_rate = get_item_tax_map(parent_doc.get('company'), child_item.item_tax_template, as_json=True) -def add_taxes_from_tax_template(child_item, parent_doc): +def add_taxes_from_tax_template(child_item, parent_doc, db_insert=True): add_taxes_from_item_tax_template = frappe.db.get_single_value("Accounts Settings", "add_taxes_from_item_tax_template") if child_item.get("item_tax_rate") and add_taxes_from_item_tax_template: @@ -1452,7 +1762,8 @@ def add_taxes_from_tax_template(child_item, parent_doc): "category" : "Total", "add_deduct_tax" : "Add" }) - tax_row.db_insert() + if db_insert: + tax_row.db_insert() def set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child_docname, trans_item): """ @@ -1592,6 +1903,11 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil for d in data: new_child_flag = False + + if not d.get("item_code"): + # ignore empty rows + continue + if not d.get("docname"): new_child_flag = True check_doc_permissions(parent, 'create') @@ -1614,7 +1930,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil qty_unchanged = prev_qty == new_qty uom_unchanged = prev_uom == new_uom conversion_factor_unchanged = prev_con_fac == new_con_fac - date_unchanged = prev_date == new_date if prev_date and new_date else False # in case of delivery note etc + date_unchanged = prev_date == getdate(new_date) if prev_date and new_date else False # in case of delivery note etc if rate_unchanged and qty_unchanged and conversion_factor_unchanged and uom_unchanged and date_unchanged: continue diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 6a550e0e975..974ade35849 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -72,7 +72,8 @@ class BuyingController(StockController, Subcontracting): # set contact and address details for supplier, if they are not mentioned if getattr(self, "supplier", None): self.update_if_missing(get_party_details(self.supplier, party_type="Supplier", ignore_permissions=self.flags.ignore_permissions, - doctype=self.doctype, company=self.company, party_address=self.supplier_address, shipping_address=self.get('shipping_address'))) + doctype=self.doctype, company=self.company, party_address=self.supplier_address, shipping_address=self.get('shipping_address'), + fetch_payment_terms_template= not self.get('ignore_default_payment_terms_template'))) self.set_missing_item_details(for_validate) diff --git a/erpnext/controllers/item_variant.py b/erpnext/controllers/item_variant.py index 051481ff603..8c361a2e561 100644 --- a/erpnext/controllers/item_variant.py +++ b/erpnext/controllers/item_variant.py @@ -344,4 +344,3 @@ def create_variant_doc_for_quick_entry(template, args): variant.name = variant.item_code validate_item_variant_attributes(variant, args) return variant.as_dict() - diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 280319321f2..4b4c8befa53 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -407,6 +407,7 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters): INNER JOIN `tabBatch` batch on sle.batch_no = batch.name where batch.disabled = 0 + and sle.is_cancelled = 0 and sle.item_code = %(item_code)s and sle.warehouse = %(warehouse)s and (sle.batch_no like %(txt)s @@ -525,6 +526,9 @@ def get_filtered_dimensions(doctype, txt, searchfield, start, page_len, filters) if meta.is_tree: query_filters.append(['is_group', '=', 0]) + if meta.has_field('disabled'): + query_filters.append(['disabled', '!=', 1]) + if meta.has_field('company'): query_filters.append(['company', '=', filters.get('company')]) diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 80ccc6d75b2..01486fcd65d 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -329,7 +329,6 @@ def make_return_doc(doctype, source_name, target_doc=None): target_doc.po_detail = source_doc.po_detail target_doc.pr_detail = source_doc.pr_detail target_doc.purchase_invoice_item = source_doc.name - target_doc.price_list_rate = 0 elif doctype == "Delivery Note": returned_qty_map = get_returned_qty_map_for_row(source_doc.name, doctype) @@ -360,7 +359,6 @@ def make_return_doc(doctype, source_name, target_doc=None): else: target_doc.pos_invoice_item = source_doc.name - target_doc.price_list_rate = 0 if default_warehouse_for_sales_return: target_doc.warehouse = default_warehouse_for_sales_return @@ -396,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, @@ -419,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 da2765deded..844c40c8a64 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe from frappe.utils import cint, flt, cstr, get_link_to_form, nowtime -from frappe import _, throw +from frappe import _, bold, throw from erpnext.stock.get_item_details import get_bin_details from erpnext.stock.utils import get_incoming_rate from erpnext.stock.get_item_details import get_conversion_factor @@ -16,7 +16,6 @@ from erpnext.controllers.stock_controller import StockController from erpnext.controllers.sales_and_purchase_return import get_rate_for_return class SellingController(StockController): - def get_feed(self): return _("To {0} | {1} {2}").format(self.customer_name, self.currency, self.grand_total) @@ -169,39 +168,96 @@ class SellingController(StockController): def validate_selling_price(self): def throw_message(idx, item_name, rate, ref_rate_field): - bold_net_rate = frappe.bold("net rate") - msg = (_("""Row #{}: Selling rate for item {} is lower than its {}. Selling {} should be atleast {}""") - .format(idx, frappe.bold(item_name), frappe.bold(ref_rate_field), bold_net_rate, frappe.bold(rate))) - msg += "

" - msg += (_("""You can alternatively disable selling price validation in {} to bypass this validation.""") - .format(get_link_to_form("Selling Settings", "Selling Settings"))) - frappe.throw(msg, title=_("Invalid Selling Price")) + throw(_("""Row #{0}: Selling rate for item {1} is lower than its {2}. + Selling {3} should be atleast {4}.

Alternatively, + you can disable selling price validation in {5} to bypass + this validation.""").format( + idx, + bold(item_name), + bold(ref_rate_field), + bold("net rate"), + bold(rate), + get_link_to_form("Selling Settings", "Selling Settings"), + ), title=_("Invalid Selling Price")) - if not frappe.db.get_single_value("Selling Settings", "validate_selling_price"): - return - if hasattr(self, "is_return") and self.is_return: + if ( + self.get("is_return") + or not frappe.db.get_single_value("Selling Settings", "validate_selling_price") + ): return - for it in self.get("items"): - if not it.item_code: + is_internal_customer = self.get('is_internal_customer') + valuation_rate_map = {} + + for item in self.items: + if not item.item_code: continue - last_purchase_rate, is_stock_item = frappe.get_cached_value("Item", it.item_code, ["last_purchase_rate", "is_stock_item"]) - last_purchase_rate_in_sales_uom = last_purchase_rate * (it.conversion_factor or 1) - if flt(it.base_net_rate) < flt(last_purchase_rate_in_sales_uom): - throw_message(it.idx, frappe.bold(it.item_name), last_purchase_rate_in_sales_uom, "last purchase rate") + last_purchase_rate, is_stock_item = frappe.get_cached_value( + "Item", item.item_code, ("last_purchase_rate", "is_stock_item") + ) - last_valuation_rate = frappe.db.sql(""" - SELECT valuation_rate FROM `tabStock Ledger Entry` WHERE item_code = %s - AND warehouse = %s AND valuation_rate > 0 - ORDER BY posting_date DESC, posting_time DESC, creation DESC LIMIT 1 - """, (it.item_code, it.warehouse)) - if last_valuation_rate: - last_valuation_rate_in_sales_uom = last_valuation_rate[0][0] * (it.conversion_factor or 1) - if is_stock_item and flt(it.base_net_rate) < flt(last_valuation_rate_in_sales_uom) \ - and not self.get('is_internal_customer'): - throw_message(it.idx, frappe.bold(it.item_name), last_valuation_rate_in_sales_uom, "valuation rate") + last_purchase_rate_in_sales_uom = ( + last_purchase_rate * (item.conversion_factor or 1) + ) + if flt(item.base_net_rate) < flt(last_purchase_rate_in_sales_uom): + throw_message( + item.idx, + item.item_name, + last_purchase_rate_in_sales_uom, + "last purchase rate" + ) + + if is_internal_customer or not is_stock_item: + continue + + valuation_rate_map[(item.item_code, item.warehouse)] = None + + if not valuation_rate_map: + return + + or_conditions = ( + f"""(item_code = {frappe.db.escape(valuation_rate[0])} + and warehouse = {frappe.db.escape(valuation_rate[1])})""" + for valuation_rate in valuation_rate_map + ) + + valuation_rates = frappe.db.sql(f""" + select + item_code, warehouse, valuation_rate + from + `tabBin` + where + ({" or ".join(or_conditions)}) + and valuation_rate > 0 + """, as_dict=True) + + for rate in valuation_rates: + valuation_rate_map[(rate.item_code, rate.warehouse)] = rate.valuation_rate + + for item in self.items: + if not item.item_code: + continue + + last_valuation_rate = valuation_rate_map.get( + (item.item_code, item.warehouse) + ) + + if not last_valuation_rate: + continue + + last_valuation_rate_in_sales_uom = ( + last_valuation_rate * (item.conversion_factor or 1) + ) + + if flt(item.base_net_rate) < flt(last_valuation_rate_in_sales_uom): + throw_message( + item.idx, + item.item_name, + last_valuation_rate_in_sales_uom, + "valuation rate" + ) def get_item_list(self): il = [] @@ -306,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 []) @@ -315,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(): @@ -366,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/controllers/status_updater.py b/erpnext/controllers/status_updater.py index 943f7aaeb12..7b24e50b143 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals import frappe -from frappe.utils import flt, comma_or, nowdate, getdate +from frappe.utils import flt, comma_or, nowdate, getdate, now from frappe import _ from frappe.model.document import Document @@ -86,7 +86,8 @@ status_map = { ], "Bank Transaction": [ ["Unreconciled", "eval:self.docstatus == 1 and self.unallocated_amount>0"], - ["Reconciled", "eval:self.docstatus == 1 and self.unallocated_amount<=0"] + ["Reconciled", "eval:self.docstatus == 1 and self.unallocated_amount<=0"], + ["Cancelled", "eval:self.docstatus == 2"] ], "POS Opening Entry": [ ["Draft", None], @@ -336,10 +337,14 @@ class StatusUpdater(Document): target.notify_update() def _update_modified(self, args, update_modified): - args['update_modified'] = '' - if update_modified: - args['update_modified'] = ', modified = now(), modified_by = {0}'\ - .format(frappe.db.escape(frappe.session.user)) + if not update_modified: + args['update_modified'] = '' + return + + args['update_modified'] = ', modified = {0}, modified_by = {1}'.format( + frappe.db.escape(now()), + frappe.db.escape(frappe.session.user) + ) def update_billing_status_for_zero_amount_refdoc(self, ref_dt): ref_fieldname = frappe.scrub(ref_dt) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 8196cff849d..17707ecae7f 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -27,6 +27,7 @@ class StockController(AccountsController): if not self.get('is_return'): self.validate_inspection() self.validate_serialized_batch() + self.clean_serial_nos() self.validate_customer_provided_item() self.set_rate_of_stock_uom() self.validate_internal_transfer() @@ -53,12 +54,17 @@ class StockController(AccountsController): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos for d in self.get("items"): if hasattr(d, 'serial_no') and hasattr(d, 'batch_no') and d.serial_no and d.batch_no: - serial_nos = get_serial_nos(d.serial_no) - for serial_no_data in frappe.get_all("Serial No", - filters={"name": ("in", serial_nos)}, fields=["batch_no", "name"]): - if serial_no_data.batch_no != d.batch_no: + serial_nos = frappe.get_all("Serial No", + fields=["batch_no", "name", "warehouse"], + filters={ + "name": ("in", get_serial_nos(d.serial_no)) + } + ) + + for row in serial_nos: + if row.warehouse and row.batch_no != d.batch_no: frappe.throw(_("Row #{0}: Serial No {1} does not belong to Batch {2}") - .format(d.idx, serial_no_data.name, d.batch_no)) + .format(d.idx, row.name, d.batch_no)) if flt(d.qty) > 0.0 and d.get("batch_no") and self.get("posting_date") and self.docstatus < 2: expiry_date = frappe.get_cached_value("Batch", d.get("batch_no"), "expiry_date") @@ -67,6 +73,12 @@ class StockController(AccountsController): frappe.throw(_("Row #{0}: The batch {1} has already expired.") .format(d.idx, get_link_to_form("Batch", d.get("batch_no")))) + def clean_serial_nos(self): + for row in self.get("items"): + if hasattr(row, "serial_no") and row.serial_no: + # replace commas by linefeed and remove all spaces in string + row.serial_no = row.serial_no.replace(",", "\n").replace(" ", "") + def get_gl_entries(self, warehouse_account=None, default_expense_account=None, default_cost_center=None): @@ -356,42 +368,68 @@ class StockController(AccountsController): }, update_modified) def validate_inspection(self): - '''Checks if quality inspection is set for Items that require inspection. - On submit, throw an exception''' - inspection_required_fieldname = None - if self.doctype in ["Purchase Receipt", "Purchase Invoice"]: - inspection_required_fieldname = "inspection_required_before_purchase" - elif self.doctype in ["Delivery Note", "Sales Invoice"]: - inspection_required_fieldname = "inspection_required_before_delivery" + """Checks if quality inspection is set/ is valid for Items that require inspection.""" + inspection_fieldname_map = { + "Purchase Receipt": "inspection_required_before_purchase", + "Purchase Invoice": "inspection_required_before_purchase", + "Sales Invoice": "inspection_required_before_delivery", + "Delivery Note": "inspection_required_before_delivery" + } + inspection_required_fieldname = inspection_fieldname_map.get(self.doctype) + # return if inspection is not required on document level if ((not inspection_required_fieldname and self.doctype != "Stock Entry") or (self.doctype == "Stock Entry" and not self.inspection_required) or (self.doctype in ["Sales Invoice", "Purchase Invoice"] and not self.update_stock)): return - for d in self.get('items'): - qa_required = False - if (inspection_required_fieldname and not d.quality_inspection and - frappe.db.get_value("Item", d.item_code, inspection_required_fieldname)): - qa_required = True - elif self.doctype == "Stock Entry" and not d.quality_inspection and d.t_warehouse: - qa_required = True - if self.docstatus == 1 and d.quality_inspection: - qa_doc = frappe.get_doc("Quality Inspection", d.quality_inspection) - if qa_doc.docstatus == 0: - link = frappe.utils.get_link_to_form('Quality Inspection', d.quality_inspection) - frappe.throw(_("Quality Inspection: {0} is not submitted for the item: {1} in row {2}").format(link, d.item_code, d.idx), QualityInspectionNotSubmittedError) + for row in self.get('items'): + qi_required = False + if (inspection_required_fieldname and frappe.db.get_value("Item", row.item_code, inspection_required_fieldname)): + qi_required = True + elif self.doctype == "Stock Entry" and row.t_warehouse: + qi_required = True # inward stock needs inspection - if qa_doc.status != 'Accepted': - frappe.throw(_("Row {0}: Quality Inspection rejected for item {1}") - .format(d.idx, d.item_code), QualityInspectionRejectedError) - elif qa_required : - action = frappe.get_doc('Stock Settings').action_if_quality_inspection_is_not_submitted - if self.docstatus==1 and action == 'Stop': - frappe.throw(_("Quality Inspection required for Item {0} to submit").format(frappe.bold(d.item_code)), - exc=QualityInspectionRequiredError) - else: - frappe.msgprint(_("Create Quality Inspection for Item {0}").format(frappe.bold(d.item_code))) + if qi_required: # validate row only if inspection is required on item level + self.validate_qi_presence(row) + if self.docstatus == 1: + self.validate_qi_submission(row) + self.validate_qi_rejection(row) + + def validate_qi_presence(self, row): + """Check if QI is present on row level. Warn on save and stop on submit if missing.""" + if not row.quality_inspection: + msg = f"Row #{row.idx}: Quality Inspection is required for Item {frappe.bold(row.item_code)}" + if self.docstatus == 1: + frappe.throw(_(msg), title=_("Inspection Required"), exc=QualityInspectionRequiredError) + else: + frappe.msgprint(_(msg), title=_("Inspection Required"), indicator="blue") + + def validate_qi_submission(self, row): + """Check if QI is submitted on row level, during submission""" + action = frappe.db.get_single_value("Stock Settings", "action_if_quality_inspection_is_not_submitted") + qa_docstatus = frappe.db.get_value("Quality Inspection", row.quality_inspection, "docstatus") + + if not qa_docstatus == 1: + link = frappe.utils.get_link_to_form('Quality Inspection', row.quality_inspection) + msg = f"Row #{row.idx}: Quality Inspection {link} is not submitted for the item: {row.item_code}" + if action == "Stop": + frappe.throw(_(msg), title=_("Inspection Submission"), exc=QualityInspectionNotSubmittedError) + else: + frappe.msgprint(_(msg), alert=True, indicator="orange") + + def validate_qi_rejection(self, row): + """Check if QI is rejected on row level, during submission""" + action = frappe.db.get_single_value("Stock Settings", "action_if_quality_inspection_is_rejected") + qa_status = frappe.db.get_value("Quality Inspection", row.quality_inspection, "status") + + if qa_status == "Rejected": + link = frappe.utils.get_link_to_form('Quality Inspection', row.quality_inspection) + msg = f"Row #{row.idx}: Quality Inspection {link} was rejected for item {row.item_code}" + if action == "Stop": + frappe.throw(_(msg), title=_("Inspection Rejected"), exc=QualityInspectionRejectedError) + else: + frappe.msgprint(_(msg), alert=True, indicator="orange") def update_blanket_order(self): blanket_orders = list(set([d.blanket_order for d in self.items if d.blanket_order])) diff --git a/erpnext/controllers/subcontracting.py b/erpnext/controllers/subcontracting.py index 36ae1102164..969829f9651 100644 --- a/erpnext/controllers/subcontracting.py +++ b/erpnext/controllers/subcontracting.py @@ -390,4 +390,4 @@ class Subcontracting(): incorrect_sn = "\n".join(incorrect_sn) link = get_link_to_form('Purchase Order', row.purchase_order) msg = f'The Serial Nos {incorrect_sn} has not supplied against the Purchase Order {link}' - frappe.throw(_(msg), title=_("Incorrect Serial Number Consumed")) \ No newline at end of file + frappe.throw(_(msg), title=_("Incorrect Serial Number Consumed")) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 56da5b71da0..05edb2530c2 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -152,7 +152,7 @@ class calculate_taxes_and_totals(object): validate_taxes_and_charges(tax) validate_inclusive_tax(tax, self.doc) - if not self.doc.get('is_consolidated'): + if not (self.doc.get('is_consolidated') or tax.get("dont_recompute_tax")): tax.item_wise_tax_detail = {} tax_fields = ["total", "tax_amount_after_discount_amount", @@ -347,7 +347,7 @@ class calculate_taxes_and_totals(object): elif tax.charge_type == "On Item Quantity": current_tax_amount = tax_rate * item.qty - if not self.doc.get("is_consolidated"): + if not (self.doc.get("is_consolidated") or tax.get("dont_recompute_tax")): self.set_item_wise_tax(item, tax, tax_rate, current_tax_amount) return current_tax_amount @@ -455,7 +455,8 @@ class calculate_taxes_and_totals(object): def _cleanup(self): if not self.doc.get('is_consolidated'): for tax in self.doc.get("taxes"): - tax.item_wise_tax_detail = json.dumps(tax.item_wise_tax_detail, separators=(',', ':')) + if not tax.get("dont_recompute_tax"): + tax.item_wise_tax_detail = json.dumps(tax.item_wise_tax_detail, separators=(',', ':')) def set_discount_amount(self): if self.doc.additional_discount_percentage: @@ -678,17 +679,13 @@ class calculate_taxes_and_totals(object): default_mode_of_payment = frappe.db.get_value('POS Payment Method', {'parent': self.doc.pos_profile, 'default': 1}, ['mode_of_payment'], as_dict=1) - self.doc.payments = [] - if default_mode_of_payment: + self.doc.payments = [] self.doc.append('payments', { 'mode_of_payment': default_mode_of_payment.mode_of_payment, 'amount': total_amount_to_pay, 'default': 1 }) - else: - self.doc.is_pos = 0 - self.doc.pos_profile = '' self.calculate_paid_amount() diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py index df73f09c493..f7c6b6c7993 100644 --- a/erpnext/crm/doctype/appointment/appointment.py +++ b/erpnext/crm/doctype/appointment/appointment.py @@ -235,4 +235,3 @@ def _get_employee_from_user(user): # frappe.db.exists returns a tuple of a tuple return frappe.get_doc('Employee', employee_docname[0][0]) return None - diff --git a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.js b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.js index dc3ae8bf41a..0c64eb8e822 100644 --- a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.js +++ b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.js @@ -7,4 +7,4 @@ function check_times(frm) { frappe.throw(__('In row {0} of Appointment Booking Slots: "To Time" must be later than "From Time".', [i + 1])); } }); -} \ No newline at end of file +} diff --git a/erpnext/crm/doctype/contract/contract.js b/erpnext/crm/doctype/contract/contract.js index 99688551630..7848de7a727 100644 --- a/erpnext/crm/doctype/contract/contract.js +++ b/erpnext/crm/doctype/contract/contract.js @@ -15,7 +15,7 @@ frappe.ui.form.on("Contract", { let contract_template = r.message.contract_template; frm.set_value("contract_terms", r.message.contract_terms); frm.set_value("requires_fulfilment", contract_template.requires_fulfilment); - + if (frm.doc.requires_fulfilment) { // Populate the fulfilment terms table from a contract template, if any r.message.contract_template.fulfilment_terms.forEach(element => { @@ -23,7 +23,7 @@ frappe.ui.form.on("Contract", { d.requirement = element.requirement; }); frm.refresh_field("fulfilment_terms"); - } + } } } }); diff --git a/erpnext/crm/doctype/contract/contract_list.js b/erpnext/crm/doctype/contract/contract_list.js index 26a2907c7cc..7d5609651a1 100644 --- a/erpnext/crm/doctype/contract/contract_list.js +++ b/erpnext/crm/doctype/contract/contract_list.js @@ -9,4 +9,4 @@ frappe.listview_settings['Contract'] = { return [__(doc.status), "gray", "status,=," + doc.status]; } }, -}; \ No newline at end of file +}; diff --git a/erpnext/crm/doctype/contract/test_contract.js b/erpnext/crm/doctype/contract/test_contract.js deleted file mode 100644 index 4c77c3d649f..00000000000 --- 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 2a2d5e1bfc0..00000000000 --- 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/contract_template.py b/erpnext/crm/doctype/contract_template/contract_template.py index 69fd86f7fb5..9281220eef4 100644 --- a/erpnext/crm/doctype/contract_template/contract_template.py +++ b/erpnext/crm/doctype/contract_template/contract_template.py @@ -24,8 +24,8 @@ def get_contract_template(template_name, doc): if contract_template.contract_terms: contract_terms = frappe.render_template(contract_template.contract_terms, doc) - + return { - 'contract_template': contract_template, + 'contract_template': contract_template, 'contract_terms': contract_terms - } \ No newline at end of file + } 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 6aaddd7df4d..00000000000 --- 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_dashboard.py b/erpnext/crm/doctype/lead/lead_dashboard.py index 69d8ca70926..3950d063f22 100644 --- a/erpnext/crm/doctype/lead/lead_dashboard.py +++ b/erpnext/crm/doctype/lead/lead_dashboard.py @@ -16,4 +16,4 @@ def get_data(): 'items': ['Opportunity', 'Quotation'] }, ] - } \ No newline at end of file + } diff --git a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js index 263005ef6c5..7aa0b777596 100644 --- a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js +++ b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js @@ -2,8 +2,8 @@ // For license information, please see license.txt frappe.ui.form.on('LinkedIn Settings', { - onload: function(frm){ - if (frm.doc.session_status == 'Expired' && frm.doc.consumer_key && frm.doc.consumer_secret){ + onload: function(frm) { + if (frm.doc.session_status == 'Expired' && frm.doc.consumer_key && frm.doc.consumer_secret) { frappe.confirm( __('Session not valid, Do you want to login?'), function(){ @@ -14,8 +14,9 @@ frappe.ui.form.on('LinkedIn Settings', { } ); } + frm.dashboard.set_headline(__("For more information, {0}.", [`${__('Click here')}`])); }, - refresh: function(frm){ + refresh: function(frm) { if (frm.doc.session_status=="Expired"){ let msg = __("Session Not Active. Save doc to login."); frm.dashboard.set_headline_alert( @@ -53,7 +54,7 @@ frappe.ui.form.on('LinkedIn Settings', { ); } }, - login: function(frm){ + login: function(frm) { if (frm.doc.consumer_key && frm.doc.consumer_secret){ frappe.dom.freeze(); frappe.call({ @@ -67,7 +68,7 @@ frappe.ui.form.on('LinkedIn Settings', { }); } }, - after_save: function(frm){ + after_save: function(frm) { frm.trigger("login"); } }); diff --git a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.json b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.json index 9eacb0011c5..f882e36c32a 100644 --- a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.json +++ b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.json @@ -2,6 +2,7 @@ "actions": [], "creation": "2020-01-30 13:36:39.492931", "doctype": "DocType", + "documentation": "https://docs.erpnext.com/docs/user/manual/en/CRM/linkedin-settings", "editable_grid": 1, "engine": "InnoDB", "field_order": [ @@ -87,7 +88,7 @@ ], "issingle": 1, "links": [], - "modified": "2020-04-16 23:22:51.966397", + "modified": "2021-02-18 15:19:21.920725", "modified_by": "Administrator", "module": "CRM", "name": "LinkedIn Settings", diff --git a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py index d8c6fb4f90f..9b88d78c1ff 100644 --- a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py +++ b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py @@ -3,11 +3,12 @@ # For license information, please see license.txt from __future__ import unicode_literals -import frappe, requests, json +import frappe +import requests from frappe import _ -from frappe.utils import get_site_url, get_url_to_form, get_link_to_form +from frappe.utils import get_url_to_form from frappe.model.document import Document -from frappe.utils.file_manager import get_file, get_file_path +from frappe.utils.file_manager import get_file_path from six.moves.urllib.parse import urlencode class LinkedInSettings(Document): @@ -42,11 +43,7 @@ class LinkedInSettings(Document): self.db_set("access_token", response["access_token"]) def get_member_profile(self): - headers = { - "Authorization": "Bearer {}".format(self.access_token) - } - url = "https://api.linkedin.com/v2/me" - response = requests.get(url=url, headers=headers) + response = requests.get(url="https://api.linkedin.com/v2/me", headers=self.get_headers()) response = frappe.parse_json(response.content.decode()) frappe.db.set_value(self.doctype, self.name, { @@ -55,16 +52,16 @@ class LinkedInSettings(Document): "session_status": "Active" }) frappe.local.response["type"] = "redirect" - frappe.local.response["location"] = get_url_to_form("LinkedIn Settings","LinkedIn Settings") + frappe.local.response["location"] = get_url_to_form("LinkedIn Settings", "LinkedIn Settings") - def post(self, text, media=None): + def post(self, text, title, media=None): if not media: - return self.post_text(text) + return self.post_text(text, title) else: media_id = self.upload_image(media) if media_id: - return self.post_text(text, media_id=media_id) + return self.post_text(text, title, media_id=media_id) else: frappe.log_error("Failed to upload media.","LinkedIn Upload Error") @@ -82,9 +79,7 @@ class LinkedInSettings(Document): }] } } - headers = { - "Authorization": "Bearer {}".format(self.access_token) - } + headers = self.get_headers() response = self.http_post(url=register_url, body=body, headers=headers) if response.status_code == 200: @@ -100,24 +95,33 @@ class LinkedInSettings(Document): return None - def post_text(self, text, media_id=None): + def post_text(self, text, title, media_id=None): url = "https://api.linkedin.com/v2/shares" - headers = { - "X-Restli-Protocol-Version": "2.0.0", - "Authorization": "Bearer {}".format(self.access_token), - "Content-Type": "application/json; charset=UTF-8" - } + headers = self.get_headers() + headers["X-Restli-Protocol-Version"] = "2.0.0" + headers["Content-Type"] = "application/json; charset=UTF-8" + body = { "distribution": { "linkedInDistributionTarget": {} }, "owner":"urn:li:organization:{0}".format(self.company_id), - "subject": "Test Share Subject", + "subject": title, "text": { "text": text } } + reference_url = self.get_reference_url(text) + if reference_url: + body["content"] = { + "contentEntities": [ + { + "entityLocation": reference_url + } + ] + } + if media_id: body["content"]= { "contentEntities": [{ @@ -141,20 +145,60 @@ class LinkedInSettings(Document): raise except Exception as e: - content = json.loads(response.content) - - if response.status_code == 401: - self.db_set("session_status", "Expired") - frappe.db.commit() - frappe.throw(content["message"], title="LinkedIn Error - Unauthorized") - elif response.status_code == 403: - frappe.msgprint(_("You Didn't have permission to access this API")) - frappe.throw(content["message"], title="LinkedIn Error - Access Denied") - else: - frappe.throw(response.reason, title=response.status_code) - + self.api_error(response) + return response + def get_headers(self): + return { + "Authorization": "Bearer {}".format(self.access_token) + } + + def get_reference_url(self, text): + import re + regex_url = r"http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+" + urls = re.findall(regex_url, text) + if urls: + return urls[0] + + def delete_post(self, post_id): + try: + response = requests.delete(url="https://api.linkedin.com/v2/shares/urn:li:share:{0}".format(post_id), headers=self.get_headers()) + if response.status_code !=200: + raise + except Exception: + self.api_error(response) + + def get_post(self, post_id): + url = "https://api.linkedin.com/v2/organizationalEntityShareStatistics?q=organizationalEntity&organizationalEntity=urn:li:organization:{0}&shares[0]=urn:li:share:{1}".format(self.company_id, post_id) + + try: + response = requests.get(url=url, headers=self.get_headers()) + if response.status_code !=200: + raise + + except Exception: + self.api_error(response) + + response = frappe.parse_json(response.content.decode()) + if len(response.elements): + return response.elements[0] + + return None + + def api_error(self, response): + content = frappe.parse_json(response.content.decode()) + + if response.status_code == 401: + self.db_set("session_status", "Expired") + frappe.db.commit() + frappe.throw(content["message"], title=_("LinkedIn Error - Unauthorized")) + elif response.status_code == 403: + frappe.msgprint(_("You didn't have permission to access this API")) + frappe.throw(content["message"], title=_("LinkedIn Error - Access Denied")) + else: + frappe.throw(response.reason, title=response.status_code) + @frappe.whitelist(allow_guest=True) def callback(code=None, error=None, error_description=None): if not error: 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 aa4b868f932..00000000000 --- 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 ac374a95f4e..5cc63d4511b 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.js +++ b/erpnext/crm/doctype/opportunity/opportunity.js @@ -53,6 +53,13 @@ frappe.ui.form.on("Opportunity", { frm.get_field("items").grid.set_multiple_add("item_code", "qty"); }, + status:function(frm){ + if (frm.doc.status == "Lost"){ + frm.trigger('set_as_lost_dialog'); + } + + }, + customer_address: function(frm, cdt, cdn) { erpnext.utils.get_address_display(frm, 'customer_address', 'address_display', false); }, @@ -88,14 +95,17 @@ frappe.ui.form.on("Opportunity", { }, __('Create')); } - frm.add_custom_button(__('Quotation'), - cur_frm.cscript.create_quotation, __('Create')); - - if(doc.status!=="Quotation") { - frm.add_custom_button(__('Lost'), () => { - frm.trigger('set_as_lost_dialog'); - }); + if (frm.doc.opportunity_from != "Customer") { + frm.add_custom_button(__('Customer'), + function() { + frm.trigger("make_customer") + }, __('Create')); } + + frm.add_custom_button(__('Quotation'), + function() { + frm.trigger("create_quotation") + }, __('Create')); } if(!frm.doc.__islocal && frm.perm[0].write && frm.doc.docstatus==0) { @@ -192,6 +202,13 @@ erpnext.crm.Opportunity = frappe.ui.form.Controller.extend({ method: "erpnext.crm.doctype.opportunity.opportunity.make_quotation", frm: cur_frm }) + }, + + make_customer: function() { + frappe.model.open_mapped_doc({ + method: "erpnext.crm.doctype.opportunity.opportunity.make_customer", + frm: cur_frm + }) } }); @@ -213,4 +230,4 @@ cur_frm.cscript.item_code = function(doc, cdt, cdn) { } }) } -} \ No newline at end of file +} diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py index 23ad98a2828..a74a94afd68 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.py +++ b/erpnext/crm/doctype/opportunity/opportunity.py @@ -287,6 +287,24 @@ def make_request_for_quotation(source_name, target_doc=None): return doclist +@frappe.whitelist() +def make_customer(source_name, target_doc=None): + def set_missing_values(source, target): + if source.opportunity_from == "Lead": + target.lead_name = source.party_name + + doclist = get_mapped_doc("Opportunity", source_name, { + "Opportunity": { + "doctype": "Customer", + "field_map": { + "currency": "default_currency", + "customer_name": "customer_name" + } + } + }, target_doc, set_missing_values) + + return doclist + @frappe.whitelist() def make_supplier_quotation(source_name, target_doc=None): doclist = get_mapped_doc("Opportunity", source_name, { @@ -372,4 +390,4 @@ def get_events(start, end, filters=None): "start": start, "end": end }, as_dict=True, update={"allDay": 0}) - return data \ No newline at end of file + return data diff --git a/erpnext/crm/doctype/opportunity/opportunity_dashboard.py b/erpnext/crm/doctype/opportunity/opportunity_dashboard.py index 68f0104fd6c..b8c53f077ae 100644 --- a/erpnext/crm/doctype/opportunity/opportunity_dashboard.py +++ b/erpnext/crm/doctype/opportunity/opportunity_dashboard.py @@ -9,4 +9,4 @@ def get_data(): 'items': ['Quotation', 'Supplier Quotation'] }, ] - } \ No newline at end of file + } diff --git a/erpnext/crm/doctype/opportunity/test_opportunity.py b/erpnext/crm/doctype/opportunity/test_opportunity.py index 04cd8a26cad..52aa0b036ae 100644 --- a/erpnext/crm/doctype/opportunity/test_opportunity.py +++ b/erpnext/crm/doctype/opportunity/test_opportunity.py @@ -87,4 +87,4 @@ def make_opportunity(**args): }) opp_doc.insert() - return opp_doc \ No newline at end of file + return opp_doc 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 3a1ede94dba..00000000000 --- 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/sales_stage/test_sales_stage.js b/erpnext/crm/doctype/sales_stage/test_sales_stage.js deleted file mode 100644 index 807af1fd983..00000000000 --- 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/crm/doctype/social_media_post/social_media_post.js b/erpnext/crm/doctype/social_media_post/social_media_post.js index 0ce8b44e19b..a8f5deea535 100644 --- a/erpnext/crm/doctype/social_media_post/social_media_post.js +++ b/erpnext/crm/doctype/social_media_post/social_media_post.js @@ -1,67 +1,139 @@ // Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt frappe.ui.form.on('Social Media Post', { - validate: function(frm){ - if (frm.doc.twitter === 0 && frm.doc.linkedin === 0){ - frappe.throw(__("Select atleast one Social Media from Share on.")) - } - if (frm.doc.scheduled_time) { - let scheduled_time = new Date(frm.doc.scheduled_time); - let date_time = new Date(); - if (scheduled_time.getTime() < date_time.getTime()){ - frappe.throw(__("Invalid Scheduled Time")); - } - } - if (frm.doc.text?.length > 280){ - frappe.throw(__("Length Must be less than 280.")) - } - }, - refresh: function(frm){ - if (frm.doc.docstatus === 1){ - if (frm.doc.post_status != "Posted"){ - add_post_btn(frm); - } - else if (frm.doc.post_status == "Posted"){ - frm.set_df_property('sheduled_time', 'read_only', 1); - } + validate: function(frm) { + if (frm.doc.twitter === 0 && frm.doc.linkedin === 0) { + frappe.throw(__("Select atleast one Social Media Platform to Share on.")); + } + if (frm.doc.scheduled_time) { + let scheduled_time = new Date(frm.doc.scheduled_time); + let date_time = new Date(); + if (scheduled_time.getTime() < date_time.getTime()) { + frappe.throw(__("Scheduled Time must be a future time.")); + } + } + frm.trigger('validate_tweet_length'); + }, - let html=''; - if (frm.doc.twitter){ - let color = frm.doc.twitter_post_id ? "green" : "red"; - let status = frm.doc.twitter_post_id ? "Posted" : "Not Posted"; - html += `
- Twitter : ${status} -
` ; - } - if (frm.doc.linkedin){ - let color = frm.doc.linkedin_post_id ? "green" : "red"; - let status = frm.doc.linkedin_post_id ? "Posted" : "Not Posted"; - html += `
- LinkedIn : ${status} -
` ; - } - html = `
${html}
`; - frm.dashboard.set_headline_alert(html); - } - } + text: function(frm) { + if (frm.doc.text) { + frm.set_df_property('text', 'description', `${frm.doc.text.length}/280`); + frm.refresh_field('text'); + frm.trigger('validate_tweet_length'); + } + }, + + validate_tweet_length: function(frm) { + if (frm.doc.text && frm.doc.text.length > 280) { + frappe.throw(__("Tweet length Must be less than 280.")); + } + }, + + onload: function(frm) { + frm.trigger('make_dashboard'); + }, + + make_dashboard: function(frm) { + if (frm.doc.post_status == "Posted") { + frappe.call({ + doc: frm.doc, + method: 'get_post', + freeze: true, + callback: (r) => { + if (!r.message) { + return; + } + + let datasets = [], colors = []; + if (r.message && r.message.twitter) { + colors.push('#1DA1F2'); + datasets.push({ + name: 'Twitter', + values: [r.message.twitter.favorite_count, r.message.twitter.retweet_count] + }); + } + if (r.message && r.message.linkedin) { + colors.push('#0077b5'); + datasets.push({ + name: 'LinkedIn', + values: [r.message.linkedin.totalShareStatistics.likeCount, r.message.linkedin.totalShareStatistics.shareCount] + }); + } + + if (datasets.length) { + frm.dashboard.render_graph({ + data: { + labels: ['Likes', 'Retweets/Shares'], + datasets: datasets + }, + + title: __("Post Metrics"), + type: 'bar', + height: 300, + colors: colors + }); + } + } + }); + } + }, + + refresh: function(frm) { + frm.trigger('text'); + + if (frm.doc.docstatus === 1) { + if (!['Posted', 'Deleted'].includes(frm.doc.post_status)) { + frm.trigger('add_post_btn'); + } + if (frm.doc.post_status !='Deleted') { + frm.add_custom_button(('Delete Post'), function() { + frappe.confirm(__('Are you sure want to delete the Post from Social Media platforms?'), + function() { + frappe.call({ + doc: frm.doc, + method: 'delete_post', + freeze: true, + callback: () => { + frm.reload_doc(); + } + }); + } + ); + }); + } + + if (frm.doc.post_status !='Deleted') { + let html=''; + if (frm.doc.twitter) { + let color = frm.doc.twitter_post_id ? "green" : "red"; + let status = frm.doc.twitter_post_id ? "Posted" : "Not Posted"; + html += `
+ Twitter : ${status} +
` ; + } + if (frm.doc.linkedin) { + let color = frm.doc.linkedin_post_id ? "green" : "red"; + let status = frm.doc.linkedin_post_id ? "Posted" : "Not Posted"; + html += `
+ LinkedIn : ${status} +
` ; + } + html = `
${html}
`; + frm.dashboard.set_headline_alert(html); + } + } + }, + + add_post_btn: function(frm) { + frm.add_custom_button(__('Post Now'), function() { + frappe.call({ + doc: frm.doc, + method: 'post', + freeze: true, + callback: function() { + frm.reload_doc(); + } + }); + }); + } }); -var add_post_btn = function(frm){ - frm.add_custom_button(('Post Now'), function(){ - post(frm); - }); -} -var post = function(frm){ - frappe.dom.freeze(); - frappe.call({ - method: "erpnext.crm.doctype.social_media_post.social_media_post.publish", - args: { - doctype: frm.doc.doctype, - name: frm.doc.name - }, - callback: function(r) { - frm.reload_doc(); - frappe.dom.unfreeze(); - } - }) - -} \ No newline at end of file diff --git a/erpnext/crm/doctype/social_media_post/social_media_post.json b/erpnext/crm/doctype/social_media_post/social_media_post.json index 0a00dca2808..98e78f949e8 100644 --- a/erpnext/crm/doctype/social_media_post/social_media_post.json +++ b/erpnext/crm/doctype/social_media_post/social_media_post.json @@ -3,9 +3,11 @@ "autoname": "format: CRM-SMP-{YYYY}-{MM}-{DD}-{###}", "creation": "2020-01-30 11:53:13.872864", "doctype": "DocType", + "documentation": "https://docs.erpnext.com/docs/user/manual/en/CRM/social-media-post", "editable_grid": 1, "engine": "InnoDB", "field_order": [ + "title", "campaign_name", "scheduled_time", "post_status", @@ -30,32 +32,24 @@ "fieldname": "text", "fieldtype": "Small Text", "label": "Tweet", - "mandatory_depends_on": "eval:doc.twitter ==1", - "show_days": 1, - "show_seconds": 1 + "mandatory_depends_on": "eval:doc.twitter ==1" }, { "fieldname": "image", "fieldtype": "Attach Image", - "label": "Image", - "show_days": 1, - "show_seconds": 1 + "label": "Image" }, { - "default": "0", + "default": "1", "fieldname": "twitter", "fieldtype": "Check", - "label": "Twitter", - "show_days": 1, - "show_seconds": 1 + "label": "Twitter" }, { - "default": "0", + "default": "1", "fieldname": "linkedin", "fieldtype": "Check", - "label": "LinkedIn", - "show_days": 1, - "show_seconds": 1 + "label": "LinkedIn" }, { "fieldname": "amended_from", @@ -64,27 +58,22 @@ "no_copy": 1, "options": "Social Media Post", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "depends_on": "eval:doc.twitter ==1", "fieldname": "content", "fieldtype": "Section Break", - "label": "Twitter", - "show_days": 1, - "show_seconds": 1 + "label": "Twitter" }, { "allow_on_submit": 1, "fieldname": "post_status", "fieldtype": "Select", "label": "Post Status", - "options": "\nScheduled\nPosted\nError", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "no_copy": 1, + "options": "\nScheduled\nPosted\nCancelled\nDeleted\nError", + "read_only": 1 }, { "allow_on_submit": 1, @@ -92,9 +81,8 @@ "fieldtype": "Data", "hidden": 1, "label": "Twitter Post Id", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "no_copy": 1, + "read_only": 1 }, { "allow_on_submit": 1, @@ -102,82 +90,69 @@ "fieldtype": "Data", "hidden": 1, "label": "LinkedIn Post Id", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "no_copy": 1, + "read_only": 1 }, { "fieldname": "campaign_name", "fieldtype": "Link", "in_list_view": 1, "label": "Campaign", - "options": "Campaign", - "show_days": 1, - "show_seconds": 1 + "options": "Campaign" }, { "fieldname": "column_break_6", "fieldtype": "Column Break", - "label": "Share On", - "show_days": 1, - "show_seconds": 1 + "label": "Share On" }, { "fieldname": "column_break_14", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "fieldname": "tweet_preview", - "fieldtype": "HTML", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "HTML" }, { "collapsible": 1, "depends_on": "eval:doc.linkedin==1", "fieldname": "linkedin_section", "fieldtype": "Section Break", - "label": "LinkedIn", - "show_days": 1, - "show_seconds": 1 + "label": "LinkedIn" }, { "collapsible": 1, "fieldname": "attachments_section", "fieldtype": "Section Break", - "label": "Attachments", - "show_days": 1, - "show_seconds": 1 + "label": "Attachments" }, { "fieldname": "linkedin_post", "fieldtype": "Text", "label": "Post", - "mandatory_depends_on": "eval:doc.linkedin ==1", - "show_days": 1, - "show_seconds": 1 + "mandatory_depends_on": "eval:doc.linkedin ==1" }, { "fieldname": "column_break_15", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "allow_on_submit": 1, "fieldname": "scheduled_time", "fieldtype": "Datetime", "label": "Scheduled Time", - "read_only_depends_on": "eval:doc.post_status == \"Posted\"", - "show_days": 1, - "show_seconds": 1 + "read_only_depends_on": "eval:doc.post_status == \"Posted\"" + }, + { + "fieldname": "title", + "fieldtype": "Data", + "label": "Title", + "reqd": 1 } ], "is_submittable": 1, "links": [], - "modified": "2020-06-14 10:31:33.961381", + "modified": "2021-04-14 14:24:59.821223", "modified_by": "Administrator", "module": "CRM", "name": "Social Media Post", @@ -228,5 +203,6 @@ ], "sort_field": "modified", "sort_order": "DESC", + "title_field": "title", "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/crm/doctype/social_media_post/social_media_post.py b/erpnext/crm/doctype/social_media_post/social_media_post.py index ed1b5839446..95320bff535 100644 --- a/erpnext/crm/doctype/social_media_post/social_media_post.py +++ b/erpnext/crm/doctype/social_media_post/social_media_post.py @@ -10,17 +10,51 @@ import datetime class SocialMediaPost(Document): def validate(self): + if (not self.twitter and not self.linkedin): + frappe.throw(_("Select atleast one Social Media Platform to Share on.")) + if self.scheduled_time: current_time = frappe.utils.now_datetime() scheduled_time = frappe.utils.get_datetime(self.scheduled_time) if scheduled_time < current_time: - frappe.throw(_("Invalid Scheduled Time")) + frappe.throw(_("Scheduled Time must be a future time.")) + + if self.text and len(self.text) > 280: + frappe.throw(_("Tweet length must be less than 280.")) def submit(self): if self.scheduled_time: self.post_status = "Scheduled" super(SocialMediaPost, self).submit() + + def on_cancel(self): + self.db_set('post_status', 'Cancelled') + @frappe.whitelist() + def delete_post(self): + if self.twitter and self.twitter_post_id: + twitter = frappe.get_doc("Twitter Settings") + twitter.delete_tweet(self.twitter_post_id) + + if self.linkedin and self.linkedin_post_id: + linkedin = frappe.get_doc("LinkedIn Settings") + linkedin.delete_post(self.linkedin_post_id) + + self.db_set('post_status', 'Deleted') + + @frappe.whitelist() + def get_post(self): + response = {} + if self.linkedin and self.linkedin_post_id: + linkedin = frappe.get_doc("LinkedIn Settings") + response['linkedin'] = linkedin.get_post(self.linkedin_post_id) + if self.twitter and self.twitter_post_id: + twitter = frappe.get_doc("Twitter Settings") + response['twitter'] = twitter.get_tweet(self.twitter_post_id) + + return response + + @frappe.whitelist() def post(self): try: if self.twitter and not self.twitter_post_id: @@ -29,28 +63,22 @@ class SocialMediaPost(Document): self.db_set("twitter_post_id", twitter_post.id) if self.linkedin and not self.linkedin_post_id: linkedin = frappe.get_doc("LinkedIn Settings") - linkedin_post = linkedin.post(self.linkedin_post, self.image) - self.db_set("linkedin_post_id", linkedin_post.headers['X-RestLi-Id'].split(":")[-1]) + linkedin_post = linkedin.post(self.linkedin_post, self.title, self.image) + self.db_set("linkedin_post_id", linkedin_post.headers['X-RestLi-Id']) self.db_set("post_status", "Posted") except: self.db_set("post_status", "Error") title = _("Error while POSTING {0}").format(self.name) - traceback = frappe.get_traceback() - frappe.log_error(message=traceback , title=title) + frappe.log_error(message=frappe.get_traceback(), title=title) def process_scheduled_social_media_posts(): - posts = frappe.get_list("Social Media Post", filters={"post_status": "Scheduled", "docstatus":1}, fields= ["name", "scheduled_time","post_status"]) + posts = frappe.get_list("Social Media Post", filters={"post_status": "Scheduled", "docstatus":1}, fields= ["name", "scheduled_time"]) start = frappe.utils.now_datetime() end = start + datetime.timedelta(minutes=10) for post in posts: if post.scheduled_time: post_time = frappe.utils.get_datetime(post.scheduled_time) if post_time > start and post_time <= end: - publish('Social Media Post', post.name) - -@frappe.whitelist() -def publish(doctype, name): - sm_post = frappe.get_doc(doctype, name) - sm_post.post() - frappe.db.commit() + sm_post = frappe.get_doc('Social Media Post', post.name) + sm_post.post() diff --git a/erpnext/crm/doctype/social_media_post/social_media_post_list.js b/erpnext/crm/doctype/social_media_post/social_media_post_list.js index c60b91a9a02..a8c8272ad08 100644 --- a/erpnext/crm/doctype/social_media_post/social_media_post_list.js +++ b/erpnext/crm/doctype/social_media_post/social_media_post_list.js @@ -1,10 +1,11 @@ frappe.listview_settings['Social Media Post'] = { - add_fields: ["status","post_status"], - get_indicator: function(doc) { - return [__(doc.post_status), { - "Scheduled": "orange", - "Posted": "green", - "Error": "red" - }[doc.post_status]]; - } + add_fields: ["status", "post_status"], + get_indicator: function(doc) { + return [__(doc.post_status), { + "Scheduled": "orange", + "Posted": "green", + "Error": "red", + "Deleted": "red" + }[doc.post_status]]; + } } diff --git a/erpnext/crm/doctype/twitter_settings/twitter_settings.js b/erpnext/crm/doctype/twitter_settings/twitter_settings.js index f6f431ca5c9..112f3d4d1c3 100644 --- a/erpnext/crm/doctype/twitter_settings/twitter_settings.js +++ b/erpnext/crm/doctype/twitter_settings/twitter_settings.js @@ -2,7 +2,7 @@ // For license information, please see license.txt frappe.ui.form.on('Twitter Settings', { - onload: function(frm){ + onload: function(frm) { if (frm.doc.session_status == 'Expired' && frm.doc.consumer_key && frm.doc.consumer_secret){ frappe.confirm( __('Session not valid, Do you want to login?'), @@ -14,10 +14,11 @@ frappe.ui.form.on('Twitter Settings', { } ); } + frm.dashboard.set_headline(__("For more information, {0}.", [`${__('Click here')}`])); }, - refresh: function(frm){ + refresh: function(frm) { let msg, color, flag=false; - if (frm.doc.session_status == "Active"){ + if (frm.doc.session_status == "Active") { msg = __("Session Active"); color = 'green'; flag = true; @@ -28,7 +29,7 @@ frappe.ui.form.on('Twitter Settings', { flag = true; } - if (flag){ + if (flag) { frm.dashboard.set_headline_alert( `
@@ -38,7 +39,7 @@ frappe.ui.form.on('Twitter Settings', { ); } }, - login: function(frm){ + login: function(frm) { if (frm.doc.consumer_key && frm.doc.consumer_secret){ frappe.dom.freeze(); frappe.call({ @@ -52,7 +53,7 @@ frappe.ui.form.on('Twitter Settings', { }); } }, - after_save: function(frm){ + after_save: function(frm) { frm.trigger("login"); } }); diff --git a/erpnext/crm/doctype/twitter_settings/twitter_settings.json b/erpnext/crm/doctype/twitter_settings/twitter_settings.json index 36776e5c202..8d05877f060 100644 --- a/erpnext/crm/doctype/twitter_settings/twitter_settings.json +++ b/erpnext/crm/doctype/twitter_settings/twitter_settings.json @@ -2,6 +2,7 @@ "actions": [], "creation": "2020-01-30 10:29:08.562108", "doctype": "DocType", + "documentation": "https://docs.erpnext.com/docs/user/manual/en/CRM/twitter-settings", "editable_grid": 1, "engine": "InnoDB", "field_order": [ @@ -77,7 +78,7 @@ "image_field": "profile_pic", "issingle": 1, "links": [], - "modified": "2020-05-13 17:50:47.934776", + "modified": "2021-02-18 15:18:07.900031", "modified_by": "Administrator", "module": "CRM", "name": "Twitter Settings", diff --git a/erpnext/crm/doctype/twitter_settings/twitter_settings.py b/erpnext/crm/doctype/twitter_settings/twitter_settings.py index 1e1beab2d25..47756560ec5 100644 --- a/erpnext/crm/doctype/twitter_settings/twitter_settings.py +++ b/erpnext/crm/doctype/twitter_settings/twitter_settings.py @@ -32,7 +32,9 @@ class TwitterSettings(Document): try: auth.get_access_token(oauth_verifier) - api = self.get_api(auth.access_token, auth.access_token_secret) + self.access_token = auth.access_token + self.access_token_secret = auth.access_token_secret + api = self.get_api() user = api.me() profile_pic = (user._json["profile_image_url"]).replace("_normal","") @@ -50,11 +52,11 @@ class TwitterSettings(Document): frappe.msgprint(_("Error! Failed to get access token.")) frappe.throw(_('Invalid Consumer Key or Consumer Secret Key')) - def get_api(self, access_token, access_token_secret): - # authentication of consumer key and secret - auth = tweepy.OAuthHandler(self.consumer_key, self.get_password(fieldname="consumer_secret")) - # authentication of access token and secret - auth.set_access_token(access_token, access_token_secret) + def get_api(self): + # authentication of consumer key and secret + auth = tweepy.OAuthHandler(self.consumer_key, self.get_password(fieldname="consumer_secret")) + # authentication of access token and secret + auth.set_access_token(self.access_token, self.access_token_secret) return tweepy.API(auth) @@ -68,13 +70,13 @@ class TwitterSettings(Document): def upload_image(self, media): media = get_file_path(media) - api = self.get_api(self.access_token, self.access_token_secret) + api = self.get_api() media = api.media_upload(media) return media.media_id def send_tweet(self, text, media_id=None): - api = self.get_api(self.access_token, self.access_token_secret) + api = self.get_api() try: if media_id: response = api.update_status(status = text, media_ids = [media_id]) @@ -84,12 +86,32 @@ class TwitterSettings(Document): return response except TweepError as e: - content = json.loads(e.response.content) - content = content["errors"][0] - if e.response.status_code == 401: - self.db_set("session_status", "Expired") - frappe.db.commit() - frappe.throw(content["message"],title="Twitter Error {0} {1}".format(e.response.status_code, e.response.reason)) + self.api_error(e) + + def delete_tweet(self, tweet_id): + api = self.get_api() + try: + api.destroy_status(tweet_id) + except TweepError as e: + self.api_error(e) + + def get_tweet(self, tweet_id): + api = self.get_api() + try: + response = api.get_status(tweet_id, trim_user=True, include_entities=True) + except TweepError as e: + self.api_error(e) + + return response._json + + def api_error(self, e): + content = json.loads(e.response.content) + content = content["errors"][0] + if e.response.status_code == 401: + self.db_set("session_status", "Expired") + frappe.db.commit() + frappe.throw(content["message"],title=_("Twitter Error {0} : {1}").format(e.response.status_code, e.response.reason)) + @frappe.whitelist(allow_guest=True) def callback(oauth_token = None, oauth_verifier = None): diff --git a/erpnext/crm/report/campaign_efficiency/campaign_efficiency.js b/erpnext/crm/report/campaign_efficiency/campaign_efficiency.js index 0bc77a3f2a8..f29c2c64e14 100644 --- a/erpnext/crm/report/campaign_efficiency/campaign_efficiency.js +++ b/erpnext/crm/report/campaign_efficiency/campaign_efficiency.js @@ -16,4 +16,3 @@ frappe.query_reports["Campaign Efficiency"] = { } ] }; - diff --git a/erpnext/crm/report/campaign_efficiency/campaign_efficiency.py b/erpnext/crm/report/campaign_efficiency/campaign_efficiency.py index ec498837f5e..238884b5190 100644 --- a/erpnext/crm/report/campaign_efficiency/campaign_efficiency.py +++ b/erpnext/crm/report/campaign_efficiency/campaign_efficiency.py @@ -132,4 +132,4 @@ def get_order_amount(leads): where prevdoc_docname in ( select name from `tabQuotation` where status = 'Ordered' and quotation_to = 'Lead' and party_name in (%s) - )""" % ', '.join(["%s"]*len(leads)), tuple(leads))[0][0] \ No newline at end of file + )""" % ', '.join(["%s"]*len(leads)), tuple(leads))[0][0] diff --git a/erpnext/crm/report/lead_conversion_time/lead_conversion_time.js b/erpnext/crm/report/lead_conversion_time/lead_conversion_time.js index 0325de9b8d9..eeb8984513e 100644 --- a/erpnext/crm/report/lead_conversion_time/lead_conversion_time.js +++ b/erpnext/crm/report/lead_conversion_time/lead_conversion_time.js @@ -20,5 +20,3 @@ frappe.query_reports["Lead Conversion Time"] = { }, ] }; - - diff --git a/erpnext/crm/report/lead_details/lead_details.js b/erpnext/crm/report/lead_details/lead_details.js index f92070daf3f..2f6d24224fb 100644 --- a/erpnext/crm/report/lead_details/lead_details.js +++ b/erpnext/crm/report/lead_details/lead_details.js @@ -49,4 +49,4 @@ frappe.query_reports["Lead Details"] = { "options": "Territory", } ] -}; \ No newline at end of file +}; diff --git a/erpnext/crm/report/lead_details/lead_details.py b/erpnext/crm/report/lead_details/lead_details.py index eeaaec2bce2..072a47611b7 100644 --- a/erpnext/crm/report/lead_details/lead_details.py +++ b/erpnext/crm/report/lead_details/lead_details.py @@ -107,7 +107,7 @@ def get_columns(): "options": "Country", "width": 100 }, - + ] return columns @@ -142,7 +142,7 @@ def get_data(filters): company = %(company)s AND `tabLead`.creation BETWEEN %(from_date)s AND %(to_date)s {conditions} - ORDER BY + ORDER BY `tabLead`.creation asc """.format(conditions=get_conditions(filters)), filters, as_dict=1) def get_conditions(filters) : @@ -153,6 +153,5 @@ def get_conditions(filters) : if filters.get("status"): conditions.append(" and `tabLead`.status=%(status)s") - - return " ".join(conditions) if conditions else "" + return " ".join(conditions) if conditions else "" diff --git a/erpnext/crm/report/lost_opportunity/lost_opportunity.js b/erpnext/crm/report/lost_opportunity/lost_opportunity.js index d79f8c8480f..97c56f8c434 100644 --- a/erpnext/crm/report/lost_opportunity/lost_opportunity.js +++ b/erpnext/crm/report/lost_opportunity/lost_opportunity.js @@ -64,4 +64,4 @@ frappe.query_reports["Lost Opportunity"] = { "options": "User" }, ] -}; \ No newline at end of file +}; diff --git a/erpnext/crm/report/lost_opportunity/lost_opportunity.py b/erpnext/crm/report/lost_opportunity/lost_opportunity.py index 1aa4afe1865..858dcc4da81 100644 --- a/erpnext/crm/report/lost_opportunity/lost_opportunity.py +++ b/erpnext/crm/report/lost_opportunity/lost_opportunity.py @@ -87,17 +87,17 @@ def get_data(filters): `tabOpportunity`.sales_stage, `tabOpportunity`.territory FROM - `tabOpportunity` + `tabOpportunity` {join} WHERE `tabOpportunity`.status = 'Lost' and `tabOpportunity`.company = %(company)s - AND `tabOpportunity`.modified BETWEEN %(from_date)s AND %(to_date)s - {conditions} - GROUP BY - `tabOpportunity`.name - ORDER BY + AND `tabOpportunity`.modified BETWEEN %(from_date)s AND %(to_date)s + {conditions} + GROUP BY + `tabOpportunity`.name + ORDER BY `tabOpportunity`.creation asc """.format(conditions=get_conditions(filters), join=get_join(filters)), filters, as_dict=1) - + def get_conditions(filters): conditions = [] @@ -117,15 +117,15 @@ def get_conditions(filters): return " ".join(conditions) if conditions else "" def get_join(filters): - join = """LEFT JOIN `tabOpportunity Lost Reason Detail` - ON `tabOpportunity Lost Reason Detail`.parenttype = 'Opportunity' and + join = """LEFT JOIN `tabOpportunity Lost Reason Detail` + ON `tabOpportunity Lost Reason Detail`.parenttype = 'Opportunity' and `tabOpportunity Lost Reason Detail`.parent = `tabOpportunity`.name""" if filters.get("lost_reason"): - join = """JOIN `tabOpportunity Lost Reason Detail` - ON `tabOpportunity Lost Reason Detail`.parenttype = 'Opportunity' and + join = """JOIN `tabOpportunity Lost Reason Detail` + ON `tabOpportunity Lost Reason Detail`.parenttype = 'Opportunity' and `tabOpportunity Lost Reason Detail`.parent = `tabOpportunity`.name and `tabOpportunity Lost Reason Detail`.lost_reason = '{0}' """.format(filters.get("lost_reason")) - - return join \ No newline at end of file + + return join diff --git a/erpnext/crm/report/prospects_engaged_but_not_converted/prospects_engaged_but_not_converted.py b/erpnext/crm/report/prospects_engaged_but_not_converted/prospects_engaged_but_not_converted.py index 3a9d57d6075..425b7a8fdd7 100644 --- a/erpnext/crm/report/prospects_engaged_but_not_converted/prospects_engaged_but_not_converted.py +++ b/erpnext/crm/report/prospects_engaged_but_not_converted/prospects_engaged_but_not_converted.py @@ -106,4 +106,4 @@ def get_lead_filters(filters): return lead_filters def get_creation_date_based_on_lead_age(filters): - return add_days(now(), (filters.get('lead_age') * -1)) \ No newline at end of file + return add_days(now(), (filters.get('lead_age') * -1)) diff --git a/erpnext/demo/domains.py b/erpnext/demo/domains.py index d5c2bfd2f02..b1db7b57b17 100644 --- a/erpnext/demo/domains.py +++ b/erpnext/demo/domains.py @@ -25,4 +25,4 @@ data = { 'Non Profit': { 'company_name': 'Erpnext Foundation' } -} \ No newline at end of file +} diff --git a/erpnext/demo/user/education.py b/erpnext/demo/user/education.py index fc31176e1e5..883a6d88cf2 100644 --- a/erpnext/demo/user/education.py +++ b/erpnext/demo/user/education.py @@ -19,7 +19,7 @@ def work(): approve_random_student_applicant() enroll_random_student(frappe.flags.current_date) # if frappe.flags.current_date.weekday()== 0: - # make_course_schedule(frappe.flags.current_date, frappe.utils.add_days(frappe.flags.current_date, 5)) + # make_course_schedule(frappe.flags.current_date, frappe.utils.add_days(frappe.flags.current_date, 5)) mark_student_attendance(frappe.flags.current_date) # make_assessment_plan() make_fees() @@ -48,7 +48,7 @@ def enroll_random_student(current_date): frappe.db.commit() assign_student_group(enrollment.student, enrollment.student_name, enrollment.program, enrolled_courses, enrollment.student_batch_name) - + def assign_student_group(student, student_name, program, courses, batch): course_list = [d["course"] for d in courses] for d in frappe.get_list("Student Group", fields=("name"), filters={"program": program, "course":("in", course_list), "disabled": 0}): @@ -69,11 +69,11 @@ def mark_student_attendance(current_date): students = get_student_group_students(d.name) for stud in students: make_attendance_records(stud.student, stud.student_name, status[weighted_choice([9,4])], None, d.name, current_date) - + def make_fees(): for d in range(1,10): random_fee = get_random("Fees", {"paid_amount": 0}) - collect_fees(random_fee, frappe.db.get_value("Fees", random_fee, "outstanding_amount")) + collect_fees(random_fee, frappe.db.get_value("Fees", random_fee, "outstanding_amount")) def make_assessment_plan(date): for d in range(1,4): @@ -84,7 +84,7 @@ def make_assessment_plan(date): doc.assessment_group = get_random("Assessment Group", {"is_group": 0, "parent": "2017-18 (Semester 2)"}) doc.grading_scale = get_random("Grading Scale") doc.maximum_assessment_score = 100 - + def make_course_schedule(start_date, end_date): for d in frappe.db.get_list("Student Group"): cs = frappe.new_doc("Scheduling Tool") @@ -114,4 +114,4 @@ def weighted_choice(weights): rnd = random.random() * running_total for i, total in enumerate(totals): if rnd < total: - return i \ No newline at end of file + return i diff --git a/erpnext/domains/agriculture.py b/erpnext/domains/agriculture.py index 8c7427ab2d1..9212d2ea719 100644 --- a/erpnext/domains/agriculture.py +++ b/erpnext/domains/agriculture.py @@ -25,4 +25,4 @@ data = { ], 'default_portal_role': 'System Manager', 'on_setup': 'erpnext.agriculture.setup.setup_agriculture' -} \ No newline at end of file +} diff --git a/erpnext/domains/education.py b/erpnext/domains/education.py index bbaa6e55d99..870624ab3b2 100644 --- a/erpnext/domains/education.py +++ b/erpnext/domains/education.py @@ -26,4 +26,4 @@ data = { ], 'on_setup': 'erpnext.education.setup.setup_education' -} \ No newline at end of file +} diff --git a/erpnext/domains/manufacturing.py b/erpnext/domains/manufacturing.py index 259ee9238e5..b9ad49e772b 100644 --- a/erpnext/domains/manufacturing.py +++ b/erpnext/domains/manufacturing.py @@ -21,4 +21,4 @@ data = { ['Stock Settings', None, 'show_barcode_field', 1] ], 'default_portal_role': 'Customer' -} \ No newline at end of file +} diff --git a/erpnext/domains/non_profit.py b/erpnext/domains/non_profit.py index b6772c53153..7c4f6b1f9de 100644 --- a/erpnext/domains/non_profit.py +++ b/erpnext/domains/non_profit.py @@ -21,4 +21,4 @@ data = { 'Non Profit' ], 'default_portal_role': 'Non Profit Manager' -} \ No newline at end of file +} diff --git a/erpnext/domains/services.py b/erpnext/domains/services.py index 7a4ffc4993f..89213720767 100644 --- a/erpnext/domains/services.py +++ b/erpnext/domains/services.py @@ -18,4 +18,4 @@ data = { ['Stock Settings', None, 'show_barcode_field', 0] ], 'default_portal_role': 'Customer' -} \ No newline at end of file +} diff --git a/erpnext/education/api.py b/erpnext/education/api.py index afa0be9b9f3..4493a3fef17 100644 --- a/erpnext/education/api.py +++ b/erpnext/education/api.py @@ -34,11 +34,14 @@ def enroll_student(source_name): } }}, ignore_permissions=True) student.save() + + student_applicant = frappe.db.get_value("Student Applicant", source_name, + ["student_category", "program"], as_dict=True) program_enrollment = frappe.new_doc("Program Enrollment") program_enrollment.student = student.name - program_enrollment.student_category = student.student_category + program_enrollment.student_category = student_applicant.student_category program_enrollment.student_name = student.title - program_enrollment.program = frappe.db.get_value("Student Applicant", source_name, "program") + program_enrollment.program = student_applicant.program frappe.publish_realtime('enroll_student_progress', {"progress": [2, 4]}, user=frappe.session.user) return program_enrollment diff --git a/erpnext/education/doctype/academic_term/academic_term.py b/erpnext/education/doctype/academic_term/academic_term.py index 3aa0be157bc..fa7f2899dcb 100644 --- a/erpnext/education/doctype/academic_term/academic_term.py +++ b/erpnext/education/doctype/academic_term/academic_term.py @@ -22,9 +22,9 @@ class AcademicTerm(Document): and getdate(self.term_start_date) > getdate(self.term_end_date): frappe.throw(_("The Term End Date cannot be earlier than the Term Start Date. Please correct the dates and try again.")) - # Check that the start of the term is not before the start of the academic year + # Check that the start of the term is not before the start of the academic year # and end of term is not after the end of the academic year""" - + year = frappe.get_doc("Academic Year",self.academic_year) if self.term_start_date and getdate(year.year_start_date) and (getdate(self.term_start_date) < getdate(year.year_start_date)): frappe.throw(_("The Term Start Date cannot be earlier than the Year Start Date of the Academic Year to which the term is linked (Academic Year {}). Please correct the dates and try again.").format(self.academic_year)) diff --git a/erpnext/education/doctype/academic_term/academic_term_dashboard.py b/erpnext/education/doctype/academic_term/academic_term_dashboard.py index 871e0f32845..eb2f90742ce 100644 --- a/erpnext/education/doctype/academic_term/academic_term_dashboard.py +++ b/erpnext/education/doctype/academic_term/academic_term_dashboard.py @@ -22,4 +22,4 @@ def get_data(): 'items': ['Assessment Plan', 'Assessment Result'] } ] - } \ No newline at end of file + } diff --git a/erpnext/education/doctype/academic_term/test_academic_term.js b/erpnext/education/doctype/academic_term/test_academic_term.js index 6d91e977c63..383b65a7032 100644 --- a/erpnext/education/doctype/academic_term/test_academic_term.js +++ b/erpnext/education/doctype/academic_term/test_academic_term.js @@ -21,4 +21,4 @@ QUnit.test('Test: Academic Term', function(assert){ }, () => done() ]); -}); \ No newline at end of file +}); diff --git a/erpnext/education/doctype/academic_year/academic_year.js b/erpnext/education/doctype/academic_year/academic_year.js index 0e8619849cd..20e25281ffc 100644 --- a/erpnext/education/doctype/academic_year/academic_year.js +++ b/erpnext/education/doctype/academic_year/academic_year.js @@ -1,2 +1,2 @@ frappe.ui.form.on("Academic Year", { -}); \ No newline at end of file +}); diff --git a/erpnext/education/doctype/academic_year/academic_year_dashboard.py b/erpnext/education/doctype/academic_year/academic_year_dashboard.py index f27f7d14cf6..d3734df8036 100644 --- a/erpnext/education/doctype/academic_year/academic_year_dashboard.py +++ b/erpnext/education/doctype/academic_year/academic_year_dashboard.py @@ -22,4 +22,4 @@ def get_data(): 'items': ['Assessment Plan', 'Assessment Result'] } ] - } \ No newline at end of file + } 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 ec2f49c5a1b..00000000000 --- 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() - ]); -}); \ No newline at end of file diff --git a/erpnext/education/doctype/article/article.js b/erpnext/education/doctype/article/article.js index edfec26273b..85b387f6217 100644 --- a/erpnext/education/doctype/article/article.js +++ b/erpnext/education/doctype/article/article.js @@ -53,4 +53,4 @@ let get_topics_without_article = function(article) { method: 'erpnext.education.doctype.article.article.get_topics_without_article', args: {'article': article} }); -}; \ No newline at end of file +}; diff --git a/erpnext/education/doctype/article/article.py b/erpnext/education/doctype/article/article.py index 8ba367da76e..b5cc5cbc7a6 100644 --- a/erpnext/education/doctype/article/article.py +++ b/erpnext/education/doctype/article/article.py @@ -18,4 +18,4 @@ def get_topics_without_article(article): topic_contents = [tc.content for tc in topic.topic_content] if not topic_contents or article not in topic_contents: data.append(topic.name) - return data \ No newline at end of file + return data diff --git a/erpnext/education/doctype/article/test_article.js b/erpnext/education/doctype/article/test_article.js deleted file mode 100644 index 9dbf063e844..00000000000 --- 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/assessment_criteria/assessment_criteria.py b/erpnext/education/doctype/assessment_criteria/assessment_criteria.py index 1ea37023b24..bfbf26cf6c1 100644 --- a/erpnext/education/doctype/assessment_criteria/assessment_criteria.py +++ b/erpnext/education/doctype/assessment_criteria/assessment_criteria.py @@ -12,4 +12,4 @@ STD_CRITERIA = ["total", "total score", "total grade", "maximum score", "score", class AssessmentCriteria(Document): def validate(self): if self.assessment_criteria.lower() in STD_CRITERIA: - frappe.throw(_("Can't create standard criteria. Please rename the criteria")) \ No newline at end of file + frappe.throw(_("Can't create standard criteria. Please rename the criteria")) diff --git a/erpnext/education/doctype/assessment_criteria/test_assessment_criteria.js b/erpnext/education/doctype/assessment_criteria/test_assessment_criteria.js index db4a4cf5a8d..724c4dac499 100644 --- a/erpnext/education/doctype/assessment_criteria/test_assessment_criteria.js +++ b/erpnext/education/doctype/assessment_criteria/test_assessment_criteria.js @@ -13,4 +13,4 @@ QUnit.test('Test: Assessment Criteria', function(assert){ }, () => done() ]); -}); \ No newline at end of file +}); diff --git a/erpnext/education/doctype/assessment_criteria_group/test_assessment_criteria_group.js b/erpnext/education/doctype/assessment_criteria_group/test_assessment_criteria_group.js index bcfcaf82e63..ab27e637239 100644 --- a/erpnext/education/doctype/assessment_criteria_group/test_assessment_criteria_group.js +++ b/erpnext/education/doctype/assessment_criteria_group/test_assessment_criteria_group.js @@ -12,4 +12,4 @@ QUnit.test('Test: Assessment Criteria Group', function(assert){ }, () => done() ]); -}); \ No newline at end of file +}); diff --git a/erpnext/education/doctype/assessment_group/assessment_group_dashboard.py b/erpnext/education/doctype/assessment_group/assessment_group_dashboard.py index 2649d4b90c9..1a23606a61d 100644 --- a/erpnext/education/doctype/assessment_group/assessment_group_dashboard.py +++ b/erpnext/education/doctype/assessment_group/assessment_group_dashboard.py @@ -12,4 +12,4 @@ def get_data(): 'items': ['Assessment Plan', 'Assessment Result'] } ] - } \ No newline at end of file + } diff --git a/erpnext/education/doctype/assessment_group/assessment_group_tree.js b/erpnext/education/doctype/assessment_group/assessment_group_tree.js index e4676831a3b..e0dfaa31fd7 100644 --- a/erpnext/education/doctype/assessment_group/assessment_group_tree.js +++ b/erpnext/education/doctype/assessment_group/assessment_group_tree.js @@ -1,3 +1,3 @@ frappe.treeview_settings["Assessment Group"] = { - -} \ No newline at end of file + +} diff --git a/erpnext/education/doctype/assessment_group/test_assessment_group.js b/erpnext/education/doctype/assessment_group/test_assessment_group.js index a127fd4adf5..00e6309837d 100644 --- a/erpnext/education/doctype/assessment_group/test_assessment_group.js +++ b/erpnext/education/doctype/assessment_group/test_assessment_group.js @@ -62,4 +62,4 @@ frappe.map_group = { () => frappe.click_button('Create New'), ]); } -}; \ No newline at end of file +}; diff --git a/erpnext/education/doctype/assessment_plan/assessment_plan.js b/erpnext/education/doctype/assessment_plan/assessment_plan.js index 726c0fcecd4..cf545c41afb 100644 --- a/erpnext/education/doctype/assessment_plan/assessment_plan.js +++ b/erpnext/education/doctype/assessment_plan/assessment_plan.js @@ -75,4 +75,4 @@ frappe.ui.form.on('Assessment Plan', { maximum_assessment_score: function(frm) { frm.trigger('course'); } -}); \ No newline at end of file +}); diff --git a/erpnext/education/doctype/assessment_plan/assessment_plan_dashboard.py b/erpnext/education/doctype/assessment_plan/assessment_plan_dashboard.py index 5e6c29dcdf3..8ac3faf6dde 100644 --- a/erpnext/education/doctype/assessment_plan/assessment_plan_dashboard.py +++ b/erpnext/education/doctype/assessment_plan/assessment_plan_dashboard.py @@ -18,4 +18,4 @@ def get_data(): 'items': ['Assessment Plan Status'] } ] - } \ No newline at end of file + } diff --git a/erpnext/education/doctype/assessment_result/assessment_result.js b/erpnext/education/doctype/assessment_result/assessment_result.js index c35f607a99d..b6d28817b5d 100644 --- a/erpnext/education/doctype/assessment_result/assessment_result.js +++ b/erpnext/education/doctype/assessment_result/assessment_result.js @@ -122,4 +122,4 @@ frappe.ui.form.on('Assessment Result Detail', { }); } } -}); \ No newline at end of file +}); diff --git a/erpnext/education/doctype/assessment_result/assessment_result.py b/erpnext/education/doctype/assessment_result/assessment_result.py index 6b873ecf97a..7dfe0cf6c27 100644 --- a/erpnext/education/doctype/assessment_result/assessment_result.py +++ b/erpnext/education/doctype/assessment_result/assessment_result.py @@ -42,7 +42,3 @@ class AssessmentResult(Document): "student":self.student, "assessment_plan":self.assessment_plan, "docstatus":("!=", 2)}) if assessment_result: frappe.throw(_("Assessment Result record {0} already exists.").format(getlink("Assessment Result",assessment_result[0].name))) - - - - diff --git a/erpnext/education/doctype/assessment_result/assessment_result_dashboard.py b/erpnext/education/doctype/assessment_result/assessment_result_dashboard.py index 438379d08e4..2526076d308 100644 --- a/erpnext/education/doctype/assessment_result/assessment_result_dashboard.py +++ b/erpnext/education/doctype/assessment_result/assessment_result_dashboard.py @@ -11,4 +11,4 @@ def get_data(): 'items': ['Final Assessment Grades', 'Course wise Assessment Report'] } ] - } \ No newline at end of file + } diff --git a/erpnext/education/doctype/assessment_result/test_assessment_result.js b/erpnext/education/doctype/assessment_result/test_assessment_result.js index b7adfacb1a2..d4eb4b8ba66 100644 --- a/erpnext/education/doctype/assessment_result/test_assessment_result.js +++ b/erpnext/education/doctype/assessment_result/test_assessment_result.js @@ -70,4 +70,4 @@ QUnit.test('Test: Assessment Result', function(assert){ () => done() ]); -}); \ No newline at end of file +}); diff --git a/erpnext/education/doctype/assessment_result/test_assessment_result.py b/erpnext/education/doctype/assessment_result/test_assessment_result.py index e5535d6085a..adce57769dd 100644 --- a/erpnext/education/doctype/assessment_result/test_assessment_result.py +++ b/erpnext/education/doctype/assessment_result/test_assessment_result.py @@ -16,4 +16,3 @@ class TestAssessmentResult(unittest.TestCase): grade = get_grade("_Test Grading Scale", 70) self.assertEqual("B", grade) - \ No newline at end of file diff --git a/erpnext/education/doctype/assessment_result_tool/assessment_result_tool.py b/erpnext/education/doctype/assessment_result_tool/assessment_result_tool.py index 649f420d41f..a0d286ccbe9 100644 --- a/erpnext/education/doctype/assessment_result_tool/assessment_result_tool.py +++ b/erpnext/education/doctype/assessment_result_tool/assessment_result_tool.py @@ -7,4 +7,4 @@ import frappe from frappe.model.document import Document class AssessmentResultTool(Document): - pass \ No newline at end of file + pass diff --git a/erpnext/education/doctype/assessment_result_tool/test_assessment_result_tool.js b/erpnext/education/doctype/assessment_result_tool/test_assessment_result_tool.js index 0bbe33194a3..7ef5c688fb9 100644 --- a/erpnext/education/doctype/assessment_result_tool/test_assessment_result_tool.js +++ b/erpnext/education/doctype/assessment_result_tool/test_assessment_result_tool.js @@ -26,4 +26,4 @@ QUnit.test('Test: Assessment Result Tool', function(assert){ }, () => done() ]); -}); \ No newline at end of file +}); 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 cc869a87fcf..00000000000 --- 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/course.js b/erpnext/education/doctype/course/course.js index 81e4a8c08d3..bd8d62c8d2a 100644 --- a/erpnext/education/doctype/course/course.js +++ b/erpnext/education/doctype/course/course.js @@ -76,4 +76,4 @@ let get_programs_without_course = function(course) { method: 'erpnext.education.doctype.course.course.get_programs_without_course', args: {'course': course} }); -} \ No newline at end of file +} diff --git a/erpnext/education/doctype/course/course.py b/erpnext/education/doctype/course/course.py index 06efa54e770..92f92ed9f3e 100644 --- a/erpnext/education/doctype/course/course.py +++ b/erpnext/education/doctype/course/course.py @@ -53,4 +53,4 @@ def get_programs_without_course(course): courses = [c.course for c in program.courses] if not courses or course not in courses: data.append(program.name) - return data \ No newline at end of file + return data diff --git a/erpnext/education/doctype/course/course_dashboard.py b/erpnext/education/doctype/course/course_dashboard.py index 8a570bdc579..8de91b1c092 100644 --- a/erpnext/education/doctype/course/course_dashboard.py +++ b/erpnext/education/doctype/course/course_dashboard.py @@ -20,4 +20,4 @@ def get_data(): 'items': ['Assessment Plan', 'Assessment Result'] }, ] - } \ No newline at end of file + } diff --git a/erpnext/education/doctype/course/test_course.js b/erpnext/education/doctype/course/test_course.js index 88fddc2bb6d..2b6860cb7f4 100644 --- a/erpnext/education/doctype/course/test_course.js +++ b/erpnext/education/doctype/course/test_course.js @@ -33,4 +33,4 @@ QUnit.test('test course', function(assert) { }, () => done() ]); -}); \ No newline at end of file +}); diff --git a/erpnext/education/doctype/course_activity/course_activity.py b/erpnext/education/doctype/course_activity/course_activity.py index e7fc08a1d7f..3aa1ea0c5b3 100644 --- a/erpnext/education/doctype/course_activity/course_activity.py +++ b/erpnext/education/doctype/course_activity/course_activity.py @@ -16,4 +16,4 @@ class CourseActivity(Document): if frappe.db.exists("Course Enrollment", self.enrollment): return True else: - frappe.throw(_("Course Enrollment {0} does not exists").format(self.enrollment)) \ No newline at end of file + frappe.throw(_("Course Enrollment {0} does not exists").format(self.enrollment)) 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 c89c89e5d3c..00000000000 --- 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 786e67e9a37..00000000000 --- 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/course_enrollment.py b/erpnext/education/doctype/course_enrollment/course_enrollment.py index 2b3acf1b93b..ce88990a70d 100644 --- a/erpnext/education/doctype/course_enrollment/course_enrollment.py +++ b/erpnext/education/doctype/course_enrollment/course_enrollment.py @@ -91,4 +91,4 @@ def check_activity_exists(enrollment, content_type, content): if activity: return activity[0].name else: - return None \ No newline at end of file + return None diff --git a/erpnext/education/doctype/course_enrollment/course_enrollment_dashboard.py b/erpnext/education/doctype/course_enrollment/course_enrollment_dashboard.py index b9dd457b61c..37972fe354c 100644 --- a/erpnext/education/doctype/course_enrollment/course_enrollment_dashboard.py +++ b/erpnext/education/doctype/course_enrollment/course_enrollment_dashboard.py @@ -12,4 +12,4 @@ def get_data(): 'items': ['Course Activity', 'Quiz Activity'] } ] - } \ No newline at end of file + } 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 216cc307991..00000000000 --- 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_enrollment/test_course_enrollment.py b/erpnext/education/doctype/course_enrollment/test_course_enrollment.py index e22c7ce0bab..874bf121f47 100644 --- a/erpnext/education/doctype/course_enrollment/test_course_enrollment.py +++ b/erpnext/education/doctype/course_enrollment/test_course_enrollment.py @@ -39,6 +39,3 @@ class TestCourseEnrollment(unittest.TestCase): doc = frappe.get_doc("Program Enrollment", entry.name) doc.cancel() doc.delete() - - - diff --git a/erpnext/education/doctype/course_schedule/course_schedule.js b/erpnext/education/doctype/course_schedule/course_schedule.js index 4275f6ef2a6..366bbd8b0d4 100644 --- a/erpnext/education/doctype/course_schedule/course_schedule.js +++ b/erpnext/education/doctype/course_schedule/course_schedule.js @@ -13,4 +13,4 @@ frappe.ui.form.on("Course Schedule", { }).addClass("btn-primary"); } } -}); \ No newline at end of file +}); diff --git a/erpnext/education/doctype/course_schedule/course_schedule.py b/erpnext/education/doctype/course_schedule/course_schedule.py index 5083ff6589d..748728d104e 100644 --- a/erpnext/education/doctype/course_schedule/course_schedule.py +++ b/erpnext/education/doctype/course_schedule/course_schedule.py @@ -14,11 +14,11 @@ class CourseSchedule(Document): self.validate_course() self.validate_date() self.validate_overlap() - + def set_title(self): """Set document Title""" self.title = self.course + " by " + (self.instructor_name if self.instructor_name else self.instructor) - + def validate_course(self): group_based_on, course = frappe.db.get_value("Student Group", self.student_group, ["group_based_on", "course"]) if group_based_on == "Course": @@ -28,23 +28,22 @@ class CourseSchedule(Document): """Validates if from_time is greater than to_time""" if self.from_time > self.to_time: frappe.throw(_("From Time cannot be greater than To Time.")) - + def validate_overlap(self): """Validates overlap for Student Group, Instructor, Room""" - + from erpnext.education.utils import validate_overlap_for #Validate overlapping course schedules. if self.student_group: validate_overlap_for(self, "Course Schedule", "student_group") - + validate_overlap_for(self, "Course Schedule", "instructor") validate_overlap_for(self, "Course Schedule", "room") #validate overlapping assessment schedules. if self.student_group: validate_overlap_for(self, "Assessment Plan", "student_group") - + validate_overlap_for(self, "Assessment Plan", "room") validate_overlap_for(self, "Assessment Plan", "supervisor", self.instructor) - diff --git a/erpnext/education/doctype/course_schedule/course_schedule_dashboard.py b/erpnext/education/doctype/course_schedule/course_schedule_dashboard.py index 0866cd65357..22ce7e1ec24 100644 --- a/erpnext/education/doctype/course_schedule/course_schedule_dashboard.py +++ b/erpnext/education/doctype/course_schedule/course_schedule_dashboard.py @@ -12,4 +12,4 @@ def get_data(): 'items': ['Student Attendance'] } ] - } \ No newline at end of file + } 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 5cdb67be482..00000000000 --- 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_schedule/test_course_schedule.py b/erpnext/education/doctype/course_schedule/test_course_schedule.py index a901f1e467a..5bb4de85846 100644 --- a/erpnext/education/doctype/course_schedule/test_course_schedule.py +++ b/erpnext/education/doctype/course_schedule/test_course_schedule.py @@ -17,28 +17,28 @@ class TestCourseSchedule(unittest.TestCase): def test_student_group_conflict(self): cs1 = make_course_schedule_test_record(simulate= True) - cs2 = make_course_schedule_test_record(schedule_date=cs1.schedule_date, from_time= cs1.from_time, + cs2 = make_course_schedule_test_record(schedule_date=cs1.schedule_date, from_time= cs1.from_time, to_time= cs1.to_time, instructor="_Test Instructor 2", room=frappe.get_all("Room")[1].name, do_not_save= 1) self.assertRaises(OverlapError, cs2.save) def test_instructor_conflict(self): cs1 = make_course_schedule_test_record(simulate= True) - - cs2 = make_course_schedule_test_record(from_time= cs1.from_time, to_time= cs1.to_time, + + cs2 = make_course_schedule_test_record(from_time= cs1.from_time, to_time= cs1.to_time, student_group="Course-TC101-2014-2015 (_Test Academic Term)", room=frappe.get_all("Room")[1].name, do_not_save= 1) self.assertRaises(OverlapError, cs2.save) def test_room_conflict(self): cs1 = make_course_schedule_test_record(simulate= True) - - cs2 = make_course_schedule_test_record(from_time= cs1.from_time, to_time= cs1.to_time, + + cs2 = make_course_schedule_test_record(from_time= cs1.from_time, to_time= cs1.to_time, student_group="Course-TC101-2014-2015 (_Test Academic Term)", instructor="_Test Instructor 2", do_not_save= 1) self.assertRaises(OverlapError, cs2.save) - + def test_no_conflict(self): cs1 = make_course_schedule_test_record(simulate= True) - - make_course_schedule_test_record(from_time= cs1.from_time, to_time= cs1.to_time, + + make_course_schedule_test_record(from_time= cs1.from_time, to_time= cs1.to_time, student_group="Course-TC102-2014-2015 (_Test Academic Term)", instructor="_Test Instructor 2", room=frappe.get_all("Room")[1].name) def make_course_schedule_test_record(**args): @@ -49,12 +49,12 @@ def make_course_schedule_test_record(**args): course_schedule.course = args.course or "TC101" course_schedule.instructor = args.instructor or "_Test Instructor" course_schedule.room = args.room or frappe.get_all("Room")[0].name - + course_schedule.schedule_date = args.schedule_date or today() course_schedule.from_time = args.from_time or to_timedelta("01:00:00") course_schedule.to_time = args.to_time or course_schedule.from_time + datetime.timedelta(hours= 1) - + if not args.do_not_save: if args.simulate: while True: diff --git a/erpnext/education/doctype/course_scheduling_tool/course_scheduling_tool.js b/erpnext/education/doctype/course_scheduling_tool/course_scheduling_tool.js index d57f46ab98e..7b0e4ab47c8 100644 --- a/erpnext/education/doctype/course_scheduling_tool/course_scheduling_tool.js +++ b/erpnext/education/doctype/course_scheduling_tool/course_scheduling_tool.js @@ -41,4 +41,4 @@ frappe.ui.form.on('Course Scheduling Tool', { }); }); } -}); \ No newline at end of file +}); 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 4419d181167..00000000000 --- 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 d8d154fb9c8..00000000000 --- 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/education_settings/education_settings.py b/erpnext/education/doctype/education_settings/education_settings.py index 658380ea429..6c7e95c80da 100644 --- a/erpnext/education/doctype/education_settings/education_settings.py +++ b/erpnext/education/doctype/education_settings/education_settings.py @@ -36,4 +36,4 @@ class EducationSettings(Document): make_property_setter('Instructor', "naming_series", "hidden", 1, "Check", validate_fields_for_doctype=False) def update_website_context(context): - context["lms_enabled"] = frappe.get_doc("Education Settings").enable_lms \ No newline at end of file + context["lms_enabled"] = frappe.get_doc("Education Settings").enable_lms 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 a08ed33e8b6..00000000000 --- 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/fee_schedule.js b/erpnext/education/doctype/fee_schedule/fee_schedule.js index 0089957df40..97691a5b62a 100644 --- a/erpnext/education/doctype/fee_schedule/fee_schedule.js +++ b/erpnext/education/doctype/fee_schedule/fee_schedule.js @@ -130,4 +130,4 @@ frappe.ui.form.on('Fee Schedule Student Group', { }); } } -}) \ No newline at end of file +}) diff --git a/erpnext/education/doctype/fee_schedule/fee_schedule_dashboard.py b/erpnext/education/doctype/fee_schedule/fee_schedule_dashboard.py index acfe4002193..4d7da21ea17 100644 --- a/erpnext/education/doctype/fee_schedule/fee_schedule_dashboard.py +++ b/erpnext/education/doctype/fee_schedule/fee_schedule_dashboard.py @@ -10,4 +10,4 @@ def get_data(): 'items': ['Fees'] } ] - } \ No newline at end of file + } 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 d495b4ce7b1..00000000000 --- 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/fee_structure.js b/erpnext/education/doctype/fee_structure/fee_structure.js index 310c4105f47..d9ab99f8180 100644 --- a/erpnext/education/doctype/fee_structure/fee_structure.js +++ b/erpnext/education/doctype/fee_structure/fee_structure.js @@ -69,4 +69,4 @@ frappe.ui.form.on('Fee Component', { } frm.set_value('total_amount', total_amount); } -}); \ No newline at end of file +}); diff --git a/erpnext/education/doctype/fee_structure/fee_structure.py b/erpnext/education/doctype/fee_structure/fee_structure.py index 781382b51be..9755717ee94 100644 --- a/erpnext/education/doctype/fee_structure/fee_structure.py +++ b/erpnext/education/doctype/fee_structure/fee_structure.py @@ -11,13 +11,13 @@ from frappe.model.mapper import get_mapped_doc class FeeStructure(Document): def validate(self): self.calculate_total() - + def calculate_total(self): """Calculates total amount.""" self.total_amount = 0 for d in self.components: self.total_amount += d.amount - + @frappe.whitelist() def make_fee_schedule(source_name, target_doc=None): @@ -31,4 +31,4 @@ def make_fee_schedule(source_name, target_doc=None): "Fee Component": { "doctype": "Fee Component" } - }, target_doc) \ No newline at end of file + }, target_doc) diff --git a/erpnext/education/doctype/fee_structure/fee_structure_dashboard.py b/erpnext/education/doctype/fee_structure/fee_structure_dashboard.py index 73e314f3512..fdf7df7aa26 100644 --- a/erpnext/education/doctype/fee_structure/fee_structure_dashboard.py +++ b/erpnext/education/doctype/fee_structure/fee_structure_dashboard.py @@ -12,4 +12,4 @@ def get_data(): 'items': ['Fees', 'Fee Schedule'] } ] - } \ No newline at end of file + } 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 61f41354c34..00000000000 --- 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/fees/fees.py b/erpnext/education/doctype/fees/fees.py index 25d67d2d5f6..7e867049047 100644 --- a/erpnext/education/doctype/fees/fees.py +++ b/erpnext/education/doctype/fees/fees.py @@ -132,4 +132,4 @@ def get_list_context(context=None): "title": _("Fees"), "get_list": get_fee_list, "row_template": "templates/includes/fee/fee_row.html" - } \ No newline at end of file + } diff --git a/erpnext/education/doctype/fees/fees_list.js b/erpnext/education/doctype/fees/fees_list.js index 52e1c4beb5a..ee8e1e382e9 100644 --- a/erpnext/education/doctype/fees/fees_list.js +++ b/erpnext/education/doctype/fees/fees_list.js @@ -9,4 +9,4 @@ frappe.listview_settings['Fees'] = { return [__("Overdue"), "red", "outstanding_amount,>,0|due_date,<=,Today"]; } } -}; \ No newline at end of file +}; diff --git a/erpnext/education/doctype/grading_scale/grading_scale.py b/erpnext/education/doctype/grading_scale/grading_scale.py index 6309d02c155..0e732971619 100644 --- a/erpnext/education/doctype/grading_scale/grading_scale.py +++ b/erpnext/education/doctype/grading_scale/grading_scale.py @@ -17,4 +17,4 @@ class GradingScale(Document): else: thresholds.append(cint(d.threshold)) if 0 not in thresholds: - frappe.throw(_("Please define grade for Threshold 0%")) \ No newline at end of file + frappe.throw(_("Please define grade for Threshold 0%")) diff --git a/erpnext/education/doctype/grading_scale/test_grading_scale.js b/erpnext/education/doctype/grading_scale/test_grading_scale.js index e363545ff8d..fb56918fdb8 100644 --- a/erpnext/education/doctype/grading_scale/test_grading_scale.js +++ b/erpnext/education/doctype/grading_scale/test_grading_scale.js @@ -99,4 +99,4 @@ QUnit.test('Test: Grading Scale', function(assert){ () => done() ]); -}); \ No newline at end of file +}); diff --git a/erpnext/education/doctype/guardian/test_guardian.js b/erpnext/education/doctype/guardian/test_guardian.js index 9bbfacd5802..1ea6dc290bd 100644 --- a/erpnext/education/doctype/guardian/test_guardian.js +++ b/erpnext/education/doctype/guardian/test_guardian.js @@ -31,4 +31,4 @@ QUnit.test('Test: Guardian', function(assert){ }, () => done() ]); -}); \ No newline at end of file +}); diff --git a/erpnext/education/doctype/instructor/instructor.js b/erpnext/education/doctype/instructor/instructor.js index 24e80fa9378..034b0aaf5dd 100644 --- a/erpnext/education/doctype/instructor/instructor.js +++ b/erpnext/education/doctype/instructor/instructor.js @@ -61,4 +61,4 @@ frappe.ui.form.on("Instructor", { }; }); } -}); \ No newline at end of file +}); diff --git a/erpnext/education/doctype/instructor/instructor_dashboard.py b/erpnext/education/doctype/instructor/instructor_dashboard.py index a404fc56c54..c19c85947d6 100644 --- a/erpnext/education/doctype/instructor/instructor_dashboard.py +++ b/erpnext/education/doctype/instructor/instructor_dashboard.py @@ -21,4 +21,4 @@ def get_data(): 'items': ['Student Group'] } ] - } \ No newline at end of file + } diff --git a/erpnext/education/doctype/program/program.js b/erpnext/education/doctype/program/program.js index 98263b55a1f..2d89351504b 100644 --- a/erpnext/education/doctype/program/program.js +++ b/erpnext/education/doctype/program/program.js @@ -4,5 +4,5 @@ cur_frm.add_fetch('fee_structure', 'total_amount', 'amount'); frappe.ui.form.on("Program", "refresh", function(frm) { - -}); \ No newline at end of file + +}); diff --git a/erpnext/education/doctype/program/program.py b/erpnext/education/doctype/program/program.py index d24df5d6142..9d886b7b9e6 100644 --- a/erpnext/education/doctype/program/program.py +++ b/erpnext/education/doctype/program/program.py @@ -11,4 +11,4 @@ class Program(Document): def get_course_list(self): program_course_list = self.courses course_list = [frappe.get_doc("Course", program_course.course) for program_course in program_course_list] - return course_list \ No newline at end of file + return course_list diff --git a/erpnext/education/doctype/program/program_dashboard.py b/erpnext/education/doctype/program/program_dashboard.py index c5d249451f2..6c503e1bf1f 100644 --- a/erpnext/education/doctype/program/program_dashboard.py +++ b/erpnext/education/doctype/program/program_dashboard.py @@ -21,4 +21,4 @@ def get_data(): 'items': ['Assessment Plan', 'Assessment Result'] } ] - } \ No newline at end of file + } diff --git a/erpnext/education/doctype/program/test_program.js b/erpnext/education/doctype/program/test_program.js index dc347cf1b06..b9ca41ae3f9 100644 --- a/erpnext/education/doctype/program/test_program.js +++ b/erpnext/education/doctype/program/test_program.js @@ -31,4 +31,4 @@ QUnit.test('Test: Program', function(assert){ }, () => done() ]); -}); \ No newline at end of file +}); diff --git a/erpnext/education/doctype/program/test_program.py b/erpnext/education/doctype/program/test_program.py index d7530365117..204f2961e7f 100644 --- a/erpnext/education/doctype/program/test_program.py +++ b/erpnext/education/doctype/program/test_program.py @@ -88,4 +88,4 @@ def setup_program(): course_list = [course['course_name'] for course in test_data['course']] program = make_program_and_linked_courses(test_data['program_name'], course_list) - return program \ No newline at end of file + return program diff --git a/erpnext/education/doctype/program_enrollment/program_enrollment.js b/erpnext/education/doctype/program_enrollment/program_enrollment.js index f9c65fbbfb3..e92d063602d 100644 --- a/erpnext/education/doctype/program_enrollment/program_enrollment.js +++ b/erpnext/education/doctype/program_enrollment/program_enrollment.js @@ -101,4 +101,4 @@ frappe.ui.form.on('Program Enrollment Course', { return { filters: [['Course', 'name', 'not in', course_list]] }; }; } -}); \ No newline at end of file +}); diff --git a/erpnext/education/doctype/program_enrollment/program_enrollment.py b/erpnext/education/doctype/program_enrollment/program_enrollment.py index b282babd0fc..dd4aa576ac0 100644 --- a/erpnext/education/doctype/program_enrollment/program_enrollment.py +++ b/erpnext/education/doctype/program_enrollment/program_enrollment.py @@ -174,4 +174,3 @@ def get_students(doctype, txt, searchfield, start, page_len, filters): tuple(students + ["%%%s%%" % txt, start, page_len] ) ) - diff --git a/erpnext/education/doctype/program_enrollment/program_enrollment_dashboard.py b/erpnext/education/doctype/program_enrollment/program_enrollment_dashboard.py index 18d307cdf07..c47f8666898 100644 --- a/erpnext/education/doctype/program_enrollment/program_enrollment_dashboard.py +++ b/erpnext/education/doctype/program_enrollment/program_enrollment_dashboard.py @@ -16,4 +16,4 @@ def get_data(): 'items': ['Student and Guardian Contact Details'] } ] - } \ No newline at end of file + } 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 aea81a0714e..00000000000 --- 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/test_program_enrollment.py b/erpnext/education/doctype/program_enrollment/test_program_enrollment.py index fec6422e75f..497ee288aac 100644 --- a/erpnext/education/doctype/program_enrollment/test_program_enrollment.py +++ b/erpnext/education/doctype/program_enrollment/test_program_enrollment.py @@ -32,4 +32,4 @@ class TestProgramEnrollment(unittest.TestCase): for entry in frappe.db.get_all("Program Enrollment"): doc = frappe.get_doc("Program Enrollment", entry.name) doc.cancel() - doc.delete() \ No newline at end of file + doc.delete() 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 8d55104a0f3..00000000000 --- 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/program_enrollment_tool_student/program_enrollment_tool_student.json b/erpnext/education/doctype/program_enrollment_tool_student/program_enrollment_tool_student.json index 9be292b65ea..1d7497387f9 100644 --- a/erpnext/education/doctype/program_enrollment_tool_student/program_enrollment_tool_student.json +++ b/erpnext/education/doctype/program_enrollment_tool_student/program_enrollment_tool_student.json @@ -1,195 +1,68 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2016-06-10 03:29:02.539914", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, + "actions": [], + "creation": "2016-06-10 03:29:02.539914", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "student_applicant", + "student", + "student_name", + "column_break_3", + "student_batch_name", + "student_category" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "fieldname": "student_applicant", - "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": "Student Applicant", - "length": 0, - "no_copy": 0, - "options": "Student Applicant", - "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, - "unique": 0 - }, + "fieldname": "student_applicant", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Student Applicant", + "options": "Student Applicant" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "fieldname": "student", - "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": "Student", - "length": 0, - "no_copy": 0, - "options": "Student", - "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, - "unique": 0 - }, + "fieldname": "student", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Student", + "options": "Student" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_3", - "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, - "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, - "unique": 0 - }, + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "student_name", - "fieldtype": "Data", - "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": "Student Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "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, - "unique": 0 - }, + "fieldname": "student_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Student Name", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "student_batch_name", - "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": "Student Batch Name", - "length": 0, - "no_copy": 0, - "options": "Student Batch Name", - "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, - "unique": 0 + "fieldname": "student_batch_name", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Student Batch Name", + "options": "Student Batch Name" + }, + { + "fieldname": "student_category", + "fieldtype": "Link", + "label": "Student Category", + "options": "Student Category", + "read_only": 1 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2018-01-02 12:03:53.890741", - "modified_by": "Administrator", - "module": "Education", - "name": "Program Enrollment Tool Student", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "restrict_to_domain": "Education", - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 0, - "track_seen": 0 + ], + "istable": 1, + "links": [], + "modified": "2021-07-29 18:19:54.471594", + "modified_by": "Administrator", + "module": "Education", + "name": "Program Enrollment Tool Student", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "restrict_to_domain": "Education", + "sort_field": "modified", + "sort_order": "DESC" } \ No newline at end of file diff --git a/erpnext/education/doctype/question/question.py b/erpnext/education/doctype/question/question.py index a7deeab6f65..fb3b50478c8 100644 --- a/erpnext/education/doctype/question/question.py +++ b/erpnext/education/doctype/question/question.py @@ -43,4 +43,4 @@ class Question(Document): elif len(answers) == 1: return answers[0] else: - return answers \ No newline at end of file + return answers diff --git a/erpnext/education/doctype/question/test_question.js b/erpnext/education/doctype/question/test_question.js deleted file mode 100644 index 509939c6b56..00000000000 --- 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/quiz.js b/erpnext/education/doctype/quiz/quiz.js index 01bcf732360..320869be31e 100644 --- a/erpnext/education/doctype/quiz/quiz.js +++ b/erpnext/education/doctype/quiz/quiz.js @@ -68,4 +68,4 @@ let get_topics_without_quiz = function(quiz) { method: 'erpnext.education.doctype.quiz.quiz.get_topics_without_quiz', args: {'quiz': quiz} }); -}; \ No newline at end of file +}; diff --git a/erpnext/education/doctype/quiz/quiz.py b/erpnext/education/doctype/quiz/quiz.py index a774b88579a..a128e1f3427 100644 --- a/erpnext/education/doctype/quiz/quiz.py +++ b/erpnext/education/doctype/quiz/quiz.py @@ -68,4 +68,4 @@ def get_topics_without_quiz(quiz): topic_contents = [tc.content for tc in topic.topic_content] if not topic_contents or quiz not in topic_contents: data.append(topic.name) - return data \ No newline at end of file + return data diff --git a/erpnext/education/doctype/quiz/test_quiz.js b/erpnext/education/doctype/quiz/test_quiz.js deleted file mode 100644 index 147d13952ab..00000000000 --- 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 94b5ab796ac..00000000000 --- 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 43f53a1dc77..00000000000 --- 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/room/room.js b/erpnext/education/doctype/room/room.js index 20cee6b2a61..1263b60ced2 100644 --- a/erpnext/education/doctype/room/room.js +++ b/erpnext/education/doctype/room/room.js @@ -1,2 +1,2 @@ frappe.ui.form.on("Room", { -}); \ No newline at end of file +}); diff --git a/erpnext/education/doctype/room/room_dashboard.py b/erpnext/education/doctype/room/room_dashboard.py index 99aac3393e6..7bcb97f709f 100644 --- a/erpnext/education/doctype/room/room_dashboard.py +++ b/erpnext/education/doctype/room/room_dashboard.py @@ -16,4 +16,4 @@ def get_data(): 'items': ['Assessment Plan'] } ] - } \ No newline at end of file + } 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 dde63ecc4c6..00000000000 --- 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/student.js b/erpnext/education/doctype/student/student.js index fd23ae41ef1..aead556dc9f 100644 --- a/erpnext/education/doctype/student/student.js +++ b/erpnext/education/doctype/student/student.js @@ -60,4 +60,4 @@ frappe.ui.form.on('Student Sibling', { return { filters: [['Student', 'name', 'not in', sibling_list]] }; }; } -}); \ No newline at end of file +}); diff --git a/erpnext/education/doctype/student/student_list.js b/erpnext/education/doctype/student/student_list.js index 763f120f417..c1bce24d153 100644 --- a/erpnext/education/doctype/student/student_list.js +++ b/erpnext/education/doctype/student/student_list.js @@ -1,3 +1,3 @@ frappe.listview_settings['Student'] = { add_fields: [ "image"] -} \ No newline at end of file +} diff --git a/erpnext/education/doctype/student/test_student.js b/erpnext/education/doctype/student/test_student.js deleted file mode 100644 index e18d39aee07..00000000000 --- 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/test_student.py b/erpnext/education/doctype/student/test_student.py index 2e5263788f7..fcb2b5fb930 100644 --- a/erpnext/education/doctype/student/test_student.py +++ b/erpnext/education/doctype/student/test_student.py @@ -68,4 +68,4 @@ def get_student(email): student_id = frappe.get_all("Student", {"student_email_id": email}, ["name"])[0].name return frappe.get_doc("Student", student_id) except IndexError: - return None \ No newline at end of file + return None diff --git a/erpnext/education/doctype/student_admission/templates/student_admission_row.html b/erpnext/education/doctype/student_admission/templates/student_admission_row.html index 99868d5f020..529d65184a8 100644 --- a/erpnext/education/doctype/student_admission/templates/student_admission_row.html +++ b/erpnext/education/doctype/student_admission/templates/student_admission_row.html @@ -41,4 +41,4 @@
- \ No newline at end of file + diff --git a/erpnext/education/doctype/student_admission/test_student_admission.js b/erpnext/education/doctype/student_admission/test_student_admission.js index 3a0bb0b2f23..e01791a78ac 100644 --- a/erpnext/education/doctype/student_admission/test_student_admission.js +++ b/erpnext/education/doctype/student_admission/test_student_admission.js @@ -37,4 +37,4 @@ QUnit.test('Test: Student Admission', function(assert) { }, () => done() ]); -}); \ No newline at end of file +}); diff --git a/erpnext/education/doctype/student_applicant/student_applicant.js b/erpnext/education/doctype/student_applicant/student_applicant.js index b4cfdf16e0d..7b41a721748 100644 --- a/erpnext/education/doctype/student_applicant/student_applicant.js +++ b/erpnext/education/doctype/student_applicant/student_applicant.js @@ -59,4 +59,4 @@ frappe.ui.form.on('Student Sibling', { frm.add_fetch("student", "gender", "gender"); frm.add_fetch("student", "date_of_birth", "date_of_birth"); } -}); \ No newline at end of file +}); diff --git a/erpnext/education/doctype/student_applicant/student_applicant.py b/erpnext/education/doctype/student_applicant/student_applicant.py index 211348201e3..193b6d32977 100644 --- a/erpnext/education/doctype/student_applicant/student_applicant.py +++ b/erpnext/education/doctype/student_applicant/student_applicant.py @@ -49,7 +49,7 @@ class StudentApplicant(Document): frappe.throw(_("Please select Student Admission which is mandatory for the paid student applicant")) def validation_from_student_admission(self): - + student_admission = get_student_admission_data(self.student_admission, self.program) if student_admission and student_admission.min_age and \ diff --git a/erpnext/education/doctype/student_applicant/student_applicant_list.js b/erpnext/education/doctype/student_applicant/student_applicant_list.js index 817a728f696..c39d46ec63a 100644 --- a/erpnext/education/doctype/student_applicant/student_applicant_list.js +++ b/erpnext/education/doctype/student_applicant/student_applicant_list.js @@ -18,4 +18,4 @@ frappe.listview_settings['Student Applicant'] = { return [__("Admitted"), "blue", "application_status,=,Admitted"]; } } -}; \ No newline at end of file +}; diff --git a/erpnext/education/doctype/student_applicant/tests/test_student_applicant.js b/erpnext/education/doctype/student_applicant/tests/test_student_applicant.js index a69ad8a5646..fa679779858 100644 --- a/erpnext/education/doctype/student_applicant/tests/test_student_applicant.js +++ b/erpnext/education/doctype/student_applicant/tests/test_student_applicant.js @@ -92,4 +92,4 @@ QUnit.test('Test: Student Applicant', function(assert){ }, () => done() ]); -}); \ No newline at end of file +}); diff --git a/erpnext/education/doctype/student_applicant/tests/test_student_applicant_dummy_data.js b/erpnext/education/doctype/student_applicant/tests/test_student_applicant_dummy_data.js index 26244ab1845..03101e41e00 100644 --- a/erpnext/education/doctype/student_applicant/tests/test_student_applicant_dummy_data.js +++ b/erpnext/education/doctype/student_applicant/tests/test_student_applicant_dummy_data.js @@ -84,4 +84,4 @@ QUnit.test('Make Students', function(assert){ }, () => done() ]); -}); \ No newline at end of file +}); diff --git a/erpnext/education/doctype/student_applicant/tests/test_student_applicant_options.js b/erpnext/education/doctype/student_applicant/tests/test_student_applicant_options.js index 114358f32a1..daa36e75ce4 100644 --- a/erpnext/education/doctype/student_applicant/tests/test_student_applicant_options.js +++ b/erpnext/education/doctype/student_applicant/tests/test_student_applicant_options.js @@ -107,4 +107,4 @@ QUnit.test('test student applicant', function(assert){ }, () => done() ]); -}); \ No newline at end of file +}); diff --git a/erpnext/education/doctype/student_attendance/student_attendance.js b/erpnext/education/doctype/student_attendance/student_attendance.js index f025a1a5397..2bbecb911f6 100644 --- a/erpnext/education/doctype/student_attendance/student_attendance.js +++ b/erpnext/education/doctype/student_attendance/student_attendance.js @@ -2,4 +2,4 @@ // For license information, please see license.txt cur_frm.add_fetch("course_schedule", "schedule_date", "date"); -cur_frm.add_fetch("course_schedule", "student_group", "student_group") \ No newline at end of file +cur_frm.add_fetch("course_schedule", "student_group", "student_group") diff --git a/erpnext/education/doctype/student_attendance/student_attendance_dashboard.py b/erpnext/education/doctype/student_attendance/student_attendance_dashboard.py index 9c41b8f3dc6..e405b8aed9d 100644 --- a/erpnext/education/doctype/student_attendance/student_attendance_dashboard.py +++ b/erpnext/education/doctype/student_attendance/student_attendance_dashboard.py @@ -9,4 +9,4 @@ def get_data(): 'items': ['Student Monthly Attendance Sheet', 'Student Batch-Wise Attendance'] } ] - } \ No newline at end of file + } diff --git a/erpnext/education/doctype/student_attendance/student_attendance_list.js b/erpnext/education/doctype/student_attendance/student_attendance_list.js index 0d3e7ade152..e89b76c8d55 100644 --- a/erpnext/education/doctype/student_attendance/student_attendance_list.js +++ b/erpnext/education/doctype/student_attendance/student_attendance_list.js @@ -8,4 +8,4 @@ frappe.listview_settings['Student Attendance'] = { return [__("Present"), "green", "status,=,Present"]; } } -}; \ No newline at end of file +}; diff --git a/erpnext/education/doctype/student_attendance/test_student_attendance.js b/erpnext/education/doctype/student_attendance/test_student_attendance.js index c7da6f6b246..3d30b090ba0 100644 --- a/erpnext/education/doctype/student_attendance/test_student_attendance.js +++ b/erpnext/education/doctype/student_attendance/test_student_attendance.js @@ -28,4 +28,4 @@ QUnit.test('Test: Student Attendance', function(assert){ () => done() ]); -}); \ No newline at end of file +}); diff --git a/erpnext/education/doctype/student_attendance_tool/student_attendance_tool.py b/erpnext/education/doctype/student_attendance_tool/student_attendance_tool.py index 028db918812..972973fbadb 100644 --- a/erpnext/education/doctype/student_attendance_tool/student_attendance_tool.py +++ b/erpnext/education/doctype/student_attendance_tool/student_attendance_tool.py @@ -38,4 +38,4 @@ def get_student_attendance_records(based_on, date=None, student_group=None, cour if student.student == attendance.student: student.status = attendance.status - return student_list \ No newline at end of file + return student_list diff --git a/erpnext/education/doctype/student_attendance_tool/test_student_attendance_tool.js b/erpnext/education/doctype/student_attendance_tool/test_student_attendance_tool.js index cea0761ae8b..b66d8397ba2 100644 --- a/erpnext/education/doctype/student_attendance_tool/test_student_attendance_tool.js +++ b/erpnext/education/doctype/student_attendance_tool/test_student_attendance_tool.js @@ -82,4 +82,4 @@ QUnit.test('Test: Student Attendace Tool', function(assert){ () => done() ]); -}); \ No newline at end of file +}); diff --git a/erpnext/education/doctype/student_group/student_group.js b/erpnext/education/doctype/student_group/student_group.js index 51e3b74a5cf..39ee9cebd10 100644 --- a/erpnext/education/doctype/student_group/student_group.js +++ b/erpnext/education/doctype/student_group/student_group.js @@ -142,4 +142,4 @@ frappe.ui.form.on('Student Group Instructor', { return { filters: [['Instructor', 'name', 'not in', instructor_list]] }; }; } -}); \ No newline at end of file +}); diff --git a/erpnext/education/doctype/student_group/student_group.py b/erpnext/education/doctype/student_group/student_group.py index 0260b808646..3d4572abf70 100644 --- a/erpnext/education/doctype/student_group/student_group.py +++ b/erpnext/education/doctype/student_group/student_group.py @@ -128,4 +128,3 @@ def fetch_students(doctype, txt, searchfield, start, page_len, filters): order by idx desc, name limit %s, %s""".format(searchfield), tuple(["%%%s%%" % txt, "%%%s%%" % txt, start, page_len])) - diff --git a/erpnext/education/doctype/student_group/student_group_dashboard.py b/erpnext/education/doctype/student_group/student_group_dashboard.py index ad7a6de7b3c..d37445f7b98 100644 --- a/erpnext/education/doctype/student_group/student_group_dashboard.py +++ b/erpnext/education/doctype/student_group/student_group_dashboard.py @@ -16,4 +16,4 @@ def get_data(): 'items': ['Course Schedule'] } ] - } \ No newline at end of file + } diff --git a/erpnext/education/doctype/student_group/test_student_group.js b/erpnext/education/doctype/student_group/test_student_group.js index 6673343be7e..4c7e47bc38f 100644 --- a/erpnext/education/doctype/student_group/test_student_group.js +++ b/erpnext/education/doctype/student_group/test_student_group.js @@ -53,4 +53,4 @@ QUnit.test('Test: Student Group', function(assert){ () => done() ]); -}); \ No newline at end of file +}); diff --git a/erpnext/education/doctype/student_group_creation_tool/student_group_creation_tool.js b/erpnext/education/doctype/student_group_creation_tool/student_group_creation_tool.js index d0d7afd701c..c189e2763c8 100644 --- a/erpnext/education/doctype/student_group_creation_tool/student_group_creation_tool.js +++ b/erpnext/education/doctype/student_group_creation_tool/student_group_creation_tool.js @@ -37,4 +37,4 @@ frappe.ui.form.on("Student Group Creation Tool", "onload", function(frm){ } }; }); -}); \ No newline at end of file +}); diff --git a/erpnext/education/doctype/student_group_creation_tool/student_group_creation_tool.py b/erpnext/education/doctype/student_group_creation_tool/student_group_creation_tool.py index dc8667ec065..28ff7d618c1 100644 --- a/erpnext/education/doctype/student_group_creation_tool/student_group_creation_tool.py +++ b/erpnext/education/doctype/student_group_creation_tool/student_group_creation_tool.py @@ -76,4 +76,4 @@ class StudentGroupCreationTool(Document): student_group.append('students', student) student_group.save() - frappe.msgprint(_("{0} Student Groups created.").format(l)) \ No newline at end of file + frappe.msgprint(_("{0} Student Groups created.").format(l)) diff --git a/erpnext/education/doctype/student_group_creation_tool/test_student_group_creation_tool.js b/erpnext/education/doctype/student_group_creation_tool/test_student_group_creation_tool.js index 34c10930b57..fa612ba2727 100644 --- a/erpnext/education/doctype/student_group_creation_tool/test_student_group_creation_tool.js +++ b/erpnext/education/doctype/student_group_creation_tool/test_student_group_creation_tool.js @@ -81,4 +81,4 @@ QUnit.test('Test: Student Group Creation Tool', function(assert){ () => done() ]); -}); \ No newline at end of file +}); diff --git a/erpnext/education/doctype/student_group_student/student_group_student.py b/erpnext/education/doctype/student_group_student/student_group_student.py index 820e30118dc..1fe4ea1dc35 100644 --- a/erpnext/education/doctype/student_group_student/student_group_student.py +++ b/erpnext/education/doctype/student_group_student/student_group_student.py @@ -7,4 +7,4 @@ import frappe from frappe.model.document import Document class StudentGroupStudent(Document): - pass \ No newline at end of file + pass 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 9b25569961a..00000000000 --- 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_leave_application/student_leave_application_dashboard.py b/erpnext/education/doctype/student_leave_application/student_leave_application_dashboard.py index fdcc1474797..0ff6d1a76ea 100644 --- a/erpnext/education/doctype/student_leave_application/student_leave_application_dashboard.py +++ b/erpnext/education/doctype/student_leave_application/student_leave_application_dashboard.py @@ -8,4 +8,4 @@ def get_data(): 'items': ['Student Attendance'] } ] - } \ No newline at end of file + } diff --git a/erpnext/education/doctype/student_leave_application/test_student_leave_application.js b/erpnext/education/doctype/student_leave_application/test_student_leave_application.js index 5af9f5d50f7..6bbf17babfa 100644 --- a/erpnext/education/doctype/student_leave_application/test_student_leave_application.js +++ b/erpnext/education/doctype/student_leave_application/test_student_leave_application.js @@ -66,4 +66,4 @@ QUnit.test('Test: Student Leave Application', function(assert){ () => done() ]); -}); \ No newline at end of file +}); diff --git a/erpnext/education/doctype/student_leave_application/test_student_leave_application.py b/erpnext/education/doctype/student_leave_application/test_student_leave_application.py index fcdd42825f3..9cae2577481 100644 --- a/erpnext/education/doctype/student_leave_application/test_student_leave_application.py +++ b/erpnext/education/doctype/student_leave_application/test_student_leave_application.py @@ -112,4 +112,4 @@ def create_holiday_list(): company = get_default_company() or frappe.get_all('Company')[0].name frappe.db.set_value('Company', company, 'default_holiday_list', holiday_list) - return holiday_list \ No newline at end of file + return holiday_list diff --git a/erpnext/education/doctype/student_log/test_student_log.js b/erpnext/education/doctype/student_log/test_student_log.js index 5775369e52e..4c90c5f6ef2 100644 --- a/erpnext/education/doctype/student_log/test_student_log.js +++ b/erpnext/education/doctype/student_log/test_student_log.js @@ -32,4 +32,4 @@ QUnit.test('Test: Student Log', function(assert){ }, () => done() ]); -}); \ No newline at end of file +}); diff --git a/erpnext/education/doctype/student_report_generation_tool/student_report_generation_tool.html b/erpnext/education/doctype/student_report_generation_tool/student_report_generation_tool.html index 72772b7b32c..a9e84e6e277 100644 --- a/erpnext/education/doctype/student_report_generation_tool/student_report_generation_tool.html +++ b/erpnext/education/doctype/student_report_generation_tool/student_report_generation_tool.html @@ -12,67 +12,67 @@ padding: 0.75in; margin: auto; } - + .print-format.landscape { max-width: 11.69in; padding: 0.2in; } - + .page-break { padding: 30px 0px; border-bottom: 1px dashed #888; } - + .page-break:first-child { padding-top: 0px; } - + .page-break:last-child { border-bottom: 0px; } - + /* mozilla hack for images in table */ body:last-child .print-format td img { width: 100% !important; } - + @media(max-width: 767px) { .print-format { padding: 0.2in; } } } - + @media print { .print-format p { margin-left: 1px; margin-right: 1px; } } - + .data-field { margin-top: 5px; margin-bottom: 5px; } - + .data-field .value { word-wrap: break-word; } - + .important .value { font-size: 120%; font-weight: bold; } - + .important label { line-height: 1.8; margin: 0px; } - + .table { margin: 20px 0px; } - + .square-image { width: 100%; height: 0; @@ -83,88 +83,88 @@ background-position: center center; border-radius: 4px; } - + .print-item-image { object-fit: contain; } - + .pdf-variables, .pdf-variable, .visible-pdf { display: none !important; } - + .print-format { font-size: 9pt; font-family: "Helvetica Neue", Helvetica, Arial, "Open Sans", sans-serif; -webkit-print-color-adjust:exact; } - + .page-break { page-break-after: always; } - + .print-heading { border-bottom: 1px solid #aaa; margin-bottom: 10px; } - + .print-heading h2 { margin: 0px; } .print-heading h4 { margin-top: 5px; } - + table.no-border, table.no-border td { border: 0px; } - + .print-format label { /* wkhtmltopdf breaks label into multiple lines when it is inline-block */ display: block; } - + .print-format img { max-width: 100%; } - + .print-format table td > .primary:first-child { font-weight: bold; } - + .print-format td, .print-format th { vertical-align: top !important; padding: 6px !important; } - + .print-format p { margin: 3px 0px 3px; } - + table td div { - + /* needed to avoid partial cutting of text between page break in wkhtmltopdf */ page-break-inside: avoid !important; - + } - + /* hack for webkit specific browser */ @media (-webkit-min-device-pixel-ratio:0) { thead, tfoot { display: table-row-group; } } - + [document-status] { margin-bottom: 5mm; } - + .signature-img { background: #fff; border-radius: 3px; margin-top: 5px; max-height: 150px; } - + .print-heading { text-align: right; text-transform: uppercase; @@ -173,16 +173,16 @@ margin-bottom: 20px; border-bottom: 1px solid #d1d8dd; } - + .print-heading h2 { font-size: 24px; } - + .print-format th { background-color: #eee !important; border-bottom: 0px !important; } - + /* modern format: for-test */ .pbi_avoid { @@ -344,7 +344,7 @@
-
+

{{ _("Student Attendance")}}


Present {{ doc.attendance.get("Present") if doc.attendance.get("Present") != None else '0' }} days @@ -352,7 +352,7 @@
-
+

{{ _("Parents Teacher Meeting Attendance")}}


Present {{ doc.parents_attendance if doc.parents_attendance != None else '0' }} 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 10be092bb92..00000000000 --- 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 4460b794786..00000000000 --- 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/topic.js b/erpnext/education/doctype/topic/topic.js index 2002b0c8e3b..0c903c5a56a 100644 --- a/erpnext/education/doctype/topic/topic.js +++ b/erpnext/education/doctype/topic/topic.js @@ -52,4 +52,4 @@ let get_courses_without_topic = function(topic) { method: 'erpnext.education.doctype.topic.topic.get_courses_without_topic', args: {'topic': topic} }); -}; \ No newline at end of file +}; diff --git a/erpnext/education/doctype/topic/topic.py b/erpnext/education/doctype/topic/topic.py index a5253e93294..fb680d725b0 100644 --- a/erpnext/education/doctype/topic/topic.py +++ b/erpnext/education/doctype/topic/topic.py @@ -56,4 +56,4 @@ def add_content_to_topics(content_type, content, topics): topic.save() frappe.db.commit() frappe.msgprint(_('{0} {1} has been added to all the selected topics successfully.').format(content_type, frappe.bold(content)), - title=_('Topics updated'), indicator='green') \ No newline at end of file + title=_('Topics updated'), indicator='green') 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 bf9a62d0376..00000000000 --- 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/education/report/program_wise_fee_collection/program_wise_fee_collection.py b/erpnext/education/report/program_wise_fee_collection/program_wise_fee_collection.py index c145359129e..c0ec0357cc6 100644 --- a/erpnext/education/report/program_wise_fee_collection/program_wise_fee_collection.py +++ b/erpnext/education/report/program_wise_fee_collection/program_wise_fee_collection.py @@ -121,4 +121,3 @@ def get_chart_data(data): }, 'type': 'bar' } - diff --git a/erpnext/education/report/student_batch_wise_attendance/student_batch_wise_attendance.js b/erpnext/education/report/student_batch_wise_attendance/student_batch_wise_attendance.js index ad04356201e..9f1fcbc8162 100644 --- a/erpnext/education/report/student_batch_wise_attendance/student_batch_wise_attendance.js +++ b/erpnext/education/report/student_batch_wise_attendance/student_batch_wise_attendance.js @@ -9,4 +9,4 @@ frappe.query_reports["Student Batch-Wise Attendance"] = { "default": frappe.datetime.get_today(), "reqd": 1 }] -} \ No newline at end of file +} diff --git a/erpnext/education/report/student_batch_wise_attendance/student_batch_wise_attendance.py b/erpnext/education/report/student_batch_wise_attendance/student_batch_wise_attendance.py index 7793dcf3953..e2576a0c710 100644 --- a/erpnext/education/report/student_batch_wise_attendance/student_batch_wise_attendance.py +++ b/erpnext/education/report/student_batch_wise_attendance/student_batch_wise_attendance.py @@ -67,4 +67,4 @@ def get_student_attendance(student_group, date): student_group= %s and date= %s and docstatus = 1 and (course_schedule is Null or course_schedule='') group by status""", (student_group, date), as_dict=1) - return student_attendance \ No newline at end of file + return student_attendance diff --git a/erpnext/education/report/student_monthly_attendance_sheet/student_monthly_attendance_sheet.js b/erpnext/education/report/student_monthly_attendance_sheet/student_monthly_attendance_sheet.js index 104d3ec06f9..62c94557d7e 100644 --- a/erpnext/education/report/student_monthly_attendance_sheet/student_monthly_attendance_sheet.js +++ b/erpnext/education/report/student_monthly_attendance_sheet/student_monthly_attendance_sheet.js @@ -39,4 +39,4 @@ frappe.query_reports["Student Monthly Attendance Sheet"] = { } }); } -} \ No newline at end of file +} diff --git a/erpnext/education/web_form/student_applicant/student_applicant.js b/erpnext/education/web_form/student_applicant/student_applicant.js index 699703c5792..ffc5e984253 100644 --- a/erpnext/education/web_form/student_applicant/student_applicant.js +++ b/erpnext/education/web_form/student_applicant/student_applicant.js @@ -1,3 +1,3 @@ frappe.ready(function() { // bind events here -}) \ No newline at end of file +}) diff --git a/erpnext/erpnext_integrations/connectors/shopify_connection.py b/erpnext/erpnext_integrations/connectors/shopify_connection.py index 5d5b2e19ce3..912674ff1a2 100644 --- a/erpnext/erpnext_integrations/connectors/shopify_connection.py +++ b/erpnext/erpnext_integrations/connectors/shopify_connection.py @@ -350,4 +350,3 @@ def is_sync_complete(shopify_settings, order): return getdate(shopify_settings.to_date) < getdate(order.get('created_at')) else: return cstr(order.get('id')) == cstr(shopify_settings.to_order_id) - diff --git a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_mws_settings.js b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_mws_settings.js index a9925adee7a..f5ea8047c6a 100644 --- a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_mws_settings.js +++ b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_mws_settings.js @@ -1,3 +1,2 @@ // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt - 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 9c8990986e2..00000000000 --- 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/amazon_mws_settings/xml_utils.py b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/xml_utils.py index a25a29f9e5f..99ede8f31de 100644 --- a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/xml_utils.py +++ b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/xml_utils.py @@ -103,4 +103,4 @@ class xml2dict(object): """parse a string""" t = ET.fromstring(s) root_tag, root_tree = self._namespace_split(t.tag, self._parse_node(t)) - return object_dict({root_tag: root_tree}) \ No newline at end of file + return object_dict({root_tag: root_tree}) diff --git a/erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.py b/erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.py index 6a846efad70..bff928c1c96 100644 --- a/erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.py +++ b/erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.py @@ -17,4 +17,4 @@ class ExotelSettings(Document): response = requests.get('https://api.exotel.com/v1/Accounts/{sid}' .format(sid = self.account_sid), auth=(self.api_key, self.api_token)) if response.status_code != 200: - frappe.throw(_("Invalid credentials")) \ No newline at end of file + frappe.throw(_("Invalid credentials")) 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 caa9399eb6c..00000000000 --- 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 b6daad8de48..00000000000 --- 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/mpesa_settings/account_balance.html b/erpnext/erpnext_integrations/doctype/mpesa_settings/account_balance.html index 2c4d4bbdecf..b74a7187f0c 100644 --- a/erpnext/erpnext_integrations/doctype/mpesa_settings/account_balance.html +++ b/erpnext/erpnext_integrations/doctype/mpesa_settings/account_balance.html @@ -25,4 +25,4 @@ {% else %}

Account Balance Information Not Available.

-{% endif %} \ No newline at end of file +{% endif %} diff --git a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_connector.py b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_connector.py index 554c6b0eb0f..d1adeeee072 100644 --- a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_connector.py +++ b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_connector.py @@ -115,4 +115,4 @@ class MpesaConnector(): saf_url = "{0}{1}".format(self.base_url, "/mpesa/stkpush/v1/processrequest") r = requests.post(saf_url, headers=headers, json=payload) - return r.json() \ No newline at end of file + return r.json() diff --git a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_custom_fields.py b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_custom_fields.py index 0499e88b5e7..139e2fb192b 100644 --- a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_custom_fields.py +++ b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_custom_fields.py @@ -50,4 +50,4 @@ def create_pos_settings(record_dict): for record in record_dict: if frappe.db.exists("POS Field", {"fieldname": record.get("fieldname")}): continue - frappe.get_doc(record).insert() \ No newline at end of file + frappe.get_doc(record).insert() diff --git a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.py b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.py index fdfaa1b0540..de933578613 100644 --- a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.py +++ b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.py @@ -276,4 +276,4 @@ def fetch_param_value(response, key, key_field): """Fetch the specified key from list of dictionary. Key is identified via the key field.""" for param in response: if param[key_field] == key: - return param["Value"] \ No newline at end of file + return param["Value"] diff --git a/erpnext/erpnext_integrations/doctype/mpesa_settings/test_mpesa_settings.py b/erpnext/erpnext_integrations/doctype/mpesa_settings/test_mpesa_settings.py index b0e662d3f32..d4cb6b982bb 100644 --- a/erpnext/erpnext_integrations/doctype/mpesa_settings/test_mpesa_settings.py +++ b/erpnext/erpnext_integrations/doctype/mpesa_settings/test_mpesa_settings.py @@ -355,4 +355,4 @@ def get_account_balance_callback_payload(): } } } - } \ No newline at end of file + } diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py index 42d4b9b2b43..73f5927df40 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py @@ -50,7 +50,7 @@ class PlaidConnector(): "secret": self.settings.plaid_secret, "products": self.products, }) - + return args def get_link_token(self, update_mode=False): diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js index 37bf2824505..3740d049839 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js @@ -135,4 +135,4 @@ erpnext.integrations.plaidLink = class plaidLink { }); }, __("Select a company"), __("Continue")); } -}; \ No newline at end of file +}; diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py index 3ef069b5e20..eddcb3401f6 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py @@ -110,7 +110,7 @@ def add_bank_accounts(response, bank, company): frappe.msgprint(_("Bank account {0} already exists and could not be created again").format(account["name"])) except Exception: frappe.log_error(frappe.get_traceback(), title=_("Plaid Link Error")) - frappe.throw(_("There was an error creating Bank Account while linking with Plaid."), + frappe.throw(_("There was an error creating Bank Account while linking with Plaid."), title=_("Plaid Link Failed")) else: 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 dc91347336d..00000000000 --- 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 b71d7048076..00000000000 --- 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/shopify_log/shopify_log.js b/erpnext/erpnext_integrations/doctype/shopify_log/shopify_log.js index d3fe7d2b4d6..12faeecc87f 100644 --- a/erpnext/erpnext_integrations/doctype/shopify_log/shopify_log.js +++ b/erpnext/erpnext_integrations/doctype/shopify_log/shopify_log.js @@ -18,5 +18,8 @@ frappe.ui.form.on('Shopify Log', { }) }).addClass('btn-primary'); } + + let app_link = "Ecommerce Integrations" + frm.dashboard.add_comment(__("Shopify Integration will be removed from ERPNext in Version 14. Please install {0} app to continue using it.", [app_link]), "yellow", true); } }); diff --git a/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.js b/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.js index 1574795dfad..a926a7e52a5 100644 --- a/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.js +++ b/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.js @@ -36,6 +36,10 @@ frappe.ui.form.on("Shopify Settings", "refresh", function(frm){ frm.toggle_reqd("delivery_note_series", frm.doc.sync_delivery_note); } + + let app_link = "Ecommerce Integrations" + frm.dashboard.add_comment(__("Shopify Integration will be removed from ERPNext in Version 14. Please install {0} app to continue using it.", [app_link]), "yellow", true); + }) $.extend(erpnext_integrations.shopify_settings, { diff --git a/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.js b/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.js index 5482b9cc695..af06b3451e0 100644 --- a/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.js +++ b/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.js @@ -346,4 +346,4 @@ erpnext.tally_migration.get_html_rows = (logs, field) => { }).join(""); return rows -} \ No newline at end of file +} 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 433c5e2cda8..00000000000 --- 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 ea06ab2dc4e..00000000000 --- 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/doctype/woocommerce_settings/woocommerce_settings.py b/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.py index bd072f40a19..45f261007f8 100644 --- a/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.py +++ b/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.py @@ -27,7 +27,7 @@ class WoocommerceSettings(Document): for doctype in ["Customer", "Address"]: df = dict(fieldname='woocommerce_email', label='Woocommerce Email', fieldtype='Data', read_only=1, print_hide=1) create_custom_field(doctype, df) - + if not frappe.get_value("Item Group", {"name": _("WooCommerce Products")}): item_group = frappe.new_doc("Item Group") item_group.item_group_name = _("WooCommerce Products") @@ -74,4 +74,4 @@ def generate_secret(): def get_series(): return { "sales_order_series" : frappe.get_meta("Sales Order").get_options("naming_series") or "SO-WOO-", - } \ No newline at end of file + } diff --git a/erpnext/erpnext_integrations/stripe_integration.py b/erpnext/erpnext_integrations/stripe_integration.py index a35ca28e0a3..820c7405328 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() \ No newline at end of file + return stripe_settings.finalize_request() diff --git a/erpnext/erpnext_integrations/utils.py b/erpnext/erpnext_integrations/utils.py index a5e162f8b5d..caafc0821e1 100644 --- a/erpnext/erpnext_integrations/utils.py +++ b/erpnext/erpnext_integrations/utils.py @@ -52,7 +52,7 @@ def create_mode_of_payment(gateway, payment_type="General"): "payment_gateway": gateway }, ['payment_account']) - mode_of_payment = frappe.db.exists("Mode of Payment", gateway) + mode_of_payment = frappe.db.exists("Mode of Payment", gateway) if not mode_of_payment and payment_gateway_account: mode_of_payment = frappe.get_doc({ "doctype": "Mode of Payment", diff --git a/erpnext/healthcare/dashboard_chart/clinical_procedures/clinical_procedures.json b/erpnext/healthcare/dashboard_chart/clinical_procedures/clinical_procedures.json index a59f149ee56..68035281561 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 6d560f74bf1..dae9db19b8d 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 0195aac8b73..82145d60248 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 052483533e9..70293b158ed 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 8fc86a1c592..65e5472aa10 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/dashboard_chart_source/department_wise_patient_appointments/department_wise_patient_appointments.js b/erpnext/healthcare/dashboard_chart_source/department_wise_patient_appointments/department_wise_patient_appointments.js index dd6dc666d23..e494489d21a 100644 --- a/erpnext/healthcare/dashboard_chart_source/department_wise_patient_appointments/department_wise_patient_appointments.js +++ b/erpnext/healthcare/dashboard_chart_source/department_wise_patient_appointments/department_wise_patient_appointments.js @@ -11,4 +11,4 @@ frappe.dashboards.chart_sources["Department wise Patient Appointments"] = { default: frappe.defaults.get_user_default("Company") } ] -}; \ No newline at end of file +}; diff --git a/erpnext/healthcare/dashboard_chart_source/department_wise_patient_appointments/department_wise_patient_appointments.py b/erpnext/healthcare/dashboard_chart_source/department_wise_patient_appointments/department_wise_patient_appointments.py index 062da6e4654..eca7143e689 100644 --- a/erpnext/healthcare/dashboard_chart_source/department_wise_patient_appointments/department_wise_patient_appointments.py +++ b/erpnext/healthcare/dashboard_chart_source/department_wise_patient_appointments/department_wise_patient_appointments.py @@ -69,4 +69,4 @@ def get(chart_name = None, chart = None, no_cache = None, filters = None, from_d } ], 'type': 'bar' - } \ 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 b92103d7509..00000000000 --- 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/appointment_type.js b/erpnext/healthcare/doctype/appointment_type/appointment_type.js index 861675acea3..99b7cb295a9 100644 --- a/erpnext/healthcare/doctype/appointment_type/appointment_type.js +++ b/erpnext/healthcare/doctype/appointment_type/appointment_type.js @@ -80,4 +80,4 @@ frappe.ui.form.on('Appointment Type Service Item', { }); } } -}); \ No newline at end of file +}); 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 93274e55c7b..00000000000 --- 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/appointment_type_service_item/appointment_type_service_item.json b/erpnext/healthcare/doctype/appointment_type_service_item/appointment_type_service_item.json index 5ff68cd682c..ccae129ea0b 100644 --- a/erpnext/healthcare/doctype/appointment_type_service_item/appointment_type_service_item.json +++ b/erpnext/healthcare/doctype/appointment_type_service_item/appointment_type_service_item.json @@ -48,13 +48,13 @@ "fieldname": "inpatient_visit_charge", "fieldtype": "Currency", "in_list_view": 1, - "label": "Inpatient Visit Charge Item" + "label": "Inpatient Visit Charge" } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-01-22 09:35:26.503443", + "modified": "2021-08-17 06:05:02.240812", "modified_by": "Administrator", "module": "Healthcare", "name": "Appointment Type Service Item", 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 80ef3d55f2a..00000000000 --- 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 03e96a4b3be..0326e5e9da7 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() @@ -63,4 +63,4 @@ def create_procedure(procedure_template, patient, practitioner): procedure.company = "_Test Company" procedure.warehouse = "_Test Warehouse - _TC" procedure.submit() - return procedure \ No newline at end of file + return procedure diff --git a/erpnext/healthcare/doctype/clinical_procedure_template/clinical_procedure_template.js b/erpnext/healthcare/doctype/clinical_procedure_template/clinical_procedure_template.js index 1ef110dc6f4..ae6b39bb181 100644 --- a/erpnext/healthcare/doctype/clinical_procedure_template/clinical_procedure_template.js +++ b/erpnext/healthcare/doctype/clinical_procedure_template/clinical_procedure_template.js @@ -188,4 +188,3 @@ frappe.tour['Clinical Procedure Template'] = [ description: __('You can also set the Medical Department for the template. After saving the document, an Item will automatically be created for billing this Clinical Procedure. You can then use this template while creating Clinical Procedures for Patients. Templates save you from filling up redundant data every single time. You can also create templates for other operations like Lab Tests, Therapy Sessions, etc.') } ]; - diff --git a/erpnext/healthcare/doctype/clinical_procedure_template/clinical_procedure_template.py b/erpnext/healthcare/doctype/clinical_procedure_template/clinical_procedure_template.py index f32b7cf9d8d..58194f10a8c 100644 --- a/erpnext/healthcare/doctype/clinical_procedure_template/clinical_procedure_template.py +++ b/erpnext/healthcare/doctype/clinical_procedure_template/clinical_procedure_template.py @@ -118,4 +118,3 @@ def change_item_code_from_template(item_code, doc): rename_doc('Item', doc.item_code, item_code, ignore_permissions=True) frappe.db.set_value('Clinical Procedure Template', doc.name, 'item_code', item_code) return - 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 1dde8b5d867..00000000000 --- 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 9ff44d8da46..00000000000 --- 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 cacfef5b17d..00000000000 --- 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 ba54ab16faa..00000000000 --- 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 d91e6bf9dc6..a65c56694e7 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/exercise_type/exercise_type.py b/erpnext/healthcare/doctype/exercise_type/exercise_type.py index fb635c85788..ae44a2b77b5 100644 --- a/erpnext/healthcare/doctype/exercise_type/exercise_type.py +++ b/erpnext/healthcare/doctype/exercise_type/exercise_type.py @@ -12,4 +12,3 @@ class ExerciseType(Document): self.name = ' - '.join(filter(None, [self.exercise_name, self.difficulty_level])) else: self.name = self.exercise_name - diff --git a/erpnext/healthcare/doctype/fee_validity/fee_validity.json b/erpnext/healthcare/doctype/fee_validity/fee_validity.json index b001bf024ce..d76b42e6836 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 058bc971929..59586e0c31b 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 @@ -60,4 +50,4 @@ def check_is_new_patient(appointment): }) if len(appointment_exists) and appointment_exists[0]: return False - return True \ No newline at end of file + return True 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 0ebb97438ca..00000000000 --- 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 7e7fd824119..957f85211de 100644 --- a/erpnext/healthcare/doctype/fee_validity/test_fee_validity.py +++ b/erpnext/healthcare/doctype/fee_validity/test_fee_validity.py @@ -22,17 +22,17 @@ 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() - # appointment should not be invoiced. Check Fee Validity created for new patient + # 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, 0) + self.assertEqual(invoiced, 1) # appointment should not be invoiced as it is within fee validity appointment = create_appointment(patient, practitioner, add_days(nowdate(), 4)) @@ -47,4 +47,4 @@ class TestFeeValidity(unittest.TestCase): # appointment should be invoiced as it is not within fee validity and the max_visits are exceeded appointment = create_appointment(patient, practitioner, add_days(nowdate(), 10), invoice=1) invoiced = frappe.db.get_value("Patient Appointment", appointment.name, "invoiced") - self.assertEqual(invoiced, 1) \ No newline at end of file + self.assertEqual(invoiced, 1) diff --git a/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.js b/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.js index fc0b24122ae..44c399856c8 100644 --- a/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.js +++ b/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.js @@ -142,4 +142,3 @@ frappe.tour['Healthcare Practitioner'] = [ description: __('If this Healthcare Practitioner also works for the In-Patient Department, set the inpatient visit charge for this Practitioner.') } ]; - diff --git a/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.json b/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.json index 8162f03f6dc..cb455eb5014 100644 --- a/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.json +++ b/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.json @@ -282,7 +282,7 @@ ], "image_field": "image", "links": [], - "modified": "2021-01-22 10:14:43.187675", + "modified": "2021-08-24 10:42:08.513054", "modified_by": "Administrator", "module": "Healthcare", "name": "Healthcare Practitioner", @@ -295,6 +295,7 @@ "read": 1, "report": 1, "role": "Laboratory User", + "select": 1, "share": 1, "write": 1 }, @@ -307,6 +308,7 @@ "read": 1, "report": 1, "role": "Physician", + "select": 1, "share": 1, "write": 1 }, @@ -319,6 +321,7 @@ "read": 1, "report": 1, "role": "Nursing User", + "select": 1, "share": 1, "write": 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 75aa208ec16..00000000000 --- 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 2cdd5506565..2d1caf7efc7 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 9ee865a62a4..8935ec7d3c9 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 9e0417a2bef..5e76ed7284f 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 b75f2718271..ea3fea6b7a5 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 a67a4117073..00000000000 --- 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 eb33ab68c0d..ecf4aa1a4bf 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 4b8503d0286..9c81c65f6b8 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 6db8f9e9c1a..00000000000 --- 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_service_unit_type/test_healthcare_service_unit_type.py b/erpnext/healthcare/doctype/healthcare_service_unit_type/test_healthcare_service_unit_type.py index 01cf4b0a494..3ee3377b004 100644 --- a/erpnext/healthcare/doctype/healthcare_service_unit_type/test_healthcare_service_unit_type.py +++ b/erpnext/healthcare/doctype/healthcare_service_unit_type/test_healthcare_service_unit_type.py @@ -30,4 +30,4 @@ def get_unit_type(): unit_type.no_of_hours = 1 unit_type.rate = 4000 unit_type.save() - return unit_type \ No newline at end of file + return unit_type 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 ca10925e598..00000000000 --- 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_medication_entry/test_inpatient_medication_entry.py b/erpnext/healthcare/doctype/inpatient_medication_entry/test_inpatient_medication_entry.py index 7cb5a4814e8..0c463ddc029 100644 --- a/erpnext/healthcare/doctype/inpatient_medication_entry/test_inpatient_medication_entry.py +++ b/erpnext/healthcare/doctype/inpatient_medication_entry/test_inpatient_medication_entry.py @@ -118,12 +118,12 @@ class TestInpatientMedicationEntry(unittest.TestCase): def tearDown(self): # cleanup - Discharge - schedule_discharge(frappe.as_json({'patient': self.patient})) + schedule_discharge(frappe.as_json({'patient': self.patient, 'discharge_ordered_datetime': now_datetime()})) self.ip_record.reload() mark_invoiced_inpatient_occupancy(self.ip_record) self.ip_record.reload() - discharge_patient(self.ip_record) + discharge_patient(self.ip_record, now_datetime()) for entry in frappe.get_all('Inpatient Medication Entry'): doc = frappe.get_doc('Inpatient Medication Entry', entry.name) @@ -153,4 +153,4 @@ def make_stock_entry(warehouse=None): # in stock uom se_child.conversion_factor = 1.0 se_child.expense_account = expense_account - stock_entry.submit() \ No newline at end of file + stock_entry.submit() diff --git a/erpnext/healthcare/doctype/inpatient_medication_order/test_inpatient_medication_order.py b/erpnext/healthcare/doctype/inpatient_medication_order/test_inpatient_medication_order.py index 21776d2380a..ec1a28034e3 100644 --- a/erpnext/healthcare/doctype/inpatient_medication_order/test_inpatient_medication_order.py +++ b/erpnext/healthcare/doctype/inpatient_medication_order/test_inpatient_medication_order.py @@ -40,13 +40,13 @@ class TestInpatientMedicationOrder(unittest.TestCase): def test_inpatient_validation(self): # Discharge - schedule_discharge(frappe.as_json({'patient': self.patient})) + schedule_discharge(frappe.as_json({'patient': self.patient, 'discharge_ordered_datetime': now_datetime()})) self.ip_record.reload() mark_invoiced_inpatient_occupancy(self.ip_record) self.ip_record.reload() - discharge_patient(self.ip_record) + discharge_patient(self.ip_record, now_datetime()) ipmo = create_ipmo(self.patient) # inpatient validation @@ -74,12 +74,12 @@ class TestInpatientMedicationOrder(unittest.TestCase): def tearDown(self): if frappe.db.get_value('Patient', self.patient, 'inpatient_record'): # cleanup - Discharge - schedule_discharge(frappe.as_json({'patient': self.patient})) + schedule_discharge(frappe.as_json({'patient': self.patient, 'discharge_ordered_datetime': now_datetime()})) self.ip_record.reload() mark_invoiced_inpatient_occupancy(self.ip_record) self.ip_record.reload() - discharge_patient(self.ip_record) + discharge_patient(self.ip_record, now_datetime()) for doctype in ["Inpatient Medication Entry", "Inpatient Medication Order"]: frappe.db.sql("delete from `tab{doctype}`".format(doctype=doctype)) @@ -140,4 +140,3 @@ def create_ipme(filters, update_stock=0): ipme = ipme.get_medication_orders() return ipme - diff --git a/erpnext/healthcare/doctype/inpatient_record/inpatient_record.js b/erpnext/healthcare/doctype/inpatient_record/inpatient_record.js index 60f0f9d56d6..750279e348b 100644 --- a/erpnext/healthcare/doctype/inpatient_record/inpatient_record.js +++ b/erpnext/healthcare/doctype/inpatient_record/inpatient_record.js @@ -41,17 +41,36 @@ frappe.ui.form.on('Inpatient Record', { }); let discharge_patient = function(frm) { - frappe.call({ - doc: frm.doc, - method: 'discharge', - callback: function(data) { - if (!data.exc) { - frm.reload_doc(); + let dialog = new frappe.ui.Dialog({ + title: 'Discharge Patient', + width: 100, + fields: [ + {fieldtype: 'Datetime', label: 'Discharge Datetime', fieldname: 'check_out', + reqd: 1, default: frappe.datetime.now_datetime() } - }, - freeze: true, - freeze_message: __('Processing Inpatient Discharge') + ], + primary_action_label: __('Discharge'), + primary_action: function() { + let check_out = dialog.get_value('check_out'); + frappe.call({ + doc: frm.doc, + method: 'discharge', + args: { + 'check_out': check_out + }, + callback: function(data) { + if (!data.exc) { + frm.reload_doc(); + } + }, + freeze: true, + freeze_message: __('Processing Inpatient Discharge') + }); + frm.refresh_fields(); + dialog.hide(); + } }); + dialog.show(); }; let admit_patient_dialog = function(frm) { diff --git a/erpnext/healthcare/doctype/inpatient_record/inpatient_record.json b/erpnext/healthcare/doctype/inpatient_record/inpatient_record.json index 0e1c2ba7664..03ecf4fb018 100644 --- a/erpnext/healthcare/doctype/inpatient_record/inpatient_record.json +++ b/erpnext/healthcare/doctype/inpatient_record/inpatient_record.json @@ -50,7 +50,7 @@ "inpatient_occupancies", "btn_transfer", "sb_discharge_details", - "discharge_ordered_date", + "discharge_ordered_datetime", "discharge_practitioner", "discharge_encounter", "discharge_datetime", @@ -374,13 +374,6 @@ "fieldtype": "Small Text", "label": "Discharge Instructions" }, - { - "fieldname": "discharge_ordered_date", - "fieldtype": "Date", - "in_list_view": 1, - "label": "Discharge Ordered Date", - "read_only": 1 - }, { "collapsible": 1, "fieldname": "rehabilitation_section", @@ -406,13 +399,20 @@ { "fieldname": "discharge_datetime", "fieldtype": "Datetime", - "label": "Discharge Date", + "label": "Discharge Datetime", "permlevel": 2 + }, + { + "fieldname": "discharge_ordered_datetime", + "fieldtype": "Datetime", + "in_list_view": 1, + "label": "Discharge Ordered Datetime", + "read_only": 1 } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2021-03-18 15:59:17.318988", + "modified": "2021-08-09 22:49:07.419692", "modified_by": "Administrator", "module": "Healthcare", "name": "Inpatient Record", diff --git a/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py b/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py index f4d1eaf2e3f..2cdfa04d5c0 100644 --- a/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py +++ b/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py @@ -27,7 +27,7 @@ class InpatientRecord(Document): def validate_dates(self): if (getdate(self.expected_discharge) < getdate(self.scheduled_date)) or \ - (getdate(self.discharge_ordered_date) < getdate(self.scheduled_date)): + (getdate(self.discharge_ordered_datetime) < getdate(self.scheduled_date)): frappe.throw(_('Expected and Discharge dates cannot be less than Admission Schedule date')) for entry in self.inpatient_occupancies: @@ -58,8 +58,10 @@ class InpatientRecord(Document): admit_patient(self, service_unit, check_in, expected_discharge) @frappe.whitelist() - def discharge(self): - discharge_patient(self) + def discharge(self, check_out=now_datetime()): + if (getdate(check_out) < getdate(self.admitted_datetime)): + frappe.throw(_('Discharge date cannot be less than Admission date')) + discharge_patient(self, check_out) @frappe.whitelist() def transfer(self, service_unit, check_in, leave_from): @@ -120,10 +122,13 @@ def schedule_inpatient(args): @frappe.whitelist() def schedule_discharge(args): discharge_order = json.loads(args) + if not discharge_order or not discharge_order['patient'] or not discharge_order['discharge_ordered_datetime']: + frappe.throw(_('Missing required details, did not create schedule discharge')) + inpatient_record_id = frappe.db.get_value('Patient', discharge_order['patient'], 'inpatient_record') if inpatient_record_id: inpatient_record = frappe.get_doc('Inpatient Record', inpatient_record_id) - check_out_inpatient(inpatient_record) + check_out_inpatient(inpatient_record, discharge_order['discharge_ordered_datetime']) set_details_from_ip_order(inpatient_record, discharge_order) inpatient_record.status = 'Discharge Scheduled' inpatient_record.save(ignore_permissions = True) @@ -143,18 +148,18 @@ def set_ip_child_records(inpatient_record, inpatient_record_child, encounter_chi table.set(df.fieldname, item.get(df.fieldname)) -def check_out_inpatient(inpatient_record): +def check_out_inpatient(inpatient_record, discharge_ordered_datetime): if inpatient_record.inpatient_occupancies: for inpatient_occupancy in inpatient_record.inpatient_occupancies: if inpatient_occupancy.left != 1: inpatient_occupancy.left = True - inpatient_occupancy.check_out = now_datetime() + inpatient_occupancy.check_out = discharge_ordered_datetime frappe.db.set_value("Healthcare Service Unit", inpatient_occupancy.service_unit, "occupancy_status", "Vacant") -def discharge_patient(inpatient_record): +def discharge_patient(inpatient_record, check_out): validate_inpatient_invoicing(inpatient_record) - inpatient_record.discharge_datetime = now_datetime() + inpatient_record.discharge_datetime = check_out inpatient_record.status = "Discharged" inpatient_record.save(ignore_permissions = True) 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 1ce9afa96d4..00000000000 --- 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 a8c7720a0a4..9b5cd717a0c 100644 --- a/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py +++ b/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py @@ -29,7 +29,7 @@ class TestInpatientRecord(unittest.TestCase): self.assertEqual("Occupied", frappe.db.get_value("Healthcare Service Unit", service_unit, "occupancy_status")) # Discharge - schedule_discharge(frappe.as_json({'patient': patient})) + schedule_discharge(frappe.as_json({'patient': patient, 'discharge_ordered_datetime': now_datetime()})) self.assertEqual("Vacant", frappe.db.get_value("Healthcare Service Unit", service_unit, "occupancy_status")) ip_record1 = frappe.get_doc("Inpatient Record", ip_record.name) @@ -37,7 +37,7 @@ class TestInpatientRecord(unittest.TestCase): self.assertRaises(frappe.ValidationError, ip_record.discharge) mark_invoiced_inpatient_occupancy(ip_record1) - discharge_patient(ip_record1) + discharge_patient(ip_record1, now_datetime()) self.assertEqual(None, frappe.db.get_value("Patient", patient, "inpatient_record")) self.assertEqual(None, frappe.db.get_value("Patient", patient, "inpatient_status")) @@ -56,7 +56,7 @@ class TestInpatientRecord(unittest.TestCase): admit_patient(ip_record, service_unit, now_datetime()) # Discharge - schedule_discharge(frappe.as_json({"patient": patient})) + schedule_discharge(frappe.as_json({"patient": patient, 'discharge_ordered_datetime': now_datetime()})) self.assertEqual("Vacant", frappe.db.get_value("Healthcare Service Unit", service_unit, "occupancy_status")) ip_record = frappe.get_doc("Inpatient Record", ip_record.name) @@ -88,12 +88,12 @@ class TestInpatientRecord(unittest.TestCase): self.assertFalse(patient_encounter.name in encounter_ids) # Discharge - schedule_discharge(frappe.as_json({"patient": patient})) + schedule_discharge(frappe.as_json({"patient": patient, 'discharge_ordered_datetime': now_datetime()})) self.assertEqual("Vacant", frappe.db.get_value("Healthcare Service Unit", service_unit, "occupancy_status")) ip_record = frappe.get_doc("Inpatient Record", ip_record.name) mark_invoiced_inpatient_occupancy(ip_record) - discharge_patient(ip_record) + discharge_patient(ip_record, now_datetime()) setup_inpatient_settings(key="do_not_bill_inpatient_encounters", value=0) def test_validate_overlap_admission(self): @@ -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 4b57cd073d0..74495a85910 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 57cb22b2699..00000000000 --- 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 ace60de752a..00000000000 --- 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 7c2ec8c3483..00000000000 --- 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 1328dda2826..00000000000 --- 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 8cc7c40025c..00000000000 --- 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 6ab6d531dfb..00000000000 --- 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 fdf49718dc8..00000000000 --- 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 d57e5536c69..00000000000 --- 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 bce42e51d07..9266467155f 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 8af1a9ccd75..4092a6a7681 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 cebcb2068ea..95cce73f231 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: @@ -50,6 +53,16 @@ class Patient(Document): else: create_customer(self) + self.set_contact() # add or update contact + + if not self.user_id and self.email and self.invite_user: + self.create_website_user() + + def load_dashboard_info(self): + if self.customer: + info = get_dashboard_info('Customer', self.customer, None) + self.set_onload('dashboard_info', info) + def set_full_name(self): if self.last_name: self.patient_name = ' '.join(filter(None, [self.first_name, self.last_name])) @@ -72,18 +85,24 @@ class Patient(Document): if not self.language: self.language = frappe.db.get_single_value('System Settings', 'language') - def add_as_website_user(self): - if self.email: - if not frappe.db.exists ('User', self.email): - user = frappe.get_doc({ - 'doctype': 'User', - 'first_name': self.first_name, - 'last_name': self.last_name, - 'email': self.email, - 'user_type': 'Website User' - }) - user.flags.ignore_permissions = True - user.add_roles('Patient') + def create_website_user(self): + if self.email and not frappe.db.exists('User', self.email): + user = frappe.get_doc({ + 'doctype': 'User', + 'first_name': self.first_name, + 'last_name': self.last_name, + 'email': self.email, + 'user_type': 'Website User', + 'gender': self.sex, + 'phone': self.phone, + 'mobile_no': self.mobile, + 'birth_date': self.dob + }) + user.flags.ignore_permissions = True + user.enabled = True + user.send_welcome_email = True + user.add_roles('Patient') + frappe.db.set_value(self.doctype, self.name, 'user_id', user.name) def autoname(self): patient_name_by = frappe.db.get_single_value('Healthcare Settings', 'patient_name_by') @@ -103,12 +122,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 = str(age.years) + ' ' + _("Years(s)") + ' ' + str(age.months) + ' ' + _("Month(s)") + ' ' + str(age.days) + ' ' + _("Day(s)") return age_str @frappe.whitelist() @@ -125,6 +151,58 @@ class Patient(Document): return {'invoice': sales_invoice.name} + def set_contact(self): + if frappe.db.exists('Dynamic Link', {'parenttype':'Contact', 'link_doctype':'Patient', 'link_name':self.name}): + old_doc = self.get_doc_before_save() + if old_doc.email != self.email or old_doc.mobile != self.mobile or old_doc.phone != self.phone: + self.update_contact() + else: + self.reload() + if self.email or self.mobile or self.phone: + contact = frappe.get_doc({ + 'doctype': 'Contact', + 'first_name': self.first_name, + 'middle_name': self.middle_name, + 'last_name': self.last_name, + 'gender': self.sex, + 'is_primary_contact': 1 + }) + contact.append('links', dict(link_doctype='Patient', link_name=self.name)) + if self.customer: + contact.append('links', dict(link_doctype='Customer', link_name=self.customer)) + + contact.insert(ignore_permissions=True) + self.update_contact(contact) # update email, mobile and phone + + def update_contact(self, contact=None): + if not contact: + contact_name = get_default_contact(self.doctype, self.name) + if contact_name: + contact = frappe.get_doc('Contact', contact_name) + + if contact: + if self.email and self.email != contact.email_id: + for email in contact.email_ids: + email.is_primary = True if email.email_id == self.email else False + contact.add_email(self.email, is_primary=True) + contact.set_primary_email() + + if self.mobile and self.mobile != contact.mobile_no: + for mobile in contact.phone_nos: + mobile.is_primary_mobile_no = True if mobile.phone == self.mobile else False + contact.add_phone(self.mobile, is_primary_mobile_no=True) + contact.set_primary('mobile_no') + + if self.phone and self.phone != contact.phone: + for phone in contact.phone_nos: + phone.is_primary_phone = True if phone.phone == self.phone else False + contact.add_phone(self.phone, is_primary_phone=True) + contact.set_primary('phone') + + contact.flags.ignore_validate = True # disable hook TODO: safe? + contact.save(ignore_permissions=True) + + def create_customer(doc): customer = frappe.get_doc({ 'doctype': 'Customer', @@ -150,8 +228,8 @@ def make_invoice(patient, company): sales_invoice.debit_to = get_receivable_account(company) item_line = sales_invoice.append('items') - item_line.item_name = 'Registeration Fee' - item_line.description = 'Registeration Fee' + item_line.item_name = 'Registration Fee' + item_line.description = 'Registration Fee' item_line.qty = 1 item_line.uom = uom item_line.conversion_factor = 1 @@ -175,8 +253,11 @@ def get_patient_detail(patient): return details def get_timeline_data(doctype, name): - """Return timeline data from medical records""" - return dict(frappe.db.sql(''' + ''' + Return Patient's timeline data from medical records + Also include the associated Customer timeline data + ''' + patient_timeline_data = dict(frappe.db.sql(''' SELECT unix_timestamp(communication_date), count(*) FROM @@ -185,3 +266,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 39603f77a06..7f7cfa8e5b9 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 e1d9ecbd242..00000000000 --- 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 2976ef13a1d..49847d5bc8a 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, @@ -241,13 +241,20 @@ frappe.ui.form.on('Patient Appointment', { frm.toggle_reqd('mode_of_payment', 0); frm.toggle_reqd('paid_amount', 0); frm.toggle_reqd('billing_item', 0); + } else if (data.message) { + frm.toggle_display('mode_of_payment', 1); + frm.toggle_display('paid_amount', 1); + frm.toggle_display('billing_item', 1); + frm.toggle_reqd('mode_of_payment', 1); + frm.toggle_reqd('paid_amount', 1); + frm.toggle_reqd('billing_item', 1); } else { // if automated appointment invoicing is disabled, hide fields frm.toggle_display('mode_of_payment', data.message ? 1 : 0); frm.toggle_display('paid_amount', data.message ? 1 : 0); frm.toggle_display('billing_item', data.message ? 1 : 0); frm.toggle_reqd('mode_of_payment', data.message ? 1 : 0); - frm.toggle_reqd('paid_amount', data.message ? 1 :0); + frm.toggle_reqd('paid_amount', data.message ? 1 : 0); frm.toggle_reqd('billing_item', data.message ? 1 : 0); } } @@ -258,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); @@ -295,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'), @@ -379,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); }); @@ -441,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); @@ -473,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: [ @@ -493,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'); @@ -513,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(); }; @@ -528,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
\ @@ -537,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"); @@ -574,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 83c92af36ac..a6929c28511 100644 --- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json +++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json @@ -131,9 +131,10 @@ "fieldtype": "Link", "label": "Service Unit", "options": "Healthcare Service Unit", - "set_only_once": 1 + "read_only": 1 }, { + "depends_on": "eval:doc.practitioner;", "fieldname": "section_break_12", "fieldtype": "Section Break", "label": "Appointment Details" @@ -141,7 +142,6 @@ { "fieldname": "practitioner", "fieldtype": "Link", - "ignore_user_permissions": 1, "in_standard_filter": 1, "label": "Healthcare Practitioner", "options": "Healthcare Practitioner", @@ -349,7 +349,7 @@ } ], "links": [], - "modified": "2021-02-08 13:13:15.116833", + "modified": "2021-08-19 17:28:41.329387", "modified_by": "Administrator", "module": "Healthcare", "name": "Patient Appointment", @@ -400,4 +400,4 @@ "title_field": "title", "track_changes": 1, "track_seen": 1 -} \ No newline at end of file +} diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py index cdd4ad39c88..f0d5af93416 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): @@ -135,8 +167,6 @@ def check_payment_fields_reqd(patient): fee_validity = frappe.db.exists('Fee Validity', {'patient': patient, 'status': 'Pending'}) if fee_validity: return {'fee_validity': fee_validity} - if check_is_new_patient(patient): - return False return True return False @@ -151,8 +181,6 @@ def invoice_appointment(appointment_doc): elif not fee_validity: if frappe.db.exists('Fee Validity Reference', {'appointment': appointment_doc.name}): return - if check_is_new_patient(appointment_doc.patient, appointment_doc.name): - return else: fee_validity = None @@ -196,9 +224,7 @@ def check_is_new_patient(patient, name=None): filters['name'] = ('!=', name) has_previous_appointment = frappe.db.exists('Patient Appointment', filters) - if has_previous_appointment: - return False - return True + return not has_previous_appointment def get_appointment_item(appointment_doc, item): @@ -307,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 = [] @@ -327,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, @@ -336,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') @@ -352,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 71fc1778455..00000000000 --- 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 5f2dc480a1b..d0db3226326 100644 --- a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py +++ b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py @@ -4,20 +4,23 @@ from __future__ import unicode_literals import unittest import frappe -from erpnext.healthcare.doctype.patient_appointment.patient_appointment import update_status, make_encounter +from erpnext.healthcare.doctype.patient_appointment.patient_appointment import update_status, make_encounter, check_payment_fields_reqd, check_is_new_patient from frappe.utils import nowdate, add_days, now_datetime from frappe.utils.make_random import get_random from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile + class TestPatientAppointment(unittest.TestCase): def setUp(self): frappe.db.sql("""delete from `tabPatient Appointment`""") frappe.db.sql("""delete from `tabFee Validity`""") frappe.db.sql("""delete from `tabPatient Encounter`""") make_pos_profile() + frappe.db.sql("""delete from `tabHealthcare Service Unit` where name like '_Test %'""") + frappe.db.sql("""delete from `tabHealthcare Service Unit` where name like '_Test Service Unit Type%'""") def test_status(self): - patient, medical_department, practitioner = create_healthcare_docs() + patient, practitioner = create_healthcare_docs() frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 0) appointment = create_appointment(patient, practitioner, nowdate()) self.assertEqual(appointment.status, 'Open') @@ -29,7 +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() @@ -43,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()) @@ -59,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) @@ -77,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) @@ -87,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) @@ -103,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') @@ -129,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) @@ -137,17 +144,17 @@ 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) self.assertEqual(appointment.service_unit, service_unit) # Discharge - schedule_discharge(frappe.as_json({'patient': patient})) + schedule_discharge(frappe.as_json({'patient': patient, 'discharge_ordered_datetime': now_datetime()})) ip_record1 = frappe.get_doc("Inpatient Record", ip_record.name) mark_invoiced_inpatient_occupancy(ip_record1) - discharge_patient(ip_record1) + discharge_patient(ip_record1, now_datetime()) def test_invalid_healthcare_service_unit_validation(self): from erpnext.healthcare.doctype.inpatient_record.inpatient_record import admit_patient, discharge_patient, schedule_discharge @@ -155,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) @@ -163,52 +170,157 @@ 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) # Discharge - schedule_discharge(frappe.as_json({'patient': patient})) + schedule_discharge(frappe.as_json({'patient': patient, 'discharge_ordered_datetime': now_datetime()})) ip_record1 = frappe.get_doc("Inpatient Record", ip_record.name) mark_invoiced_inpatient_occupancy(ip_record1) - discharge_patient(ip_record1) + discharge_patient(ip_record1, now_datetime()) + + def test_payment_should_be_mandatory_for_new_patient_appointment(self): + frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 1) + frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1) + frappe.db.set_value('Healthcare Settings', None, 'max_visits', 3) + frappe.db.set_value('Healthcare Settings', None, 'valid_days', 30) + + patient = create_patient() + assert check_is_new_patient(patient) + payment_required = check_payment_fields_reqd(patient) + assert payment_required is True + + def test_sales_invoice_should_be_generated_for_new_patient_appointment(self): + patient, practitioner = create_healthcare_docs() + frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1) + invoice_count = frappe.db.count('Sales Invoice') + + assert check_is_new_patient(patient) + create_appointment(patient, practitioner, nowdate()) + new_invoice_count = frappe.db.count('Sales Invoice') + + assert new_invoice_count == invoice_count + 1 + + 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 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) + + # 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) + + # 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) + + 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 test_patient_appointment_should_consider_permissions_while_fetching_appointments(self): + patient, practitioner = create_healthcare_docs() + create_appointment(patient, practitioner, nowdate()) + + patient, new_practitioner = create_healthcare_docs(id=2) + create_appointment(patient, new_practitioner, nowdate()) + + roles = [{"doctype": "Has Role", "role": "Physician"}] + user = create_user(roles=roles) + new_practitioner = frappe.get_doc('Healthcare Practitioner', new_practitioner) + new_practitioner.user_id = user.email + new_practitioner.save() + + frappe.set_user(user.name) + appointments = frappe.get_list('Patient Appointment') + assert len(appointments) == 1 + + frappe.set_user("Administrator") + appointments = frappe.get_list('Patient Appointment') + assert len(appointments) == 2 -def create_healthcare_docs(): - patient = create_patient() - practitioner = frappe.db.exists('Healthcare Practitioner', '_Test Healthcare Practitioner') - medical_department = frappe.db.exists('Medical Department', '_Test Medical Department') +def create_healthcare_docs(id=0): + patient = create_patient(id) + practitioner = create_practitioner(id) - 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 + return patient, practitioner - if not practitioner: - practitioner = frappe.new_doc('Healthcare Practitioner') - practitioner.first_name = '_Test Healthcare Practitioner' - practitioner.gender = 'Female' - practitioner.department = medical_department - practitioner.op_consulting_charge = 500 - practitioner.inpatient_visit_charge = 500 - practitioner.save(ignore_permissions=True) - practitioner = practitioner.name - return patient, medical_department, 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 -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 + 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_encounter(appointment): if appointment: @@ -221,6 +333,7 @@ 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, @@ -235,6 +348,7 @@ def create_appointment(patient, practitioner, appointment_date, invoice=0, proce appointment.appointment_date = appointment_date appointment.company = '_Test Company' appointment.duration = 15 + if service_unit: appointment.service_unit = service_unit if invoice: @@ -245,11 +359,14 @@ def create_appointment(patient, practitioner, appointment_date, invoice=0, proce appointment.procedure_template = create_clinical_procedure_template().get('name') if save: appointment.save(ignore_permissions=True) + return appointment + def create_healthcare_service_items(): if frappe.db.exists('Item', 'HLC-SI-001'): return 'HLC-SI-001' + item = frappe.new_doc('Item') item.item_code = 'HLC-SI-001' item.item_name = 'Consulting Charges' @@ -257,11 +374,14 @@ def create_healthcare_service_items(): item.is_stock_item = 0 item.stock_uom = 'Nos' item.save() + return item.name + def create_clinical_procedure_template(): if frappe.db.exists('Clinical Procedure Template', 'Knee Surgery and Rehab'): return frappe.get_doc('Clinical Procedure Template', 'Knee Surgery and Rehab') + template = frappe.new_doc('Clinical Procedure Template') template.template = 'Knee Surgery and Rehab' template.item_code = 'Knee Surgery and Rehab' @@ -270,8 +390,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 @@ -284,9 +406,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', @@ -295,4 +417,45 @@ def create_appointment_type(args=None): 'color': args.get('color') or '#7575ff', 'price_list': args.get('price_list') or frappe.db.get_value("Price List", {"selling": 1}), 'items': args.get('items') or items - }).insert() \ No newline at end of file + }).insert() + + +def create_service_unit_type(id=0, allow_appointments=1, overlap_appointments=0): + if frappe.db.exists('Healthcare Service Unit Type', f'_Test Service Unit Type {str(id)}'): + return f'_Test Service Unit Type {str(id)}' + + service_unit_type = frappe.new_doc('Healthcare Service Unit Type') + service_unit_type.service_unit_type = f'_Test Service Unit Type {str(id)}' + service_unit_type.allow_appointments = allow_appointments + service_unit_type.overlap_appointments = overlap_appointments + service_unit_type.save(ignore_permissions=True) + + return service_unit_type.name + + +def create_service_unit(id=0, service_unit_type=None, service_unit_capacity=0): + if frappe.db.exists('Healthcare Service Unit', f'_Test Service Unit {str(id)}'): + return f'_Test service_unit {str(id)}' + + service_unit = frappe.new_doc('Healthcare Service Unit') + service_unit.is_group = 0 + service_unit.healthcare_service_unit_name= f'_Test Service Unit {str(id)}' + service_unit.service_unit_type = service_unit_type or create_service_unit_type(id) + service_unit.service_unit_capacity = service_unit_capacity + service_unit.save(ignore_permissions=True) + + return service_unit.name + +def create_user(email=None, roles=None): + if not email: + email = '{}@frappe.com'.format(frappe.utils.random_string(10)) + user = frappe.db.exists('User', email) + if not user: + user = frappe.get_doc({ + "doctype": "User", + "email": email, + "first_name": "test_user", + "password": "password", + "roles": roles, + }).insert() + return user diff --git a/erpnext/healthcare/doctype/patient_assessment/patient_assessment.py b/erpnext/healthcare/doctype/patient_assessment/patient_assessment.py index 3033a3e6ac9..7bad20dffdc 100644 --- a/erpnext/healthcare/doctype/patient_assessment/patient_assessment.py +++ b/erpnext/healthcare/doctype/patient_assessment/patient_assessment.py @@ -31,6 +31,3 @@ def create_patient_assessment(source_name, target_doc=None): }, target_doc) return doc - - - diff --git a/erpnext/healthcare/doctype/patient_encounter/patient_encounter.js b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.js index aaeaa692e63..9da9a6c13a2 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) { @@ -257,7 +292,7 @@ var schedule_discharge = function(frm) { var dialog = new frappe.ui.Dialog ({ title: 'Inpatient Discharge', fields: [ - {fieldtype: 'Date', label: 'Discharge Ordered Date', fieldname: 'discharge_ordered_date', default: 'Today', read_only: 1}, + {fieldtype: 'Datetime', label: 'Discharge Ordered DateTime', fieldname: 'discharge_ordered_datetime', default: frappe.datetime.now_datetime()}, {fieldtype: 'Date', label: 'Followup Date', fieldname: 'followup_date'}, {fieldtype: 'Column Break'}, {fieldtype: 'Small Text', label: 'Discharge Instructions', fieldname: 'discharge_instructions'}, @@ -270,7 +305,7 @@ var schedule_discharge = function(frm) { patient: frm.doc.patient, discharge_encounter: frm.doc.name, discharge_practitioner: frm.doc.practitioner, - discharge_ordered_date: dialog.get_value('discharge_ordered_date'), + discharge_ordered_datetime: dialog.get_value('discharge_ordered_datetime'), followup_date: dialog.get_value('followup_date'), discharge_instructions: dialog.get_value('discharge_instructions'), discharge_note: dialog.get_value('discharge_note') diff --git a/erpnext/healthcare/doctype/patient_encounter/patient_encounter.json b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.json index b646ff9ebe6..994597dca7c 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 cc2141790f7..7a745ae468e 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): @@ -99,4 +179,4 @@ def create_therapy_plan(encounter): def delete_ip_medication_order(encounter): record = frappe.db.exists('Inpatient Medication Order', {'patient_encounter': encounter.name}) if record: - frappe.delete_doc('Inpatient Medication Order', record, force=1) \ No newline at end of file + frappe.delete_doc('Inpatient Medication Order', record, force=1) 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 1baabf7eef7..00000000000 --- 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 f5df152050f..96976821a71 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 887d58a2e04..9e0d3c3e278 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 @@ -187,4 +187,4 @@ def get_module(doc): if not module: module = frappe.db.get_value('DocType', doc.doctype, 'module') - return module \ No newline at end of file + return module 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 c93b788aed7..9169ea642b9 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 @@ -6,7 +6,7 @@ from __future__ import unicode_literals import frappe import unittest import json -from frappe.utils import getdate +from frappe.utils import getdate, strip_html from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import create_patient class TestPatientHistorySettings(unittest.TestCase): @@ -38,15 +38,14 @@ 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: 3
Feedback: Test Patient History Settings
".format( + expected_subject = "Date:{0}Rating:3Feedback:Test Patient History Settings".format( frappe.utils.format_date(getdate())) - self.assertEqual(medical_rec.subject, expected_subject) + self.assertEqual(strip_html(medical_rec.subject), expected_subject) self.assertEqual(medical_rec.patient, patient) self.assertEqual(medical_rec.communication_date, getdate()) @@ -101,4 +100,4 @@ def create_doc(patient): }).insert() doc.submit() - return doc \ No newline at end of file + return doc 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 66dda09e256..00000000000 --- 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 c1d9872a019..5b7d8d62c83 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) @@ -88,4 +89,4 @@ def create_lab_test(template, patient): lab_test.template = template lab_test.save() lab_test.submit() - return lab_test \ No newline at end of file + return lab_test 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 32dac2c6526..00000000000 --- 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 009614ff5dd..00000000000 --- 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 4971e79198c..00000000000 --- 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 2b4aed756bf..00000000000 --- 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 c2cf406f96d..00000000000 --- 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 113fa513f98..983fba9f5ff 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_plan_template/therapy_plan_template.py b/erpnext/healthcare/doctype/therapy_plan_template/therapy_plan_template.py index 748c12c6896..635d4beb8df 100644 --- a/erpnext/healthcare/doctype/therapy_plan_template/therapy_plan_template.py +++ b/erpnext/healthcare/doctype/therapy_plan_template/therapy_plan_template.py @@ -70,4 +70,4 @@ class TherapyPlanTemplate(Document): item_price.item_name = self.item_name item_price.price_list_rate = self.total_amount item_price.ignore_mandatory = True - item_price.save(ignore_permissions=True) \ No newline at end of file + item_price.save(ignore_permissions=True) diff --git a/erpnext/healthcare/doctype/therapy_session/therapy_session.js b/erpnext/healthcare/doctype/therapy_session/therapy_session.js index fd200036935..fbfa774c91c 100644 --- a/erpnext/healthcare/doctype/therapy_session/therapy_session.js +++ b/erpnext/healthcare/doctype/therapy_session/therapy_session.js @@ -168,4 +168,4 @@ frappe.ui.form.on('Therapy Session', { }); } } -}); \ No newline at end of file +}); diff --git a/erpnext/healthcare/doctype/therapy_type/test_therapy_type.py b/erpnext/healthcare/doctype/therapy_type/test_therapy_type.py index 21f63699753..80fc83fd6ce 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() - return exercise_type \ No newline at end of file + 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 00000000000..e69de29bb2d 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 00000000000..d661b4304f6 --- /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 00000000000..21ede7129f1 --- /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 00000000000..986c3cb6e42 --- /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 00000000000..85a312fb174 --- /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 00000000000..a92e2668fe8 --- /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 00000000000..7ab31dff791 --- /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 00000000000..e69de29bb2d 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 00000000000..20a9d6793a5 --- /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 00000000000..5f58b06af60 --- /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 00000000000..e69de29bb2d 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 00000000000..04da387f7b8 --- /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 00000000000..6d34568e15c --- /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 f4ab4466be4..00000000000 --- 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/doctype/vital_signs/vital_signs.py b/erpnext/healthcare/doctype/vital_signs/vital_signs.py index 35c823d739c..4bb3940ae0f 100644 --- a/erpnext/healthcare/doctype/vital_signs/vital_signs.py +++ b/erpnext/healthcare/doctype/vital_signs/vital_signs.py @@ -15,4 +15,3 @@ class VitalSigns(Document): def set_title(self): self.title = _('{0} on {1}').format(self.patient_name or self.patient, frappe.utils.format_date(self.signs_date))[:100] - diff --git a/erpnext/healthcare/module_onboarding/healthcare/healthcare.json b/erpnext/healthcare/module_onboarding/healthcare/healthcare.json index 56c3c135599..0aa8f9a027e 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 c45a347080c..3f25a9d6760 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 77bc5bd7adf..b46bb15b48a 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 65980ef6687..7ce122d5c0b 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 697b761e528..dfe9f71a76f 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 b2d5aef4312..2d952f30938 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 fa4c9036d7e..baa8358c060 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 1bb589164e6..74b5e7eb918 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 be486c62d1e..d16b38637cd 100644 --- a/erpnext/healthcare/page/patient_history/patient_history.html +++ b/erpnext/healthcare/page/patient_history/patient_history.html @@ -1,26 +1,18 @@ -
-
-

-
-
-
-
-
-
-
-
+
+
+
- -
-
-
-
-
-
-
-
-
- +
-
\ No newline at end of file + +
+
+
+
+
+
+
+ +
+
diff --git a/erpnext/healthcare/page/patient_history/patient_history.js b/erpnext/healthcare/page/patient_history/patient_history.js index 54343aae449..bf947cac215 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 00000000000..4560e7e1254 --- /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 5d85a7487fd..737b2e0ea28 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 c20537ea81d..ee60065618f 100644 --- a/erpnext/healthcare/page/patient_progress/patient_progress.html +++ b/erpnext/healthcare/page/patient_progress/patient_progress.html @@ -1,14 +1,15 @@
- diff --git a/erpnext/healthcare/print_format/encounter_print/encounter_print.json b/erpnext/healthcare/print_format/encounter_print/encounter_print.json index ec1e0f202a2..3c90adb0a1c 100644 --- a/erpnext/healthcare/print_format/encounter_print/encounter_print.json +++ b/erpnext/healthcare/print_format/encounter_print/encounter_print.json @@ -16,7 +16,7 @@ "name": "Encounter Print", "owner": "Administrator", "print_format_builder": 0, - "print_format_type": "Server", + "print_format_type": "Jinja", "show_section_headings": 0, "standard": "Yes" } \ No newline at end of file diff --git a/erpnext/healthcare/print_format/sample_id_print/sample_id_print.json b/erpnext/healthcare/print_format/sample_id_print/sample_id_print.json index e99ce708f46..4819e6d57ac 100644 --- a/erpnext/healthcare/print_format/sample_id_print/sample_id_print.json +++ b/erpnext/healthcare/print_format/sample_id_print/sample_id_print.json @@ -16,7 +16,7 @@ "name": "Sample ID Print", "owner": "Administrator", "print_format_builder": 0, - "print_format_type": "Server", + "print_format_type": "Jinja", "show_section_headings": 0, "standard": "Yes" } diff --git a/erpnext/healthcare/report/inpatient_medication_orders/inpatient_medication_orders.py b/erpnext/healthcare/report/inpatient_medication_orders/inpatient_medication_orders.py index b9077301bad..28b60bdcc92 100644 --- a/erpnext/healthcare/report/inpatient_medication_orders/inpatient_medication_orders.py +++ b/erpnext/healthcare/report/inpatient_medication_orders/inpatient_medication_orders.py @@ -195,4 +195,4 @@ def get_chart_data(data): chart["fieldtype"] = "Data" - return chart \ No newline at end of file + return chart diff --git a/erpnext/healthcare/report/inpatient_medication_orders/test_inpatient_medication_orders.py b/erpnext/healthcare/report/inpatient_medication_orders/test_inpatient_medication_orders.py index 4b461f1a97d..0a538fdff0d 100644 --- a/erpnext/healthcare/report/inpatient_medication_orders/test_inpatient_medication_orders.py +++ b/erpnext/healthcare/report/inpatient_medication_orders/test_inpatient_medication_orders.py @@ -25,7 +25,7 @@ class TestInpatientMedicationOrders(unittest.TestCase): 'from_date': getdate(), 'to_date': getdate(), 'patient': '_Test IPD Patient', - 'service_unit': 'Test Service Unit Ip Occupancy - _TC' + 'service_unit': '_Test Service Unit Ip Occupancy - _TC' } report = execute(filters) @@ -42,7 +42,7 @@ class TestInpatientMedicationOrders(unittest.TestCase): 'date': getdate(), 'time': datetime.timedelta(seconds=32400), 'is_completed': 0, - 'healthcare_service_unit': 'Test Service Unit Ip Occupancy - _TC' + 'healthcare_service_unit': '_Test Service Unit Ip Occupancy - _TC' }, { 'patient': '_Test IPD Patient', @@ -55,7 +55,7 @@ class TestInpatientMedicationOrders(unittest.TestCase): 'date': getdate(), 'time': datetime.timedelta(seconds=50400), 'is_completed': 0, - 'healthcare_service_unit': 'Test Service Unit Ip Occupancy - _TC' + 'healthcare_service_unit': '_Test Service Unit Ip Occupancy - _TC' }, { 'patient': '_Test IPD Patient', @@ -68,7 +68,7 @@ class TestInpatientMedicationOrders(unittest.TestCase): 'date': getdate(), 'time': datetime.timedelta(seconds=75600), 'is_completed': 0, - 'healthcare_service_unit': 'Test Service Unit Ip Occupancy - _TC' + 'healthcare_service_unit': '_Test Service Unit Ip Occupancy - _TC' } ] @@ -83,7 +83,7 @@ class TestInpatientMedicationOrders(unittest.TestCase): 'from_date': getdate(), 'to_date': getdate(), 'patient': '_Test IPD Patient', - 'service_unit': 'Test Service Unit Ip Occupancy - _TC', + 'service_unit': '_Test Service Unit Ip Occupancy - _TC', 'show_completed_orders': 0 } @@ -93,12 +93,12 @@ class TestInpatientMedicationOrders(unittest.TestCase): def tearDown(self): if frappe.db.get_value('Patient', self.patient, 'inpatient_record'): # cleanup - Discharge - schedule_discharge(frappe.as_json({'patient': self.patient})) + schedule_discharge(frappe.as_json({'patient': self.patient, 'discharge_ordered_datetime': now_datetime()})) self.ip_record.reload() mark_invoiced_inpatient_occupancy(self.ip_record) self.ip_record.reload() - discharge_patient(self.ip_record) + discharge_patient(self.ip_record, now_datetime()) for entry in frappe.get_all('Inpatient Medication Entry'): doc = frappe.get_doc('Inpatient Medication Entry', entry.name) @@ -119,7 +119,7 @@ def create_records(patient): ip_record.expected_length_of_stay = 0 ip_record.save() ip_record.reload() - service_unit = get_healthcare_service_unit('Test Service Unit Ip Occupancy') + service_unit = get_healthcare_service_unit('_Test Service Unit Ip Occupancy') admit_patient(ip_record, service_unit, now_datetime()) ipmo = create_ipmo(patient) diff --git a/erpnext/healthcare/report/lab_test_report/lab_test_report.py b/erpnext/healthcare/report/lab_test_report/lab_test_report.py index 2e59bed0373..ba4ca4172dc 100644 --- a/erpnext/healthcare/report/lab_test_report/lab_test_report.py +++ b/erpnext/healthcare/report/lab_test_report/lab_test_report.py @@ -169,7 +169,7 @@ def get_chart_data(data): 'labels': labels, 'datasets': datasets }, - 'type': 'donut', + 'type': 'bar', 'height': 300, } diff --git a/erpnext/healthcare/report/patient_appointment_analytics/patient_appointment_analytics.py b/erpnext/healthcare/report/patient_appointment_analytics/patient_appointment_analytics.py index 9c35dbb3ea5..9a4840acfea 100644 --- a/erpnext/healthcare/report/patient_appointment_analytics/patient_appointment_analytics.py +++ b/erpnext/healthcare/report/patient_appointment_analytics/patient_appointment_analytics.py @@ -191,4 +191,4 @@ class Analytics(object): 'datasets': [] }, "type": "line" - } \ No newline at end of file + } diff --git a/erpnext/healthcare/setup.py b/erpnext/healthcare/setup.py index bf4df7e4c88..891272ddf81 100644 --- a/erpnext/healthcare/setup.py +++ b/erpnext/healthcare/setup.py @@ -292,4 +292,4 @@ def get_patient_history_config(): {"label": "Medication Orders", "fieldname": "medication_orders", "fieldtype": "Table"}, {"label": "Total Orders", "fieldname": "total_orders", "fieldtype": "Float"} ]) - } \ No newline at end of file + } diff --git a/erpnext/healthcare/utils.py b/erpnext/healthcare/utils.py index d3d22c80b67..7c80bdb73ce 100644 --- a/erpnext/healthcare/utils.py +++ b/erpnext/healthcare/utils.py @@ -8,8 +8,7 @@ import frappe import json from frappe import _ from frappe.utils.formatters import format_value -from frappe.utils import time_diff_in_hours, rounded -from six import string_types +from frappe.utils import time_diff_in_hours, rounded, cstr from erpnext.healthcare.doctype.healthcare_settings.healthcare_settings import get_income_account from erpnext.healthcare.doctype.fee_validity.fee_validity import create_fee_validity from erpnext.healthcare.doctype.lab_test.lab_test import create_multiple @@ -181,9 +180,9 @@ def get_clinical_procedures_to_invoice(patient, company): service_item = frappe.db.get_single_value('Healthcare Settings', 'clinical_procedure_consumable_item') if not service_item: - msg = _('Please Configure Clinical Procedure Consumable Item in ') - msg += '''Healthcare Settings''' - frappe.throw(msg, title=_('Missing Configuration')) + frappe.throw(_('Please configure Clinical Procedure Consumable Item in {0}').format( + frappe.utils.get_link_to_form('Healthcare Settings', 'Healthcare Settings')), + title=_('Missing Configuration')) clinical_procedures_to_invoice.append({ 'reference_type': 'Clinical Procedure', @@ -312,7 +311,7 @@ def get_therapy_sessions_to_invoice(patient, company): @frappe.whitelist() def get_service_item_and_practitioner_charge(doc): - if isinstance(doc, string_types): + if isinstance(doc, str): doc = json.loads(doc) doc = frappe.get_doc(doc) @@ -543,58 +542,43 @@ def get_drugs_to_invoice(encounter): @frappe.whitelist() -def get_children(doctype, parent, company, is_root=False): - parent_fieldname = "parent_" + doctype.lower().replace(" ", "_") +def get_children(doctype, parent=None, company=None, is_root=False): + parent_fieldname = 'parent_' + doctype.lower().replace(' ', '_') fields = [ - "name as value", - "is_group as expandable", - "lft", - "rgt" + 'name as value', + 'is_group as expandable', + 'lft', + 'rgt' ] - # fields = [ "name", "is_group", "lft", "rgt" ] - filters = [["ifnull(`{0}`,'')".format(parent_fieldname), "=", "" if is_root else parent]] + + filters = [["ifnull(`{0}`,'')".format(parent_fieldname), + '=', '' if is_root else parent]] if is_root: - fields += ["service_unit_type"] if doctype == "Healthcare Service Unit" else [] - filters.append(["company", "=", company]) - + fields += ['service_unit_type'] if doctype == 'Healthcare Service Unit' else [] + filters.append(['company', '=', company]) else: - fields += ["service_unit_type", "allow_appointments", "inpatient_occupancy", "occupancy_status"] if doctype == "Healthcare Service Unit" else [] - fields += [parent_fieldname + " as parent"] + fields += ['service_unit_type', 'allow_appointments', 'inpatient_occupancy', + 'occupancy_status'] if doctype == 'Healthcare Service Unit' else [] + fields += [parent_fieldname + ' as parent'] - hc_service_units = frappe.get_list(doctype, fields=fields, filters=filters) + service_units = frappe.get_list(doctype, fields=fields, filters=filters) + for each in service_units: + if each['expandable'] == 1: # group node + available_count = frappe.db.count('Healthcare Service Unit', filters={ + 'parent_healthcare_service_unit': each['value'], + 'inpatient_occupancy': 1}) - if doctype == "Healthcare Service Unit": - for each in hc_service_units: - occupancy_msg = "" - if each["expandable"] == 1: - occupied = False - vacant = False - child_list = frappe.db.sql( - ''' - SELECT - name, occupancy_status - FROM - `tabHealthcare Service Unit` - WHERE - inpatient_occupancy = 1 - and lft > %s and rgt < %s - ''', (each['lft'], each['rgt'])) + if available_count > 0: + occupied_count = frappe.db.count('Healthcare Service Unit', { + 'parent_healthcare_service_unit': each['value'], + 'inpatient_occupancy': 1, + 'occupancy_status': 'Occupied'}) + # set occupancy status of group node + each['occupied_of_available'] = str( + occupied_count) + ' Occupied of ' + str(available_count) - for child in child_list: - if not occupied: - occupied = 0 - if child[1] == "Occupied": - occupied += 1 - if not vacant: - vacant = 0 - if child[1] == "Vacant": - vacant += 1 - if vacant and occupied: - occupancy_total = vacant + occupied - occupancy_msg = str(occupied) + " Occupied out of " + str(occupancy_total) - each["occupied_out_of_vacant"] = occupancy_msg - return hc_service_units + return service_units @frappe.whitelist() @@ -622,98 +606,181 @@ def render_docs_as_html(docs): @frappe.whitelist() def render_doc_as_html(doctype, docname, exclude_fields = []): - #render document as html, three column layout will break + """ + Render document as HTML + """ + doc = frappe.get_doc(doctype, docname) meta = frappe.get_meta(doctype) - doc_html = "
" - section_html = '' - section_label = '' - html = '' - sec_on = False + doc_html = section_html = section_label = html = "" + sec_on = has_data = False col_on = 0 - has_data = False + for df in meta.fields: - #on section break append append previous section and html to doc html + # on section break append previous section and html to doc html if df.fieldtype == "Section Break": if has_data and col_on and sec_on: doc_html += section_html + html + "
" + elif has_data and not col_on and sec_on: - doc_html += "
" \ - + section_html + html +"
" + doc_html += """ +
+
+
+ {0} +
+
+
+
+ {1} {2} +
+
+ """.format(section_label, section_html, html) + + # close divs for columns while col_on: doc_html += "
" col_on -= 1 + sec_on = True - has_data= False + has_data = False col_on = 0 - section_html = '' - html = '' + section_html = html = "" + if df.label: section_label = df.label continue - #on column break append html to section html or doc html + + # on column break append html to section html or doc html if df.fieldtype == "Column Break": - if sec_on and has_data: - section_html += "
" + section_label + "" + html + "
" - elif has_data: - doc_html += "
" + html + "
" - elif sec_on and not col_on: - section_html += "
" - html = '' + if sec_on and not col_on and has_data: + section_html += """ +
+
+
+ {0} +
+
+
+
+ {1} +
+ """.format(section_label, html) + elif col_on == 1 and has_data: + section_html += "
" + html + "
" + elif col_on > 1 and has_data: + doc_html += "
" + html + "
" + else: + doc_html += """ +
+
+ {0} +
+
+ """.format(html) + + html = "" col_on += 1 + if df.label: - html += '
' + df.label + html += "
" + df.label continue - #on table iterate in items and create table based on in_list_view, append to section html or doc html - if df.fieldtype == 'Table': + + # on table iterate through items and create table + # based on the in_list_view property + # append to section html or doc html + if df.fieldtype == "Table": items = doc.get(df.fieldname) - if not items: continue + if not items: + continue child_meta = frappe.get_meta(df.options) - if not has_data : has_data = True - table_head = '' - table_row = '' + + if not has_data: + has_data = True + table_head = table_row = "" create_head = True + for item in items: - table_row += '' + table_row += "" for cdf in child_meta.fields: if cdf.in_list_view: if create_head: - table_head += '' + cdf.label + '' + table_head += "" + cdf.label + "" if item.get(cdf.fieldname): - table_row += '' + str(item.get(cdf.fieldname)) \ - + '' + table_row += "" + cstr(item.get(cdf.fieldname)) + "" else: - table_row += '' + table_row += "" + create_head = False - table_row += '' + table_row += "" + if sec_on: - section_html += "" + table_head + table_row + '
' + section_html += """ + + {0} {1} +
+ """.format(table_head, table_row) else: - html += "" \ - + table_head + table_row + "
" + html += """ + + {0} {1} +
+ """.format(table_head, table_row) continue - #on other field types add label and value to html + # on any other field type add label and value to html if not df.hidden and not df.print_hide and doc.get(df.fieldname) and df.fieldname not in exclude_fields: - if doc.get(df.fieldname): - formatted_value = format_value(doc.get(df.fieldname), meta.get_field(df.fieldname), doc) - html += '
{0} : {1}'.format(df.label or df.fieldname, formatted_value) + formatted_value = format_value(doc.get(df.fieldname), meta.get_field(df.fieldname), doc) + html += "
{0} : {1}".format(df.label or df.fieldname, formatted_value) if not has_data : has_data = True if sec_on and col_on and has_data: - doc_html += section_html + html + '
' + doc_html += section_html + html + "
" elif sec_on and not col_on and has_data: - doc_html += "
" \ - + section_html + html +'
' - if doc_html: - doc_html = "
" %(doctype, docname) + doc_html + '
' + doc_html += """ +
+
+ {0} {1} +
+
+ """.format(section_html, html) - return {'html': doc_html} + return {"html": doc_html} + + +def update_address_links(address, method): + ''' + Hook validate Address + If Patient is linked in Address, also link the associated Customer + ''' + if 'Healthcare' not in frappe.get_active_domains(): + return + + patient_links = list(filter(lambda link: link.get('link_doctype') == 'Patient', address.links)) + + for link in patient_links: + customer = frappe.db.get_value('Patient', link.get('link_name'), 'customer') + if customer and not address.has_link('Customer', customer): + address.append('links', dict(link_doctype = 'Customer', link_name = customer)) + + +def update_patient_email_and_phone_numbers(contact, method): + ''' + Hook validate Contact + Update linked Patients' primary mobile and phone numbers + ''' + if 'Healthcare' not in frappe.get_active_domains(): + return + + if contact.is_primary_contact and (contact.email_id or contact.mobile_no or contact.phone): + patient_links = list(filter(lambda link: link.get('link_doctype') == 'Patient', contact.links)) + + for link in patient_links: + contact_details = frappe.db.get_value('Patient', link.get('link_name'), ['email', 'mobile', 'phone'], as_dict=1) + if contact.email_id and contact.email_id != contact_details.get('email'): + frappe.db.set_value('Patient', link.get('link_name'), 'email', contact.email_id) + if contact.mobile_no and contact.mobile_no != contact_details.get('mobile'): + frappe.db.set_value('Patient', link.get('link_name'), 'mobile', contact.mobile_no) + if contact.phone and contact.phone != contact_details.get('phone'): + frappe.db.set_value('Patient', link.get('link_name'), 'phone', contact.phone) diff --git a/erpnext/healthcare/web_form/patient_registration/patient_registration.js b/erpnext/healthcare/web_form/patient_registration/patient_registration.js index 7da3f1fb41c..f09e5409192 100644 --- a/erpnext/healthcare/web_form/patient_registration/patient_registration.js +++ b/erpnext/healthcare/web_form/patient_registration/patient_registration.js @@ -1,3 +1,3 @@ frappe.ready(function() { // bind events here -}); \ No newline at end of file +}); diff --git a/erpnext/healthcare/workspace/healthcare/healthcare.json b/erpnext/healthcare/workspace/healthcare/healthcare.json index b93dda2e879..ea5c61e4da9 100644 --- a/erpnext/healthcare/workspace/healthcare/healthcare.json +++ b/erpnext/healthcare/workspace/healthcare/healthcare.json @@ -66,46 +66,6 @@ "onboard": 0, "type": "Link" }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Healthcare Service Unit Type", - "link_to": "Healthcare Service Unit Type", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Healthcare Service Unit", - "link_to": "Healthcare Service Unit", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Medical Code Standard", - "link_to": "Medical Code Standard", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Medical Code", - "link_to": "Medical Code", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, { "hidden": 0, "is_query_report": 0, @@ -163,6 +123,26 @@ "onboard": 0, "type": "Link" }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Complaint", + "link_to": "Complaint", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Diagnosis", + "link_to": "Diagnosis", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, { "hidden": 0, "is_query_report": 0, @@ -210,26 +190,6 @@ "onboard": 0, "type": "Link" }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Complaint", - "link_to": "Complaint", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Diagnosis", - "link_to": "Diagnosis", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, { "dependencies": "", "hidden": 0, @@ -240,6 +200,62 @@ "onboard": 0, "type": "Link" }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Facility Management", + "link_type": "DocType", + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Healthcare Service Unit Type", + "link_to": "Healthcare Service Unit Type", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Healthcare Service Unit", + "link_to": "Healthcare Service Unit", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Medical Coding", + "link_type": "DocType", + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Medical Code Standard", + "link_to": "Medical Code Standard", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Medical Code", + "link_to": "Medical Code", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, { "hidden": 0, "is_query_report": 0, @@ -304,6 +320,16 @@ "onboard": 0, "type": "Link" }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Dosage Form", + "link_to": "Dosage Form", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, { "hidden": 0, "is_query_report": 0, @@ -332,11 +358,36 @@ "type": "Link" }, { - "dependencies": "", "hidden": 0, "is_query_report": 0, - "label": "Dosage Form", - "link_to": "Dosage Form", + "label": "Inpatient", + "link_type": "DocType", + "onboard": 0, + "type": "Card Break" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Inpatient Medication Order", + "link_to": "Inpatient Medication Order", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Inpatient Record", + "link_to": "Inpatient Record", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Inpatient Medication Entry", + "link_to": "Inpatient Medication Entry", "link_type": "DocType", "onboard": 0, "type": "Link" @@ -483,7 +534,7 @@ "type": "Link" } ], - "modified": "2020-12-01 13:38:34.841396", + "modified": "2021-01-30 19:35:45.316999", "modified_by": "Administrator", "module": "Healthcare", "name": "Healthcare", diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 57bc5145b79..746060f1e12 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -24,7 +24,8 @@ doctype_js = { "Address": "public/js/address.js", "Communication": "public/js/communication.js", "Event": "public/js/event.js", - "Newsletter": "public/js/newsletter.js" + "Newsletter": "public/js/newsletter.js", + "Contact": "public/js/contact.js" } override_doctype_class = { @@ -242,7 +243,7 @@ doc_events = { "on_update": ["erpnext.hr.doctype.employee.employee.update_user_permissions", "erpnext.portal.utils.set_default_role"] }, - ("Sales Taxes and Charges Template", 'Price List'): { + "Sales Taxes and Charges Template": { "on_update": "erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings.validate_cart_settings" }, "Website Settings": { @@ -282,7 +283,12 @@ doc_events = { "on_trash": "erpnext.regional.check_deletion_permission" }, 'Address': { - 'validate': ['erpnext.regional.india.utils.validate_gstin_for_india', 'erpnext.regional.italy.utils.set_state_code', 'erpnext.regional.india.utils.update_gst_category'] + 'validate': [ + 'erpnext.regional.india.utils.validate_gstin_for_india', + 'erpnext.regional.italy.utils.set_state_code', + 'erpnext.regional.india.utils.update_gst_category', + 'erpnext.healthcare.utils.update_address_links' + ], }, 'Supplier': { 'validate': 'erpnext.regional.india.utils.validate_pan_for_india' @@ -293,13 +299,16 @@ doc_events = { "Contact": { "on_trash": "erpnext.support.doctype.issue.issue.update_issue", "after_insert": "erpnext.telephony.doctype.call_log.call_log.link_existing_conversations", - "validate": "erpnext.crm.utils.update_lead_phone_numbers" + "validate": ["erpnext.crm.utils.update_lead_phone_numbers", "erpnext.healthcare.utils.update_patient_email_and_phone_numbers"] }, "Email Unsubscribe": { "after_insert": "erpnext.crm.doctype.email_campaign.email_campaign.unsubscribe_recipient" }, ('Quotation', 'Sales Order', 'Sales Invoice'): { 'validate': ["erpnext.erpnext_integrations.taxjar_integration.set_sales_tax"] + }, + "Company": { + "on_trash": "erpnext.regional.india.utils.delete_gst_settings_for_company" } } @@ -345,7 +354,8 @@ scheduler_events = { "erpnext.crm.doctype.opportunity.opportunity.auto_close_opportunity", "erpnext.controllers.accounts_controller.update_invoice_status", "erpnext.accounts.doctype.fiscal_year.fiscal_year.auto_create_fiscal_year", - "erpnext.hr.doctype.employee.employee.send_birthday_reminders", + "erpnext.hr.doctype.employee.employee_reminders.send_work_anniversary_reminders", + "erpnext.hr.doctype.employee.employee_reminders.send_birthday_reminders", "erpnext.projects.doctype.task.task.set_tasks_as_overdue", "erpnext.assets.doctype.asset.depreciation.post_depreciation_entries", "erpnext.hr.doctype.daily_work_summary_group.daily_work_summary_group.send_summary", @@ -377,6 +387,12 @@ scheduler_events = { "erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual.process_loan_interest_accrual_for_term_loans", "erpnext.crm.doctype.lead.lead.daily_open_lead" ], + "weekly": [ + "erpnext.hr.doctype.employee.employee_reminders.send_reminders_in_advance_weekly" + ], + "monthly": [ + "erpnext.hr.doctype.employee.employee_reminders.send_reminders_in_advance_monthly" + ], "monthly_long": [ "erpnext.accounts.deferred_revenue.process_deferred_accounting", "erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual.process_loan_interest_accrual_for_demand_loans" @@ -430,7 +446,8 @@ regional_overrides = { 'erpnext.hr.utils.calculate_annual_eligible_hra_exemption': 'erpnext.regional.india.utils.calculate_annual_eligible_hra_exemption', 'erpnext.hr.utils.calculate_hra_exemption_for_period': 'erpnext.regional.india.utils.calculate_hra_exemption_for_period', 'erpnext.controllers.accounts_controller.validate_einvoice_fields': 'erpnext.regional.india.e_invoice.utils.validate_einvoice_fields', - 'erpnext.assets.doctype.asset.asset.get_depreciation_amount': 'erpnext.regional.india.utils.get_depreciation_amount' + 'erpnext.assets.doctype.asset.asset.get_depreciation_amount': 'erpnext.regional.india.utils.get_depreciation_amount', + 'erpnext.stock.doctype.item.item.set_item_tax_from_hsn_code': 'erpnext.regional.india.utils.set_item_tax_from_hsn_code' }, 'United Arab Emirates': { 'erpnext.controllers.taxes_and_totals.update_itemised_tax_data': 'erpnext.regional.united_arab_emirates.utils.update_itemised_tax_data', diff --git a/erpnext/hotels/doctype/hotel_room/hotel_room.py b/erpnext/hotels/doctype/hotel_room/hotel_room.py index 8471aee4a03..6a2fc02574f 100644 --- a/erpnext/hotels/doctype/hotel_room/hotel_room.py +++ b/erpnext/hotels/doctype/hotel_room/hotel_room.py @@ -10,4 +10,4 @@ class HotelRoom(Document): def validate(self): if not self.capacity: self.capacity, self.extra_bed_capacity = frappe.db.get_value('Hotel Room Type', - self.hotel_room_type, ['capacity', 'extra_bed_capacity']) \ No newline at end of file + self.hotel_room_type, ['capacity', 'extra_bed_capacity']) diff --git a/erpnext/hotels/doctype/hotel_room/test_hotel_room.js b/erpnext/hotels/doctype/hotel_room/test_hotel_room.js deleted file mode 100644 index 8b2b83330f3..00000000000 --- a/erpnext/hotels/doctype/hotel_room/test_hotel_room.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: Hotel Room", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Hotel Room - () => frappe.tests.make('Hotel Room', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/hotels/doctype/hotel_room_package/test_hotel_room_package.js b/erpnext/hotels/doctype/hotel_room_package/test_hotel_room_package.js deleted file mode 100644 index f1ebad41d41..00000000000 --- a/erpnext/hotels/doctype/hotel_room_package/test_hotel_room_package.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: Hotel Room Package", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Hotel Room Package - () => frappe.tests.make('Hotel Room Package', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/hotels/doctype/hotel_room_pricing/test_hotel_room_pricing.js b/erpnext/hotels/doctype/hotel_room_pricing/test_hotel_room_pricing.js deleted file mode 100644 index ba0d1fd3e96..00000000000 --- a/erpnext/hotels/doctype/hotel_room_pricing/test_hotel_room_pricing.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: Hotel Room Pricing", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Hotel Room Pricing - () => frappe.tests.make('Hotel Room Pricing', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/hotels/doctype/hotel_room_pricing_package/test_hotel_room_pricing_package.js b/erpnext/hotels/doctype/hotel_room_pricing_package/test_hotel_room_pricing_package.js deleted file mode 100644 index 73a561c408c..00000000000 --- a/erpnext/hotels/doctype/hotel_room_pricing_package/test_hotel_room_pricing_package.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: Hotel Room Pricing Package", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Hotel Room Pricing Package - () => frappe.tests.make('Hotel Room Pricing Package', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/hotels/doctype/hotel_room_reservation/hotel_room_reservation_calendar.js b/erpnext/hotels/doctype/hotel_room_reservation/hotel_room_reservation_calendar.js index 7f7322cf4b6..7bde292a2bc 100644 --- a/erpnext/hotels/doctype/hotel_room_reservation/hotel_room_reservation_calendar.js +++ b/erpnext/hotels/doctype/hotel_room_reservation/hotel_room_reservation_calendar.js @@ -6,4 +6,4 @@ frappe.views.calendar["Hotel Room Reservation"] = { "title": "guest_name", "status": "status" } -} \ No newline at end of file +} diff --git a/erpnext/hotels/doctype/hotel_room_reservation/test_hotel_room_reservation.js b/erpnext/hotels/doctype/hotel_room_reservation/test_hotel_room_reservation.js deleted file mode 100644 index 2897139359b..00000000000 --- a/erpnext/hotels/doctype/hotel_room_reservation/test_hotel_room_reservation.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: Hotel Room Reservation", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Hotel Room Reservation - () => frappe.tests.make('Hotel Room Reservation', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/hotels/doctype/hotel_room_type/test_hotel_room_type.js b/erpnext/hotels/doctype/hotel_room_type/test_hotel_room_type.js deleted file mode 100644 index e2dd5780e3c..00000000000 --- a/erpnext/hotels/doctype/hotel_room_type/test_hotel_room_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: Hotel Room Type", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Hotel Room Type - () => frappe.tests.make('Hotel Room Type', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/hotels/doctype/hotel_settings/test_hotel_settings.js b/erpnext/hotels/doctype/hotel_settings/test_hotel_settings.js deleted file mode 100644 index bc0b7f8341d..00000000000 --- a/erpnext/hotels/doctype/hotel_settings/test_hotel_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: Hotel Settings", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Hotel Settings - () => frappe.tests.make('Hotel Settings', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/hotels/report/hotel_room_occupancy/hotel_room_occupancy.py b/erpnext/hotels/report/hotel_room_occupancy/hotel_room_occupancy.py index f77d43b3143..259edb9c06d 100644 --- a/erpnext/hotels/report/hotel_room_occupancy/hotel_room_occupancy.py +++ b/erpnext/hotels/report/hotel_room_occupancy/hotel_room_occupancy.py @@ -30,4 +30,4 @@ def get_data(filters): out.append([room_type.name, total_booked]) - return out \ No newline at end of file + return out diff --git a/erpnext/hr/doctype/appraisal/appraisal.js b/erpnext/hr/doctype/appraisal/appraisal.js index 1a30ceac6d3..50612b923ef 100644 --- a/erpnext/hr/doctype/appraisal/appraisal.js +++ b/erpnext/hr/doctype/appraisal/appraisal.js @@ -15,7 +15,7 @@ frappe.ui.form.on('Appraisal', { frm.set_value('status', 'Draft'); } }, - + kra_template: function(frm) { frm.doc.goals = []; erpnext.utils.map_current_doc({ diff --git a/erpnext/hr/doctype/appraisal/appraisal.py b/erpnext/hr/doctype/appraisal/appraisal.py index f7601870fac..c2ed4579844 100644 --- a/erpnext/hr/doctype/appraisal/appraisal.py +++ b/erpnext/hr/doctype/appraisal/appraisal.py @@ -9,7 +9,7 @@ from frappe.utils import flt, getdate from frappe import _ from frappe.model.mapper import get_mapped_doc from frappe.model.document import Document -from erpnext.hr.utils import set_employee_name +from erpnext.hr.utils import set_employee_name, validate_active_employee class Appraisal(Document): def validate(self): @@ -19,6 +19,7 @@ class Appraisal(Document): if not self.goals: frappe.throw(_("Goals cannot be empty")) + validate_active_employee(self.employee) set_employee_name(self) self.validate_dates() self.validate_existing_appraisal() diff --git a/erpnext/hr/doctype/appraisal/test_appraisal.js b/erpnext/hr/doctype/appraisal/test_appraisal.js index 9ca17e2e226..fb1354c3f6b 100644 --- a/erpnext/hr/doctype/appraisal/test_appraisal.js +++ b/erpnext/hr/doctype/appraisal/test_appraisal.js @@ -55,4 +55,3 @@ QUnit.test("Test: Expense Claim [HR]", function (assert) { () => done() ]); }); - diff --git a/erpnext/hr/doctype/appraisal_goal/appraisal_goal.py b/erpnext/hr/doctype/appraisal_goal/appraisal_goal.py index a6868ee2b1c..11d9f3944d5 100644 --- a/erpnext/hr/doctype/appraisal_goal/appraisal_goal.py +++ b/erpnext/hr/doctype/appraisal_goal/appraisal_goal.py @@ -7,4 +7,4 @@ import frappe from frappe.model.document import Document class AppraisalGoal(Document): - pass \ No newline at end of file + pass diff --git a/erpnext/hr/doctype/appraisal_template/appraisal_template_dashboard.py b/erpnext/hr/doctype/appraisal_template/appraisal_template_dashboard.py index 309427e30c2..392b370e6c3 100644 --- a/erpnext/hr/doctype/appraisal_template/appraisal_template_dashboard.py +++ b/erpnext/hr/doctype/appraisal_template/appraisal_template_dashboard.py @@ -9,4 +9,4 @@ def get_data(): 'items': ['Appraisal'] }, ], - } \ No newline at end of file + } diff --git a/erpnext/hr/doctype/appraisal_template/test_appraisal_template.js b/erpnext/hr/doctype/appraisal_template/test_appraisal_template.js index 0403cad0683..3eb64e08501 100644 --- a/erpnext/hr/doctype/appraisal_template/test_appraisal_template.js +++ b/erpnext/hr/doctype/appraisal_template/test_appraisal_template.js @@ -27,4 +27,3 @@ QUnit.test("Test: Appraisal Template [HR]", function (assert) { () => done() ]); }); - diff --git a/erpnext/hr/doctype/appraisal_template_goal/appraisal_template_goal.py b/erpnext/hr/doctype/appraisal_template_goal/appraisal_template_goal.py index ca58e0c3202..b3c5704fa53 100644 --- a/erpnext/hr/doctype/appraisal_template_goal/appraisal_template_goal.py +++ b/erpnext/hr/doctype/appraisal_template_goal/appraisal_template_goal.py @@ -7,4 +7,4 @@ import frappe from frappe.model.document import Document class AppraisalTemplateGoal(Document): - pass \ No newline at end of file + pass diff --git a/erpnext/hr/doctype/attendance/attendance.py b/erpnext/hr/doctype/attendance/attendance.py index 3412675d811..c1a7c8f88a5 100644 --- a/erpnext/hr/doctype/attendance/attendance.py +++ b/erpnext/hr/doctype/attendance/attendance.py @@ -8,11 +8,13 @@ from frappe.utils import getdate, nowdate from frappe import _ from frappe.model.document import Document from frappe.utils import cstr, get_datetime, formatdate +from erpnext.hr.utils import validate_active_employee class Attendance(Document): def validate(self): from erpnext.controllers.status_updater import validate_status validate_status(self.status, ["Present", "Absent", "On Leave", "Half Day", "Work From Home"]) + validate_active_employee(self.employee) self.validate_attendance_date() self.validate_duplicate_record() self.validate_employee_status() @@ -133,7 +135,7 @@ def mark_attendance(employee, attendance_date, status, shift=None, leave_type=No def mark_bulk_attendance(data): import json from pprint import pprint - if isinstance(data, frappe.string_types): + if isinstance(data, str): data = json.loads(data) data = frappe._dict(data) company = frappe.get_value('Employee', data.employee, 'company') diff --git a/erpnext/hr/doctype/attendance/attendance_calendar.js b/erpnext/hr/doctype/attendance/attendance_calendar.js index 45664896965..d9f6d2eb3eb 100644 --- a/erpnext/hr/doctype/attendance/attendance_calendar.js +++ b/erpnext/hr/doctype/attendance/attendance_calendar.js @@ -9,4 +9,4 @@ frappe.views.calendar["Attendance"] = { } }, get_events_method: "erpnext.hr.doctype.attendance.attendance.get_events" -}; \ No newline at end of file +}; diff --git a/erpnext/hr/doctype/attendance/test_attendance.js b/erpnext/hr/doctype/attendance/test_attendance.js index 8f30e8cc161..b3e7fef02a0 100644 --- a/erpnext/hr/doctype/attendance/test_attendance.js +++ b/erpnext/hr/doctype/attendance/test_attendance.js @@ -36,4 +36,4 @@ QUnit.test("Test: Attendance [HR]", function (assert) { "attendance for Present day is marked"), () => done() ]); -}); \ No newline at end of file +}); diff --git a/erpnext/hr/doctype/attendance_request/attendance_request.py b/erpnext/hr/doctype/attendance_request/attendance_request.py index 090d53262cf..7f88fed73a2 100644 --- a/erpnext/hr/doctype/attendance_request/attendance_request.py +++ b/erpnext/hr/doctype/attendance_request/attendance_request.py @@ -8,10 +8,11 @@ from frappe import _ from frappe.model.document import Document from frappe.utils import date_diff, add_days, getdate from erpnext.hr.doctype.employee.employee import is_holiday -from erpnext.hr.utils import validate_dates +from erpnext.hr.utils import validate_dates, validate_active_employee class AttendanceRequest(Document): def validate(self): + validate_active_employee(self.employee) validate_dates(self, self.from_date, self.to_date) if self.half_day: if not getdate(self.from_date)<=getdate(self.half_day_date)<=getdate(self.to_date): diff --git a/erpnext/hr/doctype/attendance_request/attendance_request_dashboard.py b/erpnext/hr/doctype/attendance_request/attendance_request_dashboard.py index cfdd6d3aefb..2d3eb000119 100644 --- a/erpnext/hr/doctype/attendance_request/attendance_request_dashboard.py +++ b/erpnext/hr/doctype/attendance_request/attendance_request_dashboard.py @@ -8,4 +8,4 @@ def get_data(): 'items': ['Attendance'] } ] - } \ No newline at end of file + } diff --git a/erpnext/hr/doctype/attendance_request/test_attendance_request.js b/erpnext/hr/doctype/attendance_request/test_attendance_request.js deleted file mode 100644 index d40ec61b086..00000000000 --- a/erpnext/hr/doctype/attendance_request/test_attendance_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: Attendance Request", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Attendance Request - () => frappe.tests.make('Attendance Request', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/hr/doctype/attendance_request/test_attendance_request.py b/erpnext/hr/doctype/attendance_request/test_attendance_request.py index 3c42bd9fc35..9e668aa72fb 100644 --- a/erpnext/hr/doctype/attendance_request/test_attendance_request.py +++ b/erpnext/hr/doctype/attendance_request/test_attendance_request.py @@ -15,7 +15,11 @@ class TestAttendanceRequest(unittest.TestCase): for doctype in ["Attendance Request", "Attendance"]: frappe.db.sql("delete from `tab{doctype}`".format(doctype=doctype)) + def tearDown(self): + frappe.db.rollback() + def test_on_duty_attendance_request(self): + "Test creation/updation of Attendace from Attendance Request, on duty." today = nowdate() employee = get_employee() attendance_request = frappe.new_doc("Attendance Request") @@ -26,17 +30,36 @@ class TestAttendanceRequest(unittest.TestCase): attendance_request.company = "_Test Company" attendance_request.insert() attendance_request.submit() - attendance = frappe.get_doc('Attendance', { - 'employee': employee.name, - 'attendance_date': date(date.today().year, 1, 1), - 'docstatus': 1 - }) - self.assertEqual(attendance.status, 'Present') + + attendance = frappe.db.get_value( + "Attendance", + filters={ + "attendance_request": attendance_request.name, + "attendance_date": date(date.today().year, 1, 1) + }, + fieldname=["status", "docstatus"], + as_dict=True + ) + self.assertEqual(attendance.status, "Present") + self.assertEqual(attendance.docstatus, 1) + + # cancelling attendance request cancels linked attendances attendance_request.cancel() - attendance.reload() - self.assertEqual(attendance.docstatus, 2) + + # cancellation alters docname + # fetch attendance value again to avoid stale docname + attendance_docstatus = frappe.db.get_value( + "Attendance", + filters={ + "attendance_request": attendance_request.name, + "attendance_date": date(date.today().year, 1, 1) + }, + fieldname="docstatus" + ) + self.assertEqual(attendance_docstatus, 2) def test_work_from_home_attendance_request(self): + "Test creation/updation of Attendace from Attendance Request, work from home." today = nowdate() employee = get_employee() attendance_request = frappe.new_doc("Attendance Request") @@ -47,15 +70,30 @@ class TestAttendanceRequest(unittest.TestCase): attendance_request.company = "_Test Company" attendance_request.insert() attendance_request.submit() - attendance = frappe.get_doc('Attendance', { - 'employee': employee.name, - 'attendance_date': date(date.today().year, 1, 1), - 'docstatus': 1 - }) - self.assertEqual(attendance.status, 'Work From Home') + + attendance_status = frappe.db.get_value( + "Attendance", + filters={ + "attendance_request": attendance_request.name, + "attendance_date": date(date.today().year, 1, 1) + }, + fieldname="status" + ) + self.assertEqual(attendance_status, 'Work From Home') + attendance_request.cancel() - attendance.reload() - self.assertEqual(attendance.docstatus, 2) + + # cancellation alters docname + # fetch attendance value again to avoid stale docname + attendance_docstatus = frappe.db.get_value( + "Attendance", + filters={ + "attendance_request": attendance_request.name, + "attendance_date": date(date.today().year, 1, 1) + }, + fieldname="docstatus" + ) + self.assertEqual(attendance_docstatus, 2) def get_employee(): return frappe.get_doc("Employee", "_T-Employee-00001") diff --git a/erpnext/hr/doctype/branch/branch.py b/erpnext/hr/doctype/branch/branch.py index fab2ffc1a37..a847c8e2174 100644 --- a/erpnext/hr/doctype/branch/branch.py +++ b/erpnext/hr/doctype/branch/branch.py @@ -7,4 +7,4 @@ import frappe from frappe.model.document import Document class Branch(Document): - pass \ No newline at end of file + pass diff --git a/erpnext/hr/doctype/branch/test_branch.js b/erpnext/hr/doctype/branch/test_branch.js deleted file mode 100644 index c315385f116..00000000000 --- a/erpnext/hr/doctype/branch/test_branch.js +++ /dev/null @@ -1,23 +0,0 @@ -QUnit.module('hr'); - -QUnit.test("Test: Branch [HR]", function (assert) { - assert.expect(1); - let done = assert.async(); - - frappe.run_serially([ - // test branch creation - () => frappe.set_route("List", "Branch", "List"), - () => frappe.new_doc("Branch"), - () => frappe.timeout(1), - () => frappe.quick_entry.dialog.$wrapper.find('.edit-full').click(), - () => frappe.timeout(1), - () => cur_frm.set_value("branch", "Test Branch"), - - // save form - () => cur_frm.save(), - () => frappe.timeout(1), - () => assert.equal("Test Branch", cur_frm.doc.branch, - 'name of branch correctly saved'), - () => done() - ]); -}); \ No newline at end of file diff --git a/erpnext/hr/doctype/branch/test_branch.py b/erpnext/hr/doctype/branch/test_branch.py index 5ba02b36b8a..807698ba0a2 100644 --- a/erpnext/hr/doctype/branch/test_branch.py +++ b/erpnext/hr/doctype/branch/test_branch.py @@ -4,4 +4,4 @@ from __future__ import unicode_literals import frappe -test_records = frappe.get_test_records('Branch') \ No newline at end of file +test_records = frappe.get_test_records('Branch') diff --git a/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py b/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py index a6fe429be17..3db81654a65 100644 --- a/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py +++ b/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py @@ -7,12 +7,13 @@ import frappe from frappe import _ from frappe.utils import date_diff, add_days, getdate, cint, format_date from frappe.model.document import Document -from erpnext.hr.utils import validate_dates, validate_overlap, get_leave_period, \ - get_holidays_for_employee, create_additional_leave_ledger_entry +from erpnext.hr.utils import validate_dates, validate_overlap, get_leave_period, validate_active_employee, \ + create_additional_leave_ledger_entry, get_holiday_dates_for_employee class CompensatoryLeaveRequest(Document): def validate(self): + validate_active_employee(self.employee) validate_dates(self, self.work_from_date, self.work_end_date) if self.half_day: if not self.half_day_date: @@ -38,7 +39,7 @@ class CompensatoryLeaveRequest(Document): frappe.throw(_("You are not present all day(s) between compensatory leave request days")) def validate_holidays(self): - holidays = get_holidays_for_employee(self.employee, self.work_from_date, self.work_end_date) + holidays = get_holiday_dates_for_employee(self.employee, self.work_from_date, self.work_end_date) if len(holidays) < date_diff(self.work_end_date, self.work_from_date) + 1: if date_diff(self.work_end_date, self.work_from_date): msg = _("The days between {0} to {1} are not valid holidays.").format(frappe.bold(format_date(self.work_from_date)), frappe.bold(format_date(self.work_end_date))) diff --git a/erpnext/hr/doctype/compensatory_leave_request/test_compensatory_leave_request.js b/erpnext/hr/doctype/compensatory_leave_request/test_compensatory_leave_request.js deleted file mode 100644 index bebcaac400e..00000000000 --- a/erpnext/hr/doctype/compensatory_leave_request/test_compensatory_leave_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: Compensatory Leave Request", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Compensatory Leave Request - () => frappe.tests.make('Compensatory Leave Request', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/hr/doctype/daily_work_summary/test_daily_work_summary.js b/erpnext/hr/doctype/daily_work_summary/test_daily_work_summary.js deleted file mode 100644 index d2ceb8bd527..00000000000 --- a/erpnext/hr/doctype/daily_work_summary/test_daily_work_summary.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: Daily Work Summary", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Daily Work Summary - () => frappe.tests.make('Daily Work Summary', [ - // values to be set - { key: 'value' } - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); \ No newline at end of file diff --git a/erpnext/hr/doctype/department/department_tree.js b/erpnext/hr/doctype/department/department_tree.js index 52d864bc0e6..5c7726de6a6 100644 --- a/erpnext/hr/doctype/department/department_tree.js +++ b/erpnext/hr/doctype/department/department_tree.js @@ -25,4 +25,4 @@ frappe.treeview_settings["Department"] = { onload: function(treeview) { treeview.make_tree(); } -}; \ No newline at end of file +}; diff --git a/erpnext/hr/doctype/department/test_department.js b/erpnext/hr/doctype/department/test_department.js deleted file mode 100644 index 3a571f76535..00000000000 --- a/erpnext/hr/doctype/department/test_department.js +++ /dev/null @@ -1,23 +0,0 @@ -QUnit.module('hr'); - -QUnit.test("Test: Department [HR]", function (assert) { - assert.expect(1); - let done = assert.async(); - - frappe.run_serially([ - // test department creation - () => frappe.set_route("List", "Department", "List"), - () => frappe.new_doc("Department"), - () => frappe.timeout(1), - () => frappe.quick_entry.dialog.$wrapper.find('.edit-full').click(), - () => frappe.timeout(1), - () => cur_frm.set_value("department_name", "Test Department"), - () => cur_frm.set_value("leave_block_list", "Test Leave block list"), - // save form - () => cur_frm.save(), - () => frappe.timeout(1), - () => assert.equal("Test Department", cur_frm.doc.department_name, - 'name of department correctly saved'), - () => done() - ]); -}); \ No newline at end of file diff --git a/erpnext/hr/doctype/department/test_department.py b/erpnext/hr/doctype/department/test_department.py index 2eeca26e303..e4f6645ee42 100644 --- a/erpnext/hr/doctype/department/test_department.py +++ b/erpnext/hr/doctype/department/test_department.py @@ -21,4 +21,4 @@ def create_department(department_name, parent_department=None): return doc -test_records = frappe.get_test_records('Department') \ No newline at end of file +test_records = frappe.get_test_records('Department') diff --git a/erpnext/hr/doctype/designation/designation.py b/erpnext/hr/doctype/designation/designation.py index efd864ad593..a3f84aab5f0 100644 --- a/erpnext/hr/doctype/designation/designation.py +++ b/erpnext/hr/doctype/designation/designation.py @@ -7,4 +7,4 @@ import frappe from frappe.model.document import Document class Designation(Document): - pass \ No newline at end of file + pass diff --git a/erpnext/hr/doctype/designation/test_designation.js b/erpnext/hr/doctype/designation/test_designation.js deleted file mode 100644 index 45c34171911..00000000000 --- a/erpnext/hr/doctype/designation/test_designation.js +++ /dev/null @@ -1,23 +0,0 @@ -QUnit.module('hr'); - -QUnit.test("Test: Designation [HR]", function (assert) { - assert.expect(1); - let done = assert.async(); - - frappe.run_serially([ - // test designation creation - () => frappe.set_route("List", "Designation", "List"), - () => frappe.new_doc("Designation"), - () => frappe.timeout(1), - () => frappe.quick_entry.dialog.$wrapper.find('.edit-full').click(), - () => frappe.timeout(1), - () => cur_frm.set_value("designation_name", "Test Designation"), - () => cur_frm.set_value("description", "This designation is just for testing."), - // save form - () => cur_frm.save(), - () => frappe.timeout(1), - () => assert.equal("Test Designation", cur_frm.doc.designation_name, - 'name of designation correctly saved'), - () => done() - ]); -}); \ No newline at end of file diff --git a/erpnext/hr/doctype/designation/test_designation.py b/erpnext/hr/doctype/designation/test_designation.py index 3b300941a65..2778862a1c2 100644 --- a/erpnext/hr/doctype/designation/test_designation.py +++ b/erpnext/hr/doctype/designation/test_designation.py @@ -17,4 +17,4 @@ def create_designation(**args): "description": args.description or "_Test description" }) designation.save() - return designation \ No newline at end of file + return designation diff --git a/erpnext/hr/doctype/driver/test_driver.js b/erpnext/hr/doctype/driver/test_driver.js deleted file mode 100644 index ff9f61e66a6..00000000000 --- a/erpnext/hr/doctype/driver/test_driver.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: Driver", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Driver - () => frappe.tests.make('Driver', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/hr/doctype/employee/employee.py b/erpnext/hr/doctype/employee/employee.py index fa017d9d4c2..643f3da2ff7 100755 --- a/erpnext/hr/doctype/employee/employee.py +++ b/erpnext/hr/doctype/employee/employee.py @@ -1,7 +1,5 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt - -from __future__ import unicode_literals import frappe from frappe.utils import getdate, validate_email_address, today, add_years, cstr @@ -9,12 +7,13 @@ from frappe.model.naming import set_name_by_naming_series from frappe import throw, _, scrub from frappe.permissions import add_user_permission, remove_user_permission, \ set_user_permission_if_allowed, has_permission, get_doc_permissions -from frappe.model.document import Document from erpnext.utilities.transaction_base import delete_events from frappe.utils.nestedset import NestedSet -class EmployeeUserDisabledError(frappe.ValidationError): pass -class EmployeeLeftValidationError(frappe.ValidationError): pass +class EmployeeUserDisabledError(frappe.ValidationError): + pass +class InactiveEmployeeStatusError(frappe.ValidationError): + pass class Employee(NestedSet): nsm_parent_field = 'reports_to' @@ -196,7 +195,7 @@ class Employee(NestedSet): message += "

  • " + "
  • ".join(link_to_employees) message += "

" message += _("Please make sure the employees above report to another Active employee.") - throw(message, EmployeeLeftValidationError, _("Cannot Relieve Employee")) + throw(message, InactiveEmployeeStatusError, _("Cannot Relieve Employee")) if not self.relieving_date: throw(_("Please enter relieving date.")) @@ -284,94 +283,8 @@ def update_user_permissions(doc, method): employee = frappe.get_doc("Employee", {"user_id": doc.name}) employee.update_user_permissions() -def send_birthday_reminders(): - """Send Employee birthday reminders if no 'Stop Birthday Reminders' is not set.""" - if int(frappe.db.get_single_value("HR Settings", "stop_birthday_reminders") or 0): - return - - employees_born_today = get_employees_who_are_born_today() - - for company, birthday_persons in employees_born_today.items(): - employee_emails = get_all_employee_emails(company) - birthday_person_emails = [get_employee_email(doc) for doc in birthday_persons] - recipients = list(set(employee_emails) - set(birthday_person_emails)) - - reminder_text, message = get_birthday_reminder_text_and_message(birthday_persons) - send_birthday_reminder(recipients, reminder_text, birthday_persons, message) - - if len(birthday_persons) > 1: - # special email for people sharing birthdays - for person in birthday_persons: - person_email = person["user_id"] or person["personal_email"] or person["company_email"] - others = [d for d in birthday_persons if d != person] - reminder_text, message = get_birthday_reminder_text_and_message(others) - send_birthday_reminder(person_email, reminder_text, others, message) - def get_employee_email(employee_doc): - return employee_doc["user_id"] or employee_doc["personal_email"] or employee_doc["company_email"] - -def get_birthday_reminder_text_and_message(birthday_persons): - if len(birthday_persons) == 1: - birthday_person_text = birthday_persons[0]['name'] - else: - # converts ["Jim", "Rim", "Dim"] to Jim, Rim & Dim - person_names = [d['name'] for d in birthday_persons] - last_person = person_names[-1] - birthday_person_text = ", ".join(person_names[:-1]) - birthday_person_text = _("{} & {}").format(birthday_person_text, last_person) - - reminder_text = _("Today is {0}'s birthday 🎉").format(birthday_person_text) - message = _("A friendly reminder of an important date for our team.") - message += "
" - message += _("Everyone, let’s congratulate {0} on their birthday.").format(birthday_person_text) - - return reminder_text, message - -def send_birthday_reminder(recipients, reminder_text, birthday_persons, message): - frappe.sendmail( - recipients=recipients, - subject=_("Birthday Reminder"), - template="birthday_reminder", - args=dict( - reminder_text=reminder_text, - birthday_persons=birthday_persons, - message=message, - ), - header=_("Birthday Reminder 🎂") - ) - -def get_employees_who_are_born_today(): - """Get all employee born today & group them based on their company""" - from collections import defaultdict - employees_born_today = frappe.db.multisql({ - "mariadb": """ - SELECT `personal_email`, `company`, `company_email`, `user_id`, `employee_name` AS 'name', `image` - FROM `tabEmployee` - WHERE - DAY(date_of_birth) = DAY(%(today)s) - AND - MONTH(date_of_birth) = MONTH(%(today)s) - AND - `status` = 'Active' - """, - "postgres": """ - SELECT "personal_email", "company", "company_email", "user_id", "employee_name" AS 'name', "image" - FROM "tabEmployee" - WHERE - DATE_PART('day', "date_of_birth") = date_part('day', %(today)s) - AND - DATE_PART('month', "date_of_birth") = date_part('month', %(today)s) - AND - "status" = 'Active' - """, - }, dict(today=today()), as_dict=1) - - grouped_employees = defaultdict(lambda: []) - - for employee_doc in employees_born_today: - grouped_employees[employee_doc.get('company')].append(employee_doc) - - return grouped_employees + return employee_doc.get("user_id") or employee_doc.get("personal_email") or employee_doc.get("company_email") def get_holiday_list_for_employee(employee, raise_exception=True): if employee: @@ -388,17 +301,40 @@ def get_holiday_list_for_employee(employee, raise_exception=True): return holiday_list -def is_holiday(employee, date=None, raise_exception=True): - '''Returns True if given Employee has an holiday on the given date - :param employee: Employee `name` - :param date: Date to check. Will check for today if None''' +def is_holiday(employee, date=None, raise_exception=True, only_non_weekly=False, with_description=False): + ''' + Returns True if given Employee has an holiday on the given date + :param employee: Employee `name` + :param date: Date to check. Will check for today if None + :param raise_exception: Raise an exception if no holiday list found, default is True + :param only_non_weekly: Check only non-weekly holidays, default is False + ''' holiday_list = get_holiday_list_for_employee(employee, raise_exception) if not date: date = today() - if holiday_list: - return frappe.get_all('Holiday List', dict(name=holiday_list, holiday_date=date)) and True or False + if not holiday_list: + return False + + filters = { + 'parent': holiday_list, + 'holiday_date': date + } + if only_non_weekly: + filters['weekly_off'] = False + + holidays = frappe.get_all( + 'Holiday', + fields=['description'], + filters=filters, + pluck='description' + ) + + if with_description: + return len(holidays) > 0, holidays + + return len(holidays) > 0 @frappe.whitelist() def deactivate_sales_person(status = None, employee = None): @@ -501,7 +437,6 @@ def get_children(doctype, parent=None, company=None, is_root=False, is_tree=Fals return employees - def on_doctype_update(): frappe.db.add_index("Employee", ["lft", "rgt"]) @@ -518,4 +453,4 @@ def has_upload_permission(doc, ptype='read', user=None): user = frappe.session.user if get_doc_permissions(doc, user=user, ptype=ptype).get(ptype): return True - return doc.user_id == user \ No newline at end of file + return doc.user_id == user diff --git a/erpnext/hr/doctype/employee/employee_reminders.py b/erpnext/hr/doctype/employee/employee_reminders.py new file mode 100644 index 00000000000..2155c027a9b --- /dev/null +++ b/erpnext/hr/doctype/employee/employee_reminders.py @@ -0,0 +1,247 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + +import frappe +from frappe import _ +from frappe.utils import comma_sep, getdate, today, add_months, add_days +from erpnext.hr.doctype.employee.employee import get_all_employee_emails, get_employee_email +from erpnext.hr.utils import get_holidays_for_employee + +# ----------------- +# HOLIDAY REMINDERS +# ----------------- +def send_reminders_in_advance_weekly(): + to_send_in_advance = int(frappe.db.get_single_value("HR Settings", "send_holiday_reminders") or 1) + frequency = frappe.db.get_single_value("HR Settings", "frequency") + if not (to_send_in_advance and frequency == "Weekly"): + return + + send_advance_holiday_reminders("Weekly") + +def send_reminders_in_advance_monthly(): + to_send_in_advance = int(frappe.db.get_single_value("HR Settings", "send_holiday_reminders") or 1) + frequency = frappe.db.get_single_value("HR Settings", "frequency") + if not (to_send_in_advance and frequency == "Monthly"): + return + + send_advance_holiday_reminders("Monthly") + +def send_advance_holiday_reminders(frequency): + """Send Holiday Reminders in Advance to Employees + `frequency` (str): 'Weekly' or 'Monthly' + """ + if frequency == "Weekly": + start_date = getdate() + end_date = add_days(getdate(), 7) + elif frequency == "Monthly": + # Sent on 1st of every month + start_date = getdate() + end_date = add_months(getdate(), 1) + else: + return + + employees = frappe.db.get_all('Employee', pluck='name') + for employee in employees: + holidays = get_holidays_for_employee( + employee, + start_date, end_date, + only_non_weekly=True, + raise_exception=False + ) + + if not (holidays is None): + send_holidays_reminder_in_advance(employee, holidays) + +def send_holidays_reminder_in_advance(employee, holidays): + employee_doc = frappe.get_doc('Employee', employee) + employee_email = get_employee_email(employee_doc) + frequency = frappe.db.get_single_value("HR Settings", "frequency") + + email_header = _("Holidays this Month.") if frequency == "Monthly" else _("Holidays this Week.") + frappe.sendmail( + recipients=[employee_email], + subject=_("Upcoming Holidays Reminder"), + template="holiday_reminder", + args=dict( + reminder_text=_("Hey {}! This email is to remind you about the upcoming holidays.").format(employee_doc.get('first_name')), + message=_("Below is the list of upcoming holidays for you:"), + advance_holiday_reminder=True, + holidays=holidays, + frequency=frequency[:-2] + ), + header=email_header + ) + +# ------------------ +# BIRTHDAY REMINDERS +# ------------------ +def send_birthday_reminders(): + """Send Employee birthday reminders if no 'Stop Birthday Reminders' is not set.""" + to_send = int(frappe.db.get_single_value("HR Settings", "send_birthday_reminders") or 1) + if not to_send: + return + + employees_born_today = get_employees_who_are_born_today() + + for company, birthday_persons in employees_born_today.items(): + employee_emails = get_all_employee_emails(company) + birthday_person_emails = [get_employee_email(doc) for doc in birthday_persons] + recipients = list(set(employee_emails) - set(birthday_person_emails)) + + reminder_text, message = get_birthday_reminder_text_and_message(birthday_persons) + send_birthday_reminder(recipients, reminder_text, birthday_persons, message) + + if len(birthday_persons) > 1: + # special email for people sharing birthdays + for person in birthday_persons: + person_email = person["user_id"] or person["personal_email"] or person["company_email"] + others = [d for d in birthday_persons if d != person] + reminder_text, message = get_birthday_reminder_text_and_message(others) + send_birthday_reminder(person_email, reminder_text, others, message) + +def get_birthday_reminder_text_and_message(birthday_persons): + if len(birthday_persons) == 1: + birthday_person_text = birthday_persons[0]['name'] + else: + # converts ["Jim", "Rim", "Dim"] to Jim, Rim & Dim + person_names = [d['name'] for d in birthday_persons] + birthday_person_text = comma_sep(person_names, frappe._("{0} & {1}"), False) + + reminder_text = _("Today is {0}'s birthday 🎉").format(birthday_person_text) + message = _("A friendly reminder of an important date for our team.") + message += "
" + message += _("Everyone, let’s congratulate {0} on their birthday.").format(birthday_person_text) + + return reminder_text, message + +def send_birthday_reminder(recipients, reminder_text, birthday_persons, message): + frappe.sendmail( + recipients=recipients, + subject=_("Birthday Reminder"), + template="birthday_reminder", + args=dict( + reminder_text=reminder_text, + birthday_persons=birthday_persons, + message=message, + ), + header=_("Birthday Reminder 🎂") + ) + +def get_employees_who_are_born_today(): + """Get all employee born today & group them based on their company""" + return get_employees_having_an_event_today("birthday") + +def get_employees_having_an_event_today(event_type): + """Get all employee who have `event_type` today + & group them based on their company. `event_type` + can be `birthday` or `work_anniversary`""" + + from collections import defaultdict + + # Set column based on event type + if event_type == 'birthday': + condition_column = 'date_of_birth' + elif event_type == 'work_anniversary': + condition_column = 'date_of_joining' + else: + return + + employees_born_today = frappe.db.multisql({ + "mariadb": f""" + SELECT `personal_email`, `company`, `company_email`, `user_id`, `employee_name` AS 'name', `image`, `date_of_joining` + FROM `tabEmployee` + WHERE + DAY({condition_column}) = DAY(%(today)s) + AND + MONTH({condition_column}) = MONTH(%(today)s) + AND + `status` = 'Active' + """, + "postgres": f""" + SELECT "personal_email", "company", "company_email", "user_id", "employee_name" AS 'name', "image" + FROM "tabEmployee" + WHERE + DATE_PART('day', {condition_column}) = date_part('day', %(today)s) + AND + DATE_PART('month', {condition_column}) = date_part('month', %(today)s) + AND + "status" = 'Active' + """, + }, dict(today=today(), condition_column=condition_column), as_dict=1) + + grouped_employees = defaultdict(lambda: []) + + for employee_doc in employees_born_today: + grouped_employees[employee_doc.get('company')].append(employee_doc) + + return grouped_employees + + +# -------------------------- +# WORK ANNIVERSARY REMINDERS +# -------------------------- +def send_work_anniversary_reminders(): + """Send Employee Work Anniversary Reminders if 'Send Work Anniversary Reminders' is checked""" + to_send = int(frappe.db.get_single_value("HR Settings", "send_work_anniversary_reminders") or 1) + if not to_send: + return + + employees_joined_today = get_employees_having_an_event_today("work_anniversary") + + for company, anniversary_persons in employees_joined_today.items(): + employee_emails = get_all_employee_emails(company) + anniversary_person_emails = [get_employee_email(doc) for doc in anniversary_persons] + recipients = list(set(employee_emails) - set(anniversary_person_emails)) + + reminder_text, message = get_work_anniversary_reminder_text_and_message(anniversary_persons) + send_work_anniversary_reminder(recipients, reminder_text, anniversary_persons, message) + + if len(anniversary_persons) > 1: + # email for people sharing work anniversaries + for person in anniversary_persons: + person_email = person["user_id"] or person["personal_email"] or person["company_email"] + others = [d for d in anniversary_persons if d != person] + reminder_text, message = get_work_anniversary_reminder_text_and_message(others) + send_work_anniversary_reminder(person_email, reminder_text, others, message) + +def get_work_anniversary_reminder_text_and_message(anniversary_persons): + if len(anniversary_persons) == 1: + anniversary_person = anniversary_persons[0]['name'] + persons_name = anniversary_person + # Number of years completed at the company + completed_years = getdate().year - anniversary_persons[0]['date_of_joining'].year + anniversary_person += f" completed {completed_years} years" + else: + person_names_with_years = [] + names = [] + for person in anniversary_persons: + person_text = person['name'] + names.append(person_text) + # Number of years completed at the company + completed_years = getdate().year - person['date_of_joining'].year + person_text += f" completed {completed_years} years" + person_names_with_years.append(person_text) + + # converts ["Jim", "Rim", "Dim"] to Jim, Rim & Dim + anniversary_person = comma_sep(person_names_with_years, frappe._("{0} & {1}"), False) + persons_name = comma_sep(names, frappe._("{0} & {1}"), False) + + reminder_text = _("Today {0} at our Company! 🎉").format(anniversary_person) + message = _("A friendly reminder of an important date for our team.") + message += "
" + message += _("Everyone, let’s congratulate {0} on their work anniversary!").format(persons_name) + + return reminder_text, message + +def send_work_anniversary_reminder(recipients, reminder_text, anniversary_persons, message): + frappe.sendmail( + recipients=recipients, + subject=_("Work Anniversary Reminder"), + template="anniversary_reminder", + args=dict( + reminder_text=reminder_text, + anniversary_persons=anniversary_persons, + message=message, + ), + header=_("🎊️🎊️ Work Anniversary Reminder 🎊️🎊️") + ) diff --git a/erpnext/hr/doctype/employee/employee_tree.js b/erpnext/hr/doctype/employee/employee_tree.js index 9ab091a1eb6..7d6a70013d4 100644 --- a/erpnext/hr/doctype/employee/employee_tree.js +++ b/erpnext/hr/doctype/employee/employee_tree.js @@ -33,4 +33,4 @@ frappe.treeview_settings['Employee'] = { condition: 'frappe.boot.user.can_create.indexOf("Employee") !== -1' } ], -}; \ No newline at end of file +}; diff --git a/erpnext/hr/doctype/employee/test_employee.js b/erpnext/hr/doctype/employee/test_employee.js index 200dcd79666..3a414584804 100644 --- a/erpnext/hr/doctype/employee/test_employee.js +++ b/erpnext/hr/doctype/employee/test_employee.js @@ -37,4 +37,4 @@ QUnit.test("Test: Employee [HR]", function (assert) { () => frappe.timeout(10), () => done() ]); -}); \ No newline at end of file +}); diff --git a/erpnext/hr/doctype/employee/test_employee.py b/erpnext/hr/doctype/employee/test_employee.py index 7d652a7366a..5feb6de8f2b 100644 --- a/erpnext/hr/doctype/employee/test_employee.py +++ b/erpnext/hr/doctype/employee/test_employee.py @@ -1,40 +1,15 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt -from __future__ import unicode_literals - import frappe import erpnext import unittest import frappe.utils -from erpnext.hr.doctype.employee.employee import EmployeeLeftValidationError +from erpnext.hr.doctype.employee.employee import InactiveEmployeeStatusError test_records = frappe.get_test_records('Employee') class TestEmployee(unittest.TestCase): - def test_birthday_reminders(self): - employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0]) - employee.date_of_birth = "1992" + frappe.utils.nowdate()[4:] - employee.company_email = "test@example.com" - employee.company = "_Test Company" - employee.save() - - from erpnext.hr.doctype.employee.employee import get_employees_who_are_born_today, send_birthday_reminders - - employees_born_today = get_employees_who_are_born_today() - self.assertTrue(employees_born_today.get("_Test Company")) - - frappe.db.sql("delete from `tabEmail Queue`") - - hr_settings = frappe.get_doc("HR Settings", "HR Settings") - hr_settings.stop_birthday_reminders = 0 - hr_settings.save() - - send_birthday_reminders() - - email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True) - self.assertTrue("Subject: Birthday Reminder" in email_queue[0].message) - def test_employee_status_left(self): employee1 = make_employee("test_employee_1@company.com") employee2 = make_employee("test_employee_2@company.com") @@ -45,10 +20,33 @@ class TestEmployee(unittest.TestCase): employee2_doc.save() employee1_doc.reload() employee1_doc.status = 'Left' - self.assertRaises(EmployeeLeftValidationError, employee1_doc.save) + self.assertRaises(InactiveEmployeeStatusError, employee1_doc.save) + + def test_employee_status_inactive(self): + from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure + from erpnext.payroll.doctype.salary_structure.salary_structure import make_salary_slip + from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list + + employee = make_employee("test_employee_status@company.com") + employee_doc = frappe.get_doc("Employee", employee) + employee_doc.status = "Inactive" + employee_doc.save() + employee_doc.reload() + + make_holiday_list() + frappe.db.set_value("Company", erpnext.get_default_company(), "default_holiday_list", "Salary Slip Test Holiday List") + + frappe.db.sql("""delete from `tabSalary Structure` where name='Test Inactive Employee Salary Slip'""") + salary_structure = make_salary_structure("Test Inactive Employee Salary Slip", "Monthly", + employee=employee_doc.name, company=employee_doc.company) + salary_slip = make_salary_slip(salary_structure.name, employee=employee_doc.name) + + self.assertRaises(InactiveEmployeeStatusError, salary_slip.save) + + def tearDown(self): + frappe.db.rollback() def make_employee(user, company=None, **kwargs): - "" if not frappe.db.get_value("User", user): frappe.get_doc({ "doctype": "User", @@ -80,4 +78,5 @@ def make_employee(user, company=None, **kwargs): employee.insert() return employee.name else: + frappe.db.set_value("Employee", {"employee_name":user}, "status", "Active") return frappe.get_value("Employee", {"employee_name":user}, "name") diff --git a/erpnext/hr/doctype/employee/test_employee_reminders.py b/erpnext/hr/doctype/employee/test_employee_reminders.py new file mode 100644 index 00000000000..7e560f512d1 --- /dev/null +++ b/erpnext/hr/doctype/employee/test_employee_reminders.py @@ -0,0 +1,173 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + +import frappe +import unittest + +from frappe.utils import getdate +from datetime import timedelta +from erpnext.hr.doctype.employee.test_employee import make_employee +from erpnext.hr.doctype.hr_settings.hr_settings import set_proceed_with_frequency_change + + +class TestEmployeeReminders(unittest.TestCase): + @classmethod + def setUpClass(cls): + from erpnext.hr.doctype.holiday_list.test_holiday_list import make_holiday_list + + # Create a test holiday list + test_holiday_dates = cls.get_test_holiday_dates() + test_holiday_list = make_holiday_list( + 'TestHolidayRemindersList', + holiday_dates=[ + {'holiday_date': test_holiday_dates[0], 'description': 'test holiday1'}, + {'holiday_date': test_holiday_dates[1], 'description': 'test holiday2'}, + {'holiday_date': test_holiday_dates[2], 'description': 'test holiday3', 'weekly_off': 1}, + {'holiday_date': test_holiday_dates[3], 'description': 'test holiday4'}, + {'holiday_date': test_holiday_dates[4], 'description': 'test holiday5'}, + {'holiday_date': test_holiday_dates[5], 'description': 'test holiday6'}, + ], + from_date=getdate()-timedelta(days=10), + to_date=getdate()+timedelta(weeks=5) + ) + + # Create a test employee + test_employee = frappe.get_doc( + 'Employee', + make_employee('test@gopher.io', company="_Test Company") + ) + + # Attach the holiday list to employee + test_employee.holiday_list = test_holiday_list.name + test_employee.save() + + # Attach to class + cls.test_employee = test_employee + cls.test_holiday_dates = test_holiday_dates + + @classmethod + def get_test_holiday_dates(cls): + today_date = getdate() + return [ + today_date, + today_date-timedelta(days=4), + today_date-timedelta(days=3), + today_date+timedelta(days=1), + today_date+timedelta(days=3), + today_date+timedelta(weeks=3) + ] + + def setUp(self): + # Clear Email Queue + frappe.db.sql("delete from `tabEmail Queue`") + + def test_is_holiday(self): + from erpnext.hr.doctype.employee.employee import is_holiday + + self.assertTrue(is_holiday(self.test_employee.name)) + self.assertTrue(is_holiday(self.test_employee.name, date=self.test_holiday_dates[1])) + self.assertFalse(is_holiday(self.test_employee.name, date=getdate()-timedelta(days=1))) + + # Test weekly_off holidays + self.assertTrue(is_holiday(self.test_employee.name, date=self.test_holiday_dates[2])) + self.assertFalse(is_holiday(self.test_employee.name, date=self.test_holiday_dates[2], only_non_weekly=True)) + + # Test with descriptions + has_holiday, descriptions = is_holiday(self.test_employee.name, with_description=True) + self.assertTrue(has_holiday) + self.assertTrue('test holiday1' in descriptions) + + def test_birthday_reminders(self): + employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0]) + employee.date_of_birth = "1992" + frappe.utils.nowdate()[4:] + employee.company_email = "test@example.com" + employee.company = "_Test Company" + employee.save() + + from erpnext.hr.doctype.employee.employee_reminders import get_employees_who_are_born_today, send_birthday_reminders + + employees_born_today = get_employees_who_are_born_today() + self.assertTrue(employees_born_today.get("_Test Company")) + + hr_settings = frappe.get_doc("HR Settings", "HR Settings") + hr_settings.send_birthday_reminders = 1 + hr_settings.save() + + send_birthday_reminders() + + email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True) + self.assertTrue("Subject: Birthday Reminder" in email_queue[0].message) + + def test_work_anniversary_reminders(self): + employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0]) + employee.date_of_joining = "1998" + frappe.utils.nowdate()[4:] + employee.company_email = "test@example.com" + employee.company = "_Test Company" + employee.save() + + from erpnext.hr.doctype.employee.employee_reminders import get_employees_having_an_event_today, send_work_anniversary_reminders + + employees_having_work_anniversary = get_employees_having_an_event_today('work_anniversary') + self.assertTrue(employees_having_work_anniversary.get("_Test Company")) + + hr_settings = frappe.get_doc("HR Settings", "HR Settings") + hr_settings.send_work_anniversary_reminders = 1 + hr_settings.save() + + send_work_anniversary_reminders() + + email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True) + self.assertTrue("Subject: Work Anniversary Reminder" in email_queue[0].message) + + def test_send_holidays_reminder_in_advance(self): + from erpnext.hr.utils import get_holidays_for_employee + from erpnext.hr.doctype.employee.employee_reminders import send_holidays_reminder_in_advance + + # Get HR settings and enable advance holiday reminders + hr_settings = frappe.get_doc("HR Settings", "HR Settings") + hr_settings.send_holiday_reminders = 1 + set_proceed_with_frequency_change() + hr_settings.frequency = 'Weekly' + hr_settings.save() + + holidays = get_holidays_for_employee( + self.test_employee.get('name'), + getdate(), getdate() + timedelta(days=3), + only_non_weekly=True, + raise_exception=False + ) + + send_holidays_reminder_in_advance( + self.test_employee.get('name'), + holidays + ) + + email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True) + self.assertEqual(len(email_queue), 1) + + def test_advance_holiday_reminders_monthly(self): + from erpnext.hr.doctype.employee.employee_reminders import send_reminders_in_advance_monthly + # Get HR settings and enable advance holiday reminders + hr_settings = frappe.get_doc("HR Settings", "HR Settings") + hr_settings.send_holiday_reminders = 1 + set_proceed_with_frequency_change() + hr_settings.frequency = 'Monthly' + hr_settings.save() + + send_reminders_in_advance_monthly() + + email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True) + self.assertTrue(len(email_queue) > 0) + + def test_advance_holiday_reminders_weekly(self): + from erpnext.hr.doctype.employee.employee_reminders import send_reminders_in_advance_weekly + # Get HR settings and enable advance holiday reminders + hr_settings = frappe.get_doc("HR Settings", "HR Settings") + hr_settings.send_holiday_reminders = 1 + hr_settings.frequency = 'Weekly' + hr_settings.save() + + send_reminders_in_advance_weekly() + + email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True) + self.assertTrue(len(email_queue) > 0) diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.py b/erpnext/hr/doctype/employee_advance/employee_advance.py index cb72f6b6d96..cbb3cc813b4 100644 --- a/erpnext/hr/doctype/employee_advance/employee_advance.py +++ b/erpnext/hr/doctype/employee_advance/employee_advance.py @@ -8,6 +8,7 @@ from frappe import _ from frappe.model.document import Document from frappe.utils import flt, nowdate from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_bank_cash_account +from erpnext.hr.utils import validate_active_employee class EmployeeAdvanceOverPayment(frappe.ValidationError): pass @@ -18,11 +19,11 @@ class EmployeeAdvance(Document): 'make_payment_via_journal_entry') def validate(self): + validate_active_employee(self.employee) self.set_status() def on_cancel(self): self.ignore_linked_doctypes = ('GL Entry') - self.set_status() def set_status(self): if self.docstatus == 0: @@ -183,9 +184,9 @@ def make_return_entry(employee, company, employee_advance_name, return_amount, bank_cash_account = get_default_bank_cash_account(company, account_type='Cash', mode_of_payment = mode_of_payment) if not bank_cash_account: frappe.throw(_("Please set a Default Cash Account in Company defaults")) - + advance_account_currency = frappe.db.get_value('Account', advance_account, 'account_currency') - + je = frappe.new_doc('Journal Entry') je.posting_date = nowdate() je.voucher_type = get_voucher_type(mode_of_payment) @@ -229,4 +230,4 @@ def get_voucher_type(mode_of_payment=None): if mode_of_payment_type == "Bank": voucher_type = "Bank Entry" - return voucher_type \ No newline at end of file + return voucher_type diff --git a/erpnext/hr/doctype/employee_advance/test_employee_advance.js b/erpnext/hr/doctype/employee_advance/test_employee_advance.js deleted file mode 100644 index 1b9ec6f6d0c..00000000000 --- a/erpnext/hr/doctype/employee_advance/test_employee_advance.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: Employee Advance", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Employee Advance - () => frappe.tests.make('Employee Advance', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/hr/doctype/employee_advance/test_employee_advance.py b/erpnext/hr/doctype/employee_advance/test_employee_advance.py index c88b2b8e49e..100968bb7aa 100644 --- a/erpnext/hr/doctype/employee_advance/test_employee_advance.py +++ b/erpnext/hr/doctype/employee_advance/test_employee_advance.py @@ -48,4 +48,4 @@ def make_employee_advance(employee_name): doc.insert() doc.submit() - return doc \ No newline at end of file + return doc diff --git a/erpnext/hr/doctype/employee_attendance_tool/employee_attendance_tool.css b/erpnext/hr/doctype/employee_attendance_tool/employee_attendance_tool.css index d25fb2247ed..c8d6644b2f8 100644 --- a/erpnext/hr/doctype/employee_attendance_tool/employee_attendance_tool.css +++ b/erpnext/hr/doctype/employee_attendance_tool/employee_attendance_tool.css @@ -18,4 +18,4 @@ .checkbox{ margin-top: -3px; -} \ No newline at end of file +} diff --git a/erpnext/hr/doctype/employee_attendance_tool/employee_attendance_tool.js b/erpnext/hr/doctype/employee_attendance_tool/employee_attendance_tool.js index 3205a92b1b6..2de5eb0d64e 100644 --- a/erpnext/hr/doctype/employee_attendance_tool/employee_attendance_tool.js +++ b/erpnext/hr/doctype/employee_attendance_tool/employee_attendance_tool.js @@ -267,5 +267,3 @@ erpnext.EmployeeSelector = Class.extend({ mark_employee_toolbar.appendTo($(this.wrapper)); } }); - - diff --git a/erpnext/hr/doctype/employee_attendance_tool/test_employee_attendance_tool.js b/erpnext/hr/doctype/employee_attendance_tool/test_employee_attendance_tool.js index 2827d4ba289..48d4344df22 100644 --- a/erpnext/hr/doctype/employee_attendance_tool/test_employee_attendance_tool.js +++ b/erpnext/hr/doctype/employee_attendance_tool/test_employee_attendance_tool.js @@ -58,4 +58,4 @@ QUnit.test("Test: Employee attendance tool [HR]", function (assert) { }, () => done() ]); -}); \ No newline at end of file +}); diff --git a/erpnext/hr/doctype/employee_checkin/employee_checkin.py b/erpnext/hr/doctype/employee_checkin/employee_checkin.py index 15fbd4e0153..6c0cd4f963b 100644 --- a/erpnext/hr/doctype/employee_checkin/employee_checkin.py +++ b/erpnext/hr/doctype/employee_checkin/employee_checkin.py @@ -9,9 +9,11 @@ from frappe.model.document import Document from frappe import _ from erpnext.hr.doctype.shift_assignment.shift_assignment import get_actual_start_end_datetime_of_shift +from erpnext.hr.utils import validate_active_employee class EmployeeCheckin(Document): def validate(self): + validate_active_employee(self.employee) self.validate_duplicate_log() self.fetch_shift() @@ -122,7 +124,7 @@ def mark_attendance_and_link_log(logs, attendance_status, attendance_date, worki def calculate_working_hours(logs, check_in_out_type, working_hours_calc_type): """Given a set of logs in chronological order calculates the total working hours based on the parameters. Zero is returned for all invalid cases. - + :param logs: The List of 'Employee Checkin'. :param check_in_out_type: One of: 'Alternating entries as IN and OUT during the same shift', 'Strictly based on Log Type in Employee Checkin' :param working_hours_calc_type: One of: 'First Check-in and Last Check-out', 'Every Valid Check-in and Check-out' @@ -174,4 +176,3 @@ def time_diff_in_hours(start, end): def find_index_in_dict(dict_list, key, value): return next((index for (index, d) in enumerate(dict_list) if d[key] == value), None) - diff --git a/erpnext/hr/doctype/employee_checkin/test_employee_checkin.py b/erpnext/hr/doctype/employee_checkin/test_employee_checkin.py index 9f12ef24e62..7ba511f08d5 100644 --- a/erpnext/hr/doctype/employee_checkin/test_employee_checkin.py +++ b/erpnext/hr/doctype/employee_checkin/test_employee_checkin.py @@ -42,11 +42,11 @@ class TestEmployeeCheckin(unittest.TestCase): self.assertEqual(logs_count, 4) attendance_count = frappe.db.count('Attendance', {'status':'Present', 'working_hours':8.2, 'employee':employee, 'attendance_date':now_date}) - self.assertEqual(attendance_count, 1) + self.assertEqual(attendance_count, 1) def test_calculate_working_hours(self): check_in_out_type = ['Alternating entries as IN and OUT during the same shift', - 'Strictly based on Log Type in Employee Checkin'] + 'Strictly based on Log Type in Employee Checkin'] working_hours_calc_type = ['First Check-in and Last Check-out', 'Every Valid Check-in and Check-out'] logs_type_1 = [ diff --git a/erpnext/hr/doctype/employee_education/employee_education.py b/erpnext/hr/doctype/employee_education/employee_education.py index a1d449291c9..f0a76172b2c 100644 --- a/erpnext/hr/doctype/employee_education/employee_education.py +++ b/erpnext/hr/doctype/employee_education/employee_education.py @@ -7,4 +7,4 @@ import frappe from frappe.model.document import Document class EmployeeEducation(Document): - pass \ No newline at end of file + pass diff --git a/erpnext/hr/doctype/employee_external_work_history/employee_external_work_history.py b/erpnext/hr/doctype/employee_external_work_history/employee_external_work_history.py index c7166309f37..517ef57be85 100644 --- a/erpnext/hr/doctype/employee_external_work_history/employee_external_work_history.py +++ b/erpnext/hr/doctype/employee_external_work_history/employee_external_work_history.py @@ -7,4 +7,4 @@ import frappe from frappe.model.document import Document class EmployeeExternalWorkHistory(Document): - pass \ No newline at end of file + pass diff --git a/erpnext/hr/doctype/employee_grade/employee_grade_dashboard.py b/erpnext/hr/doctype/employee_grade/employee_grade_dashboard.py index f2656e9a2b2..df679104185 100644 --- a/erpnext/hr/doctype/employee_grade/employee_grade_dashboard.py +++ b/erpnext/hr/doctype/employee_grade/employee_grade_dashboard.py @@ -10,4 +10,4 @@ def get_data(): 'items': ['Employee Onboarding Template', 'Employee Separation Template'] } ] - } \ No newline at end of file + } diff --git a/erpnext/hr/doctype/employee_grade/test_employee_grade.js b/erpnext/hr/doctype/employee_grade/test_employee_grade.js deleted file mode 100644 index d684fb2ad1b..00000000000 --- a/erpnext/hr/doctype/employee_grade/test_employee_grade.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: Employee Grade", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Employee Grade - () => frappe.tests.make('Employee Grade', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/hr/doctype/employee_grievance/employee_grievance.py b/erpnext/hr/doctype/employee_grievance/employee_grievance.py index 503b5ea4449..17055829efb 100644 --- a/erpnext/hr/doctype/employee_grievance/employee_grievance.py +++ b/erpnext/hr/doctype/employee_grievance/employee_grievance.py @@ -12,4 +12,3 @@ class EmployeeGrievance(Document): bold("Invalid"), bold("Resolved")) ) - diff --git a/erpnext/hr/doctype/employee_grievance/employee_grievance_list.js b/erpnext/hr/doctype/employee_grievance/employee_grievance_list.js index fc08e216099..11672ca4e0e 100644 --- a/erpnext/hr/doctype/employee_grievance/employee_grievance_list.js +++ b/erpnext/hr/doctype/employee_grievance/employee_grievance_list.js @@ -9,4 +9,4 @@ frappe.listview_settings["Employee Grievance"] = { }; return [__(doc.status), colors[doc.status], "status,=," + doc.status]; } -}; \ No newline at end of file +}; diff --git a/erpnext/hr/doctype/employee_grievance/test_employee_grievance.py b/erpnext/hr/doctype/employee_grievance/test_employee_grievance.py index a615b20a5a2..ed897ee1032 100644 --- a/erpnext/hr/doctype/employee_grievance/test_employee_grievance.py +++ b/erpnext/hr/doctype/employee_grievance/test_employee_grievance.py @@ -48,4 +48,3 @@ def create_grievance_type(): grievance_type.save() return grievance_type.name - diff --git a/erpnext/hr/doctype/employee_group/test_employee_group.py b/erpnext/hr/doctype/employee_group/test_employee_group.py index 3a6bf8594b3..26a61c407b2 100644 --- a/erpnext/hr/doctype/employee_group/test_employee_group.py +++ b/erpnext/hr/doctype/employee_group/test_employee_group.py @@ -29,4 +29,4 @@ def make_employee_group(): def get_employee_group(): employee_group = frappe.db.exists("Employee Group", "_Test Employee Group") - return employee_group \ No newline at end of file + return employee_group diff --git a/erpnext/hr/doctype/employee_health_insurance/test_employee_health_insurance.js b/erpnext/hr/doctype/employee_health_insurance/test_employee_health_insurance.js deleted file mode 100644 index 245cb32971f..00000000000 --- a/erpnext/hr/doctype/employee_health_insurance/test_employee_health_insurance.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: Employee Health Insurance", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Employee Health Insurance - () => frappe.tests.make('Employee Health Insurance', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/hr/doctype/employee_internal_work_history/employee_internal_work_history.py b/erpnext/hr/doctype/employee_internal_work_history/employee_internal_work_history.py index d0f3d8d016a..2f385a8113e 100644 --- a/erpnext/hr/doctype/employee_internal_work_history/employee_internal_work_history.py +++ b/erpnext/hr/doctype/employee_internal_work_history/employee_internal_work_history.py @@ -7,4 +7,4 @@ import frappe from frappe.model.document import Document class EmployeeInternalWorkHistory(Document): - pass \ No newline at end of file + pass diff --git a/erpnext/hr/doctype/employee_onboarding/employee_onboarding.py b/erpnext/hr/doctype/employee_onboarding/employee_onboarding.py index 6cc2bf5cd85..0cb50475bf8 100644 --- a/erpnext/hr/doctype/employee_onboarding/employee_onboarding.py +++ b/erpnext/hr/doctype/employee_onboarding/employee_onboarding.py @@ -57,4 +57,3 @@ def make_employee(source_name, target_doc=None): }} }, target_doc, set_missing_values) return doc - diff --git a/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.js b/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.js deleted file mode 100644 index d15cef77dca..00000000000 --- a/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.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: Employee Onboarding", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Employee Onboarding - () => frappe.tests.make('Employee Onboarding', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/hr/doctype/employee_onboarding_template/employee_onboarding_template_dashboard.py b/erpnext/hr/doctype/employee_onboarding_template/employee_onboarding_template_dashboard.py index 837da530162..ab0eb2f5dce 100644 --- a/erpnext/hr/doctype/employee_onboarding_template/employee_onboarding_template_dashboard.py +++ b/erpnext/hr/doctype/employee_onboarding_template/employee_onboarding_template_dashboard.py @@ -9,4 +9,4 @@ def get_data(): 'items': ['Employee Onboarding'] }, ], - } \ No newline at end of file + } diff --git a/erpnext/hr/doctype/employee_onboarding_template/test_employee_onboarding_template.js b/erpnext/hr/doctype/employee_onboarding_template/test_employee_onboarding_template.js deleted file mode 100644 index 10912edb6ac..00000000000 --- a/erpnext/hr/doctype/employee_onboarding_template/test_employee_onboarding_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: Employee Onboarding Template", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Employee Onboarding Template - () => frappe.tests.make('Employee Onboarding Template', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/hr/doctype/employee_promotion/employee_promotion.py b/erpnext/hr/doctype/employee_promotion/employee_promotion.py index 83fb235f92c..a3a61834c8c 100644 --- a/erpnext/hr/doctype/employee_promotion/employee_promotion.py +++ b/erpnext/hr/doctype/employee_promotion/employee_promotion.py @@ -7,12 +7,11 @@ import frappe from frappe import _ from frappe.model.document import Document from frappe.utils import getdate -from erpnext.hr.utils import update_employee +from erpnext.hr.utils import update_employee, validate_active_employee class EmployeePromotion(Document): def validate(self): - if frappe.get_value("Employee", self.employee, "status") != "Active": - frappe.throw(_("Cannot promote Employee with status Left or Inactive")) + validate_active_employee(self.employee) def before_submit(self): if getdate(self.promotion_date) > getdate(): diff --git a/erpnext/hr/doctype/employee_promotion/test_employee_promotion.js b/erpnext/hr/doctype/employee_promotion/test_employee_promotion.js deleted file mode 100644 index 5f0a5baf818..00000000000 --- a/erpnext/hr/doctype/employee_promotion/test_employee_promotion.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: Employee Promotion", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Employee Promotion - () => frappe.tests.make('Employee Promotion', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/hr/doctype/employee_referral/employee_referral.py b/erpnext/hr/doctype/employee_referral/employee_referral.py index 45d68729ce6..547a95e3bdf 100644 --- a/erpnext/hr/doctype/employee_referral/employee_referral.py +++ b/erpnext/hr/doctype/employee_referral/employee_referral.py @@ -7,9 +7,11 @@ import frappe from frappe import _ from frappe.utils import get_link_to_form from frappe.model.document import Document +from erpnext.hr.utils import validate_active_employee class EmployeeReferral(Document): def validate(self): + validate_active_employee(self.referrer) self.set_full_name() self.set_referral_bonus_payment_status() @@ -68,4 +70,3 @@ def create_additional_salary(doc): additional_salary.ref_docname = doc.name return additional_salary - diff --git a/erpnext/hr/doctype/employee_referral/employee_referral_dashboard.py b/erpnext/hr/doctype/employee_referral/employee_referral_dashboard.py index afa2a1ff1fc..caca2961a1a 100644 --- a/erpnext/hr/doctype/employee_referral/employee_referral_dashboard.py +++ b/erpnext/hr/doctype/employee_referral/employee_referral_dashboard.py @@ -12,4 +12,4 @@ def get_data(): }, ] - } \ No newline at end of file + } diff --git a/erpnext/hr/doctype/employee_referral/employee_referral_list.js b/erpnext/hr/doctype/employee_referral/employee_referral_list.js index 7533ab635f5..38dfc4d4c86 100644 --- a/erpnext/hr/doctype/employee_referral/employee_referral_list.js +++ b/erpnext/hr/doctype/employee_referral/employee_referral_list.js @@ -11,4 +11,4 @@ frappe.listview_settings['Employee Referral'] = { return [__(doc.status), "red", "status,=," + doc.status]; } }, -}; \ No newline at end of file +}; diff --git a/erpnext/hr/doctype/employee_referral/test_employee_referral.py b/erpnext/hr/doctype/employee_referral/test_employee_referral.py index a674f390265..599f3262240 100644 --- a/erpnext/hr/doctype/employee_referral/test_employee_referral.py +++ b/erpnext/hr/doctype/employee_referral/test_employee_referral.py @@ -57,4 +57,4 @@ def create_employee_referral(): emp_ref.save() emp_ref.submit() - return emp_ref \ No newline at end of file + return emp_ref diff --git a/erpnext/hr/doctype/employee_separation/test_employee_separation.js b/erpnext/hr/doctype/employee_separation/test_employee_separation.js deleted file mode 100644 index d6c635951f7..00000000000 --- a/erpnext/hr/doctype/employee_separation/test_employee_separation.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: Employee Separation", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Employee Separation - () => frappe.tests.make('Employee Separation', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/hr/doctype/employee_separation/test_employee_separation.py b/erpnext/hr/doctype/employee_separation/test_employee_separation.py index 713fcf526b5..0b72efa1378 100644 --- a/erpnext/hr/doctype/employee_separation/test_employee_separation.py +++ b/erpnext/hr/doctype/employee_separation/test_employee_separation.py @@ -23,4 +23,4 @@ class TestEmployeeSeparation(unittest.TestCase): separation.submit() self.assertEqual(separation.docstatus, 1) separation.cancel() - self.assertEqual(separation.project, "") \ No newline at end of file + self.assertEqual(separation.project, "") diff --git a/erpnext/hr/doctype/employee_separation_template/employee_separation_template_dashboard.py b/erpnext/hr/doctype/employee_separation_template/employee_separation_template_dashboard.py index 39345f07663..75f985cec39 100644 --- a/erpnext/hr/doctype/employee_separation_template/employee_separation_template_dashboard.py +++ b/erpnext/hr/doctype/employee_separation_template/employee_separation_template_dashboard.py @@ -9,4 +9,4 @@ def get_data(): 'items': ['Employee Separation'] }, ], - } \ No newline at end of file + } diff --git a/erpnext/hr/doctype/employee_separation_template/test_employee_separation_template.js b/erpnext/hr/doctype/employee_separation_template/test_employee_separation_template.js deleted file mode 100644 index 66fd4508046..00000000000 --- a/erpnext/hr/doctype/employee_separation_template/test_employee_separation_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: Employee Separation Template", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Employee Separation Template - () => frappe.tests.make('Employee Separation Template', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/hr/doctype/employee_transfer/employee_transfer.py b/erpnext/hr/doctype/employee_transfer/employee_transfer.py index 6eec9fa12a9..c2007747fb3 100644 --- a/erpnext/hr/doctype/employee_transfer/employee_transfer.py +++ b/erpnext/hr/doctype/employee_transfer/employee_transfer.py @@ -10,10 +10,6 @@ from frappe.utils import getdate from erpnext.hr.utils import update_employee class EmployeeTransfer(Document): - def validate(self): - if frappe.get_value("Employee", self.employee, "status") != "Active": - frappe.throw(_("Cannot transfer Employee with status Left or Inactive")) - def before_submit(self): if getdate(self.transfer_date) > getdate(): frappe.throw(_("Employee Transfer cannot be submitted before Transfer Date"), diff --git a/erpnext/hr/doctype/employee_transfer/test_employee_transfer.js b/erpnext/hr/doctype/employee_transfer/test_employee_transfer.js deleted file mode 100644 index 05a3e1a5738..00000000000 --- a/erpnext/hr/doctype/employee_transfer/test_employee_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: Employee Transfer", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Employee Transfer - () => frappe.tests.make('Employee Transfer', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/hr/doctype/employee_transfer_property/test_employee_transfer_property.js b/erpnext/hr/doctype/employee_transfer_property/test_employee_transfer_property.js deleted file mode 100644 index 00a334a63d7..00000000000 --- a/erpnext/hr/doctype/employee_transfer_property/test_employee_transfer_property.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: Employee Transfer Property", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Employee Transfer Property - () => frappe.tests.make('Employee Transfer Property', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/hr/doctype/employment_type/employment_type.py b/erpnext/hr/doctype/employment_type/employment_type.py index fb306b65d28..00aa6bb9bc4 100644 --- a/erpnext/hr/doctype/employment_type/employment_type.py +++ b/erpnext/hr/doctype/employment_type/employment_type.py @@ -7,4 +7,4 @@ import frappe from frappe.model.document import Document class EmploymentType(Document): - pass \ No newline at end of file + pass diff --git a/erpnext/hr/doctype/employment_type/test_employment_type.js b/erpnext/hr/doctype/employment_type/test_employment_type.js index 9835aabd481..fd7c6a1ce33 100644 --- a/erpnext/hr/doctype/employment_type/test_employment_type.js +++ b/erpnext/hr/doctype/employment_type/test_employment_type.js @@ -19,4 +19,4 @@ QUnit.test("Test: Employment type [HR]", function (assert) { 'name of employment type correctly saved'), () => done() ]); -}); \ No newline at end of file +}); diff --git a/erpnext/hr/doctype/employment_type/test_employment_type.py b/erpnext/hr/doctype/employment_type/test_employment_type.py index e138136605c..0297ffa01a3 100644 --- a/erpnext/hr/doctype/employment_type/test_employment_type.py +++ b/erpnext/hr/doctype/employment_type/test_employment_type.py @@ -4,4 +4,4 @@ from __future__ import unicode_literals import frappe -test_records = frappe.get_test_records('Employment Type') \ No newline at end of file +test_records = frappe.get_test_records('Employment Type') diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.js b/erpnext/hr/doctype/expense_claim/expense_claim.js index 629341ff2a5..3c4c672816c 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.js +++ b/erpnext/hr/doctype/expense_claim/expense_claim.js @@ -442,4 +442,4 @@ frappe.ui.form.on("Expense Taxes and Charges", { tax_amount: function(frm, cdt, cdn) { frm.trigger("calculate_total_tax", cdt, cdn); } -}); \ No newline at end of file +}); diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.py b/erpnext/hr/doctype/expense_claim/expense_claim.py index 5010fc3f75c..4dc089cb1d2 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.py +++ b/erpnext/hr/doctype/expense_claim/expense_claim.py @@ -6,7 +6,7 @@ import frappe, erpnext from frappe import _ from frappe.utils import get_fullname, flt, cstr, get_link_to_form from frappe.model.document import Document -from erpnext.hr.utils import set_employee_name, share_doc_with_approver +from erpnext.hr.utils import set_employee_name, share_doc_with_approver, validate_active_employee from erpnext.accounts.party import get_party_account from erpnext.accounts.general_ledger import make_gl_entries from erpnext.accounts.doctype.sales_invoice.sales_invoice import get_bank_cash_account @@ -23,6 +23,7 @@ class ExpenseClaim(AccountsController): 'make_payment_via_journal_entry') def validate(self): + validate_active_employee(self.employee) self.validate_advances() self.validate_sanctioned_amount() self.calculate_total_amount() @@ -35,8 +36,8 @@ class ExpenseClaim(AccountsController): if self.task and not self.project: self.project = frappe.db.get_value("Task", self.task, "project") - def set_status(self): - self.status = { + def set_status(self, update=False): + status = { "0": "Draft", "1": "Submitted", "2": "Cancelled" @@ -44,14 +45,18 @@ class ExpenseClaim(AccountsController): paid_amount = flt(self.total_amount_reimbursed) + flt(self.total_advance_amount) precision = self.precision("grand_total") - if (self.is_paid or (flt(self.total_sanctioned_amount) > 0 - and flt(self.grand_total, precision) == flt(paid_amount, precision))) \ - and self.docstatus == 1 and self.approval_status == 'Approved': - self.status = "Paid" + if (self.is_paid or (flt(self.total_sanctioned_amount) > 0 and self.docstatus == 1 + and flt(self.grand_total, precision) == flt(paid_amount, precision))) and self.approval_status == 'Approved': + status = "Paid" elif flt(self.total_sanctioned_amount) > 0 and self.docstatus == 1 and self.approval_status == 'Approved': - self.status = "Unpaid" + status = "Unpaid" elif self.docstatus == 1 and self.approval_status == 'Rejected': - self.status = 'Rejected' + status = 'Rejected' + + if update: + self.db_set("status", status) + else: + self.status = status def on_update(self): share_doc_with_approver(self, self.expense_approver) @@ -72,9 +77,9 @@ class ExpenseClaim(AccountsController): self.make_gl_entries() if self.is_paid: - update_reimbursed_amount(self) + update_reimbursed_amount(self, self.grand_total) - self.set_status() + self.set_status(update=True) self.update_claimed_amount_in_employee_advance() def on_cancel(self): @@ -84,9 +89,8 @@ class ExpenseClaim(AccountsController): self.make_gl_entries(cancel=True) if self.is_paid: - update_reimbursed_amount(self) + update_reimbursed_amount(self, -1 * self.grand_total) - self.set_status() self.update_claimed_amount_in_employee_advance() def update_claimed_amount_in_employee_advance(self): @@ -266,20 +270,10 @@ class ExpenseClaim(AccountsController): if not expense.default_account or not validate: expense.default_account = get_expense_claim_account(expense.expense_type, self.company)["account"] -def update_reimbursed_amount(doc, jv=None): +def update_reimbursed_amount(doc, amount): - condition = "" - - if jv: - condition += "and voucher_no = '{0}'".format(jv) - - amt = frappe.db.sql("""select ifnull(sum(debit_in_account_currency), 0) - ifnull(sum(credit_in_account_currency), 0)as amt - from `tabGL Entry` where against_voucher_type = 'Expense Claim' and against_voucher = %s - and party = %s {condition}""".format(condition=condition), #nosec - (doc.name, doc.employee) ,as_dict=1)[0].amt - - doc.total_amount_reimbursed = amt - frappe.db.set_value("Expense Claim", doc.name , "total_amount_reimbursed", amt) + doc.total_amount_reimbursed += amount + frappe.db.set_value("Expense Claim", doc.name , "total_amount_reimbursed", doc.total_amount_reimbursed) doc.set_status() frappe.db.set_value("Expense Claim", doc.name , "status", doc.status) diff --git a/erpnext/hr/doctype/expense_claim/expense_claim_dashboard.py b/erpnext/hr/doctype/expense_claim/expense_claim_dashboard.py index 7de8f4fc13a..fe973507019 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim_dashboard.py +++ b/erpnext/hr/doctype/expense_claim/expense_claim_dashboard.py @@ -17,4 +17,4 @@ def get_data(): 'items': ['Employee Advance'] }, ] - } \ No newline at end of file + } diff --git a/erpnext/hr/doctype/expense_claim/test_expense_claim.js b/erpnext/hr/doctype/expense_claim/test_expense_claim.js index d0c43d3be47..2529faec983 100644 --- a/erpnext/hr/doctype/expense_claim/test_expense_claim.js +++ b/erpnext/hr/doctype/expense_claim/test_expense_claim.js @@ -42,4 +42,3 @@ QUnit.test("Test: Expense Claim [HR]", function (assert) { () => done() ]); }); - diff --git a/erpnext/hr/doctype/expense_claim/test_expense_claim.py b/erpnext/hr/doctype/expense_claim/test_expense_claim.py index 96ea686706c..b5fc1fbe758 100644 --- a/erpnext/hr/doctype/expense_claim/test_expense_claim.py +++ b/erpnext/hr/doctype/expense_claim/test_expense_claim.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe import unittest -from frappe.utils import random_string, nowdate +from frappe.utils import random_string, nowdate, flt from erpnext.hr.doctype.expense_claim.expense_claim import make_bank_entry from erpnext.accounts.doctype.account.test_account import create_account from erpnext.hr.doctype.employee.test_employee import make_employee @@ -72,7 +72,7 @@ class TestExpenseClaim(unittest.TestCase): def test_expense_claim_gl_entry(self): payable_account = get_payable_account(company_name) taxes = generate_taxes() - expense_claim = make_expense_claim(payable_account, 300, 200, company_name, "Travel Expenses - _TC4", + expense_claim = make_expense_claim(payable_account, 300, 200, company_name, "Travel Expenses - _TC4", do_not_submit=True, taxes=taxes) expense_claim.submit() @@ -138,6 +138,31 @@ class TestExpenseClaim(unittest.TestCase): expense_claim.submit() frappe.set_user("Administrator") + def test_multiple_payment_entries_against_expense(self): + # Creating expense claim + payable_account = get_payable_account("_Test Company") + expense_claim = make_expense_claim(payable_account, 5500, 5500, "_Test Company", "Travel Expenses - _TC") + expense_claim.save() + expense_claim.submit() + + # Payment entry 1: paying 500 + make_payment_entry(expense_claim, payable_account,500) + outstanding_amount, total_amount_reimbursed = get_outstanding_and_total_reimbursed_amounts(expense_claim) + self.assertEqual(outstanding_amount, 5000) + self.assertEqual(total_amount_reimbursed, 500) + + # Payment entry 1: paying 2000 + make_payment_entry(expense_claim, payable_account,2000) + outstanding_amount, total_amount_reimbursed = get_outstanding_and_total_reimbursed_amounts(expense_claim) + self.assertEqual(outstanding_amount, 3000) + self.assertEqual(total_amount_reimbursed, 2500) + + # Payment entry 1: paying 3000 + make_payment_entry(expense_claim, payable_account,3000) + outstanding_amount, total_amount_reimbursed = get_outstanding_and_total_reimbursed_amounts(expense_claim) + self.assertEqual(outstanding_amount, 0) + self.assertEqual(total_amount_reimbursed, 5500) + def get_payable_account(company): return frappe.get_cached_value('Company', company, 'default_payable_account') @@ -191,3 +216,23 @@ def make_expense_claim(payable_account, amount, sanctioned_amount, company, acco return expense_claim expense_claim.submit() return expense_claim + +def get_outstanding_and_total_reimbursed_amounts(expense_claim): + outstanding_amount = flt(frappe.db.get_value("Expense Claim", expense_claim.name, "total_sanctioned_amount")) - \ + flt(frappe.db.get_value("Expense Claim", expense_claim.name, "total_amount_reimbursed")) + total_amount_reimbursed = flt(frappe.db.get_value("Expense Claim", expense_claim.name, "total_amount_reimbursed")) + + return outstanding_amount,total_amount_reimbursed + +def make_payment_entry(expense_claim, payable_account, amt): + from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry + + pe = get_payment_entry("Expense Claim", expense_claim.name, bank_account="_Test Bank USD - _TC", bank_amount=amt) + pe.reference_no = "1" + pe.reference_date = nowdate() + pe.source_exchange_rate = 1 + pe.paid_to = payable_account + pe.references[0].allocated_amount = amt + pe.insert() + pe.submit() + diff --git a/erpnext/hr/doctype/expense_claim_detail/expense_claim_detail.py b/erpnext/hr/doctype/expense_claim_detail/expense_claim_detail.py index 8bfa1ade072..5d48990c5ce 100644 --- a/erpnext/hr/doctype/expense_claim_detail/expense_claim_detail.py +++ b/erpnext/hr/doctype/expense_claim_detail/expense_claim_detail.py @@ -7,4 +7,4 @@ import frappe from frappe.model.document import Document class ExpenseClaimDetail(Document): - pass \ No newline at end of file + pass diff --git a/erpnext/hr/doctype/expense_claim_type/expense_claim_type.py b/erpnext/hr/doctype/expense_claim_type/expense_claim_type.py index 2595506486d..a637a540213 100644 --- a/erpnext/hr/doctype/expense_claim_type/expense_claim_type.py +++ b/erpnext/hr/doctype/expense_claim_type/expense_claim_type.py @@ -25,4 +25,4 @@ class ExpenseClaimType(Document): """Error when Company of Ledger account doesn't match with Company Selected""" if frappe.db.get_value("Account", entry.default_account, "company") != entry.company: frappe.throw(_("Account {0} does not match with Company {1}" - ).format(entry.default_account, entry.company)) \ No newline at end of file + ).format(entry.default_account, entry.company)) diff --git a/erpnext/hr/doctype/expense_claim_type/test_expense_claim_type.js b/erpnext/hr/doctype/expense_claim_type/test_expense_claim_type.js index 62234e08a04..3c9ed35313d 100644 --- a/erpnext/hr/doctype/expense_claim_type/test_expense_claim_type.js +++ b/erpnext/hr/doctype/expense_claim_type/test_expense_claim_type.js @@ -27,4 +27,3 @@ QUnit.test("Test: Expense Claim Type [HR]", function (assert) { () => done() ]); }); - diff --git a/erpnext/hr/doctype/holiday/holiday.py b/erpnext/hr/doctype/holiday/holiday.py index aabab0b0d35..78a95b9b741 100644 --- a/erpnext/hr/doctype/holiday/holiday.py +++ b/erpnext/hr/doctype/holiday/holiday.py @@ -7,4 +7,4 @@ import frappe from frappe.model.document import Document class Holiday(Document): - pass \ No newline at end of file + pass diff --git a/erpnext/hr/doctype/holiday_list/holiday_list_dashboard.py b/erpnext/hr/doctype/holiday_list/holiday_list_dashboard.py index 22e1de0c342..05641c7dc26 100644 --- a/erpnext/hr/doctype/holiday_list/holiday_list_dashboard.py +++ b/erpnext/hr/doctype/holiday_list/holiday_list_dashboard.py @@ -18,4 +18,4 @@ def get_data(): 'items': ['Service Level', 'Service Level Agreement'] } ] - } \ No newline at end of file + } diff --git a/erpnext/hr/doctype/holiday_list/test_holiday_list.js b/erpnext/hr/doctype/holiday_list/test_holiday_list.js index bfcafa9460c..ce766143a62 100644 --- a/erpnext/hr/doctype/holiday_list/test_holiday_list.js +++ b/erpnext/hr/doctype/holiday_list/test_holiday_list.js @@ -39,4 +39,4 @@ QUnit.test("Test: Holiday list [HR]", function (assert) { }, () => done() ]); -}); \ No newline at end of file +}); diff --git a/erpnext/hr/doctype/hr_settings/hr_settings.js b/erpnext/hr/doctype/hr_settings/hr_settings.js index fd082fda09b..ec99472d9bc 100644 --- a/erpnext/hr/doctype/hr_settings/hr_settings.js +++ b/erpnext/hr/doctype/hr_settings/hr_settings.js @@ -5,4 +5,4 @@ frappe.ui.form.on('HR Settings', { restrict_backdated_leave_application: function(frm) { frm.toggle_reqd("role_allowed_to_create_backdated_leave_application", frm.doc.restrict_backdated_leave_application); } -}); \ No newline at end of file +}); diff --git a/erpnext/hr/doctype/hr_settings/hr_settings.json b/erpnext/hr/doctype/hr_settings/hr_settings.json index 2396a8eee92..8aa3c0ca9f1 100644 --- a/erpnext/hr/doctype/hr_settings/hr_settings.json +++ b/erpnext/hr/doctype/hr_settings/hr_settings.json @@ -11,8 +11,14 @@ "emp_created_by", "column_break_4", "standard_working_hours", - "stop_birthday_reminders", "expense_approver_mandatory_in_expense_claim", + "reminders_section", + "send_birthday_reminders", + "column_break_9", + "send_work_anniversary_reminders", + "column_break_11", + "send_holiday_reminders", + "frequency", "leave_settings", "send_leave_notification", "leave_approval_notification_template", @@ -50,13 +56,6 @@ "fieldname": "column_break_4", "fieldtype": "Column Break" }, - { - "default": "0", - "description": "Don't send employee birthday reminders", - "fieldname": "stop_birthday_reminders", - "fieldtype": "Check", - "label": "Stop Birthday Reminders" - }, { "default": "1", "fieldname": "expense_approver_mandatory_in_expense_claim", @@ -142,13 +141,53 @@ "fieldname": "standard_working_hours", "fieldtype": "Int", "label": "Standard Working Hours" + }, + { + "collapsible": 1, + "fieldname": "reminders_section", + "fieldtype": "Section Break", + "label": "Reminders" + }, + { + "default": "1", + "fieldname": "send_holiday_reminders", + "fieldtype": "Check", + "label": "Holidays" + }, + { + "default": "1", + "fieldname": "send_work_anniversary_reminders", + "fieldtype": "Check", + "label": "Work Anniversaries " + }, + { + "default": "Weekly", + "depends_on": "eval:doc.send_holiday_reminders", + "fieldname": "frequency", + "fieldtype": "Select", + "label": "Set the frequency for holiday reminders", + "options": "Weekly\nMonthly" + }, + { + "default": "1", + "fieldname": "send_birthday_reminders", + "fieldtype": "Check", + "label": "Birthdays" + }, + { + "fieldname": "column_break_9", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_11", + "fieldtype": "Column Break" } ], "icon": "fa fa-cog", "idx": 1, "issingle": 1, "links": [], - "modified": "2021-05-11 10:52:56.192773", + "modified": "2021-08-24 14:54:12.834162", "modified_by": "Administrator", "module": "HR", "name": "HR Settings", diff --git a/erpnext/hr/doctype/hr_settings/hr_settings.py b/erpnext/hr/doctype/hr_settings/hr_settings.py index ced98fb9a58..a47409363c7 100644 --- a/erpnext/hr/doctype/hr_settings/hr_settings.py +++ b/erpnext/hr/doctype/hr_settings/hr_settings.py @@ -1,18 +1,79 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt # For license information, please see license.txt -from __future__ import unicode_literals import frappe + from frappe.model.document import Document +from frappe.utils import format_date + +# Wether to proceed with frequency change +PROCEED_WITH_FREQUENCY_CHANGE = False class HRSettings(Document): def validate(self): self.set_naming_series() + # Based on proceed flag + global PROCEED_WITH_FREQUENCY_CHANGE + if not PROCEED_WITH_FREQUENCY_CHANGE: + self.validate_frequency_change() + PROCEED_WITH_FREQUENCY_CHANGE = False + def set_naming_series(self): from erpnext.setup.doctype.naming_series.naming_series import set_by_naming_series set_by_naming_series("Employee", "employee_number", self.get("emp_created_by")=="Naming Series", hide_name_field=True) + def validate_frequency_change(self): + weekly_job, monthly_job = None, None + + try: + weekly_job = frappe.get_doc( + 'Scheduled Job Type', + 'employee_reminders.send_reminders_in_advance_weekly' + ) + + monthly_job = frappe.get_doc( + 'Scheduled Job Type', + 'employee_reminders.send_reminders_in_advance_monthly' + ) + except frappe.DoesNotExistError: + return + + next_weekly_trigger = weekly_job.get_next_execution() + next_monthly_trigger = monthly_job.get_next_execution() + + if self.freq_changed_from_monthly_to_weekly(): + if next_monthly_trigger < next_weekly_trigger: + self.show_freq_change_warning(next_monthly_trigger, next_weekly_trigger) + + elif self.freq_changed_from_weekly_to_monthly(): + if next_monthly_trigger > next_weekly_trigger: + self.show_freq_change_warning(next_weekly_trigger, next_monthly_trigger) + + def freq_changed_from_weekly_to_monthly(self): + return self.has_value_changed("frequency") and self.frequency == "Monthly" + + def freq_changed_from_monthly_to_weekly(self): + return self.has_value_changed("frequency") and self.frequency == "Weekly" + + def show_freq_change_warning(self, from_date, to_date): + from_date = frappe.bold(format_date(from_date)) + to_date = frappe.bold(format_date(to_date)) + frappe.msgprint( + msg=frappe._('Employees will miss holiday reminders from {} until {}.
Do you want to proceed with this change?').format(from_date, to_date), + title='Confirm change in Frequency', + primary_action={ + 'label': frappe._('Yes, Proceed'), + 'client_action': 'erpnext.proceed_save_with_reminders_frequency_change' + }, + raise_exception=frappe.ValidationError + ) + +@frappe.whitelist() +def set_proceed_with_frequency_change(): + '''Enables proceed with frequency change''' + global PROCEED_WITH_FREQUENCY_CHANGE + PROCEED_WITH_FREQUENCY_CHANGE = True diff --git a/erpnext/hr/doctype/hr_settings/test_hr_settings.js b/erpnext/hr/doctype/hr_settings/test_hr_settings.js deleted file mode 100644 index f32640ba5c6..00000000000 --- a/erpnext/hr/doctype/hr_settings/test_hr_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: HR Settings", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new HR Settings - () => frappe.tests.make('HR Settings', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/hr/doctype/identification_document_type/test_identification_document_type.js b/erpnext/hr/doctype/identification_document_type/test_identification_document_type.js deleted file mode 100644 index 65879098e87..00000000000 --- a/erpnext/hr/doctype/identification_document_type/test_identification_document_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: Identification Document Type", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Identification Document Type - () => frappe.tests.make('Identification Document Type', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/hr/doctype/job_applicant/job_applicant.js b/erpnext/hr/doctype/job_applicant/job_applicant.js index c62515597ce..7658bc93539 100644 --- a/erpnext/hr/doctype/job_applicant/job_applicant.js +++ b/erpnext/hr/doctype/job_applicant/job_applicant.js @@ -38,4 +38,4 @@ frappe.ui.form.on("Job Applicant", { }); } -}); \ No newline at end of file +}); diff --git a/erpnext/hr/doctype/job_applicant/job_applicant.py b/erpnext/hr/doctype/job_applicant/job_applicant.py index 0594ba395ba..14aeb03a87e 100644 --- a/erpnext/hr/doctype/job_applicant/job_applicant.py +++ b/erpnext/hr/doctype/job_applicant/job_applicant.py @@ -50,4 +50,3 @@ class JobApplicant(Document): if names: frappe.throw(_("Email Address must be unique, already exists for {0}").format(comma_and(names)), frappe.DuplicateEntryError) - diff --git a/erpnext/hr/doctype/job_applicant/job_applicant_dashboard.py b/erpnext/hr/doctype/job_applicant/job_applicant_dashboard.py index 7f131151e18..ed97978a8ad 100644 --- a/erpnext/hr/doctype/job_applicant/job_applicant_dashboard.py +++ b/erpnext/hr/doctype/job_applicant/job_applicant_dashboard.py @@ -12,4 +12,4 @@ def get_data(): 'items': ['Job Offer'] }, ], - } \ No newline at end of file + } diff --git a/erpnext/hr/doctype/job_applicant/test_job_applicant.js b/erpnext/hr/doctype/job_applicant/test_job_applicant.js index b5391c8bf36..741a182addc 100644 --- a/erpnext/hr/doctype/job_applicant/test_job_applicant.js +++ b/erpnext/hr/doctype/job_applicant/test_job_applicant.js @@ -26,4 +26,3 @@ QUnit.test("Test: Job Opening [HR]", function (assert) { () => done() ]); }); - diff --git a/erpnext/hr/doctype/job_applicant_source/test_job_applicant_source.js b/erpnext/hr/doctype/job_applicant_source/test_job_applicant_source.js deleted file mode 100644 index c093928f321..00000000000 --- a/erpnext/hr/doctype/job_applicant_source/test_job_applicant_source.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: Job Applicant Source", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Job Applicant Source - () => frappe.tests.make('Job Applicant Source', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/hr/doctype/job_offer/test_job_offer.js b/erpnext/hr/doctype/job_offer/test_job_offer.js index c9d7d2bef79..5339b9c3d67 100644 --- a/erpnext/hr/doctype/job_offer/test_job_offer.js +++ b/erpnext/hr/doctype/job_offer/test_job_offer.js @@ -48,4 +48,4 @@ QUnit.test("Test: Job Offer [HR]", function (assert) { () => frappe.timeout(2), () => done() ]); -}); \ No newline at end of file +}); diff --git a/erpnext/hr/doctype/job_offer/test_job_offer.py b/erpnext/hr/doctype/job_offer/test_job_offer.py index b3e1dc8d87b..edb21321fcc 100644 --- a/erpnext/hr/doctype/job_offer/test_job_offer.py +++ b/erpnext/hr/doctype/job_offer/test_job_offer.py @@ -79,4 +79,4 @@ def create_staffing_plan(**args): }) staffing_plan.insert() staffing_plan.submit() - return staffing_plan \ No newline at end of file + return staffing_plan diff --git a/erpnext/hr/doctype/job_opening/job_opening_dashboard.py b/erpnext/hr/doctype/job_opening/job_opening_dashboard.py index c0890b4f57c..31ef33ef2ce 100644 --- a/erpnext/hr/doctype/job_opening/job_opening_dashboard.py +++ b/erpnext/hr/doctype/job_opening/job_opening_dashboard.py @@ -9,4 +9,4 @@ def get_data(): 'items': ['Job Applicant'] } ], - } \ No newline at end of file + } diff --git a/erpnext/hr/doctype/job_opening/templates/job_opening_row.html b/erpnext/hr/doctype/job_opening/templates/job_opening_row.html index c015101600a..69bf49bef77 100644 --- a/erpnext/hr/doctype/job_opening/templates/job_opening_row.html +++ b/erpnext/hr/doctype/job_opening/templates/job_opening_row.html @@ -1,16 +1,16 @@

{{ doc.job_title }}

{{ doc.description }}

- {%- if doc.publish_salary_range -%} + {%- if doc.publish_salary_range -%}

{{_("Salary range per month")}}: {{ frappe.format_value(frappe.utils.flt(doc.lower_range), currency=doc.currency) }} - {{ frappe.format_value(frappe.utils.flt(doc.upper_range), currency=doc.currency) }}

{% endif %}
{%- if doc.job_application_route -%} - {{ _("Apply Now") }} {% else %} - {{ _("Apply Now") }} {% endif %} diff --git a/erpnext/hr/doctype/job_opening/test_job_opening.js b/erpnext/hr/doctype/job_opening/test_job_opening.js index b9e6c0a8b2d..cc2f027e85b 100644 --- a/erpnext/hr/doctype/job_opening/test_job_opening.js +++ b/erpnext/hr/doctype/job_opening/test_job_opening.js @@ -24,4 +24,3 @@ QUnit.test("Test: Job Opening [HR]", function (assert) { () => done() ]); }); - diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.js b/erpnext/hr/doctype/leave_allocation/leave_allocation.js index e9e129cdd24..d94764104d0 100755 --- a/erpnext/hr/doctype/leave_allocation/leave_allocation.js +++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.js @@ -100,4 +100,4 @@ frappe.ui.form.on("Leave Allocation", { frm.set_value("total_leaves_allocated", flt(frm.doc.new_leaves_allocated)); } } -}); \ No newline at end of file +}); diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation_dashboard.py b/erpnext/hr/doctype/leave_allocation/leave_allocation_dashboard.py index 7456aebb457..7a063d92eac 100644 --- a/erpnext/hr/doctype/leave_allocation/leave_allocation_dashboard.py +++ b/erpnext/hr/doctype/leave_allocation/leave_allocation_dashboard.py @@ -17,4 +17,4 @@ def get_data(): 'items': ['Employee Leave Balance'] } ] - } \ No newline at end of file + } diff --git a/erpnext/hr/doctype/leave_allocation/test_leave_allocation.js b/erpnext/hr/doctype/leave_allocation/test_leave_allocation.js index 0ef78f2f883..d5364fc8b2e 100644 --- a/erpnext/hr/doctype/leave_allocation/test_leave_allocation.js +++ b/erpnext/hr/doctype/leave_allocation/test_leave_allocation.js @@ -38,4 +38,4 @@ QUnit.test("Test: Leave allocation [HR]", function (assert) { "total leave calculation is correctly set"), () => done() ]); -}); \ No newline at end of file +}); diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py index cee6f374fdc..2e37c135947 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.py +++ b/erpnext/hr/doctype/leave_application/leave_application.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe from frappe import _ from frappe.utils import cint, cstr, date_diff, flt, formatdate, getdate, get_link_to_form, get_fullname, add_days, nowdate -from erpnext.hr.utils import set_employee_name, get_leave_period, share_doc_with_approver +from erpnext.hr.utils import set_employee_name, get_leave_period, share_doc_with_approver, validate_active_employee from erpnext.hr.doctype.leave_block_list.leave_block_list import get_applicable_block_dates from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee from erpnext.buying.doctype.supplier_scorecard.supplier_scorecard import daterange @@ -22,6 +22,7 @@ class LeaveApplication(Document): return _("{0}: From {0} of type {1}").format(self.employee_name, self.leave_type) def validate(self): + validate_active_employee(self.employee) set_employee_name(self) self.validate_dates() self.validate_balance_leaves() @@ -661,26 +662,30 @@ def is_lwp(leave_type): @frappe.whitelist() def get_events(start, end, filters=None): + from frappe.desk.reportview import get_filters_cond events = [] - employee = frappe.db.get_value("Employee", {"user_id": frappe.session.user}, ["name", "company"], - as_dict=True) + employee = frappe.db.get_value("Employee", + filters={"user_id": frappe.session.user}, + fieldname=["name", "company"], + as_dict=True + ) + if employee: employee, company = employee.name, employee.company else: - employee='' - company=frappe.db.get_value("Global Defaults", None, "default_company") + employee = '' + company = frappe.db.get_value("Global Defaults", None, "default_company") - from frappe.desk.reportview import get_filters_cond conditions = get_filters_cond("Leave Application", filters, []) # show department leaves for employee if "Employee" in frappe.get_roles(): add_department_leaves(events, start, end, employee, company) add_leaves(events, start, end, conditions) - add_block_dates(events, start, end, employee, company) add_holidays(events, start, end, employee, company) + return events def add_department_leaves(events, start, end, employee, company): @@ -696,26 +701,37 @@ def add_department_leaves(events, start, end, employee, company): filter_conditions = " and employee in (\"%s\")" % '", "'.join(department_employees) add_leaves(events, start, end, filter_conditions=filter_conditions) + def add_leaves(events, start, end, filter_conditions=None): + from frappe.desk.reportview import build_match_conditions conditions = [] - if not cint(frappe.db.get_value("HR Settings", None, "show_leaves_of_all_department_members_in_calendar")): - from frappe.desk.reportview import build_match_conditions match_conditions = build_match_conditions("Leave Application") if match_conditions: conditions.append(match_conditions) - query = """select name, from_date, to_date, employee_name, half_day, - status, employee, docstatus - from `tabLeave Application` where - from_date <= %(end)s and to_date >= %(start)s <= to_date - and docstatus < 2 - and status!='Rejected' """ + query = """SELECT + docstatus, + name, + employee, + employee_name, + leave_type, + from_date, + to_date, + half_day, + status, + color + FROM `tabLeave Application` + WHERE + from_date <= %(end)s AND to_date >= %(start)s <= to_date + AND docstatus < 2 + AND status != 'Rejected' + """ if conditions: - query += ' and ' + ' and '.join(conditions) + query += ' AND ' + ' AND '.join(conditions) if filter_conditions: query += filter_conditions @@ -728,11 +744,13 @@ def add_leaves(events, start, end, filter_conditions=None): "to_date": d.to_date, "docstatus": d.docstatus, "color": d.color, - "title": cstr(d.employee_name) + (' ' + _('(Half Day)') if d.half_day else ''), + "all_day": int(not d.half_day), + "title": cstr(d.employee_name) + f' ({cstr(d.leave_type)})' + (' ' + _('(Half Day)') if d.half_day else ''), } if e not in events: events.append(e) + def add_block_dates(events, start, end, employee, company): # block days from erpnext.hr.doctype.leave_block_list.leave_block_list import get_applicable_block_dates diff --git a/erpnext/hr/doctype/leave_application/leave_application_calendar.js b/erpnext/hr/doctype/leave_application/leave_application_calendar.js index 0286f300646..0ba0285552b 100644 --- a/erpnext/hr/doctype/leave_application/leave_application_calendar.js +++ b/erpnext/hr/doctype/leave_application/leave_application_calendar.js @@ -7,7 +7,9 @@ frappe.views.calendar["Leave Application"] = { "end": "to_date", "id": "name", "title": "title", - "docstatus": 1 + "docstatus": 1, + "color": "color", + "allDay": "all_day" }, options: { header: { @@ -17,4 +19,4 @@ frappe.views.calendar["Leave Application"] = { } }, get_events_method: "erpnext.hr.doctype.leave_application.leave_application.get_events" -} \ No newline at end of file +} diff --git a/erpnext/hr/doctype/leave_application/leave_application_dashboard.py b/erpnext/hr/doctype/leave_application/leave_application_dashboard.py index c1d6a6665b6..c45717f5870 100644 --- a/erpnext/hr/doctype/leave_application/leave_application_dashboard.py +++ b/erpnext/hr/doctype/leave_application/leave_application_dashboard.py @@ -17,4 +17,4 @@ def get_data(): 'items': ['Employee Leave Balance'] } ] - } \ No newline at end of file + } diff --git a/erpnext/hr/doctype/leave_application/leave_application_email_template.html b/erpnext/hr/doctype/leave_application/leave_application_email_template.html index 209302e8f30..14ca41bebca 100644 --- a/erpnext/hr/doctype/leave_application/leave_application_email_template.html +++ b/erpnext/hr/doctype/leave_application/leave_application_email_template.html @@ -21,5 +21,5 @@ Status {{status}} - + diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.js b/erpnext/hr/doctype/leave_application/test_leave_application.js index 6d7b6a70588..0866b0b6d2a 100644 --- a/erpnext/hr/doctype/leave_application/test_leave_application.js +++ b/erpnext/hr/doctype/leave_application/test_leave_application.js @@ -39,4 +39,4 @@ QUnit.test("Test: Leave application [HR]", function (assert) { // "leave for correct employee is submitted"), () => done() ]); -}); \ No newline at end of file +}); diff --git a/erpnext/hr/doctype/leave_block_list/leave_block_list_dashboard.py b/erpnext/hr/doctype/leave_block_list/leave_block_list_dashboard.py index 2aa54984ec5..45aa4915bc6 100644 --- a/erpnext/hr/doctype/leave_block_list/leave_block_list_dashboard.py +++ b/erpnext/hr/doctype/leave_block_list/leave_block_list_dashboard.py @@ -8,4 +8,4 @@ def get_data(): 'items': ['Department'] } ] - } \ No newline at end of file + } diff --git a/erpnext/hr/doctype/leave_block_list/test_leave_block_list.js b/erpnext/hr/doctype/leave_block_list/test_leave_block_list.js index 453787865c7..b39601b490d 100644 --- a/erpnext/hr/doctype/leave_block_list/test_leave_block_list.js +++ b/erpnext/hr/doctype/leave_block_list/test_leave_block_list.js @@ -24,4 +24,4 @@ QUnit.test("Test: Leave block list [HR]", function (assert) { 'name of blocked leave list correctly saved'), () => done() ]); -}); \ No newline at end of file +}); diff --git a/erpnext/hr/doctype/leave_block_list_allow/leave_block_list_allow.py b/erpnext/hr/doctype/leave_block_list_allow/leave_block_list_allow.py index be06b768bf7..8e5a09e01ec 100644 --- a/erpnext/hr/doctype/leave_block_list_allow/leave_block_list_allow.py +++ b/erpnext/hr/doctype/leave_block_list_allow/leave_block_list_allow.py @@ -9,4 +9,4 @@ import frappe from frappe.model.document import Document class LeaveBlockListAllow(Document): - pass \ No newline at end of file + pass diff --git a/erpnext/hr/doctype/leave_block_list_date/leave_block_list_date.py b/erpnext/hr/doctype/leave_block_list_date/leave_block_list_date.py index f4028f54eba..54978a1e83a 100644 --- a/erpnext/hr/doctype/leave_block_list_date/leave_block_list_date.py +++ b/erpnext/hr/doctype/leave_block_list_date/leave_block_list_date.py @@ -9,4 +9,4 @@ import frappe from frappe.model.document import Document class LeaveBlockListDate(Document): - pass \ No newline at end of file + pass diff --git a/erpnext/hr/doctype/leave_control_panel/leave_control_panel.js b/erpnext/hr/doctype/leave_control_panel/leave_control_panel.js index b60e225a727..4a450807ccf 100644 --- a/erpnext/hr/doctype/leave_control_panel/leave_control_panel.js +++ b/erpnext/hr/doctype/leave_control_panel/leave_control_panel.js @@ -21,4 +21,4 @@ frappe.ui.form.on("Leave Control Panel", { }); } } -}); \ No newline at end of file +}); diff --git a/erpnext/hr/doctype/leave_control_panel/test_leave_control_panel.js b/erpnext/hr/doctype/leave_control_panel/test_leave_control_panel.js index 2b5cec1c1e1..9d373277175 100644 --- a/erpnext/hr/doctype/leave_control_panel/test_leave_control_panel.js +++ b/erpnext/hr/doctype/leave_control_panel/test_leave_control_panel.js @@ -47,4 +47,4 @@ QUnit.test("Test: Leave control panel [HR]", function (assert) { }, () => done() ]); -}); \ No newline at end of file +}); diff --git a/erpnext/hr/doctype/leave_encashment/leave_encashment.py b/erpnext/hr/doctype/leave_encashment/leave_encashment.py index e041b7fb8f8..d136210a043 100644 --- a/erpnext/hr/doctype/leave_encashment/leave_encashment.py +++ b/erpnext/hr/doctype/leave_encashment/leave_encashment.py @@ -7,7 +7,7 @@ import frappe from frappe import _ from frappe.model.document import Document from frappe.utils import getdate, nowdate, flt -from erpnext.hr.utils import set_employee_name +from erpnext.hr.utils import set_employee_name, validate_active_employee from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment import get_assigned_salary_structure from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import create_leave_ledger_entry from erpnext.hr.doctype.leave_allocation.leave_allocation import get_unused_leaves @@ -15,6 +15,7 @@ from erpnext.hr.doctype.leave_allocation.leave_allocation import get_unused_leav class LeaveEncashment(Document): def validate(self): set_employee_name(self) + validate_active_employee(self.employee) self.get_leave_details_for_encashment() self.validate_salary_structure() @@ -133,4 +134,4 @@ def create_leave_encashment(leave_allocation): leave_type=allocation.leave_type, encashment_date=allocation.to_date )) - leave_encashment.insert(ignore_permissions=True) \ No newline at end of file + leave_encashment.insert(ignore_permissions=True) diff --git a/erpnext/hr/doctype/leave_encashment/test_leave_encashment.js b/erpnext/hr/doctype/leave_encashment/test_leave_encashment.js deleted file mode 100644 index cafd9602cbe..00000000000 --- a/erpnext/hr/doctype/leave_encashment/test_leave_encashment.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: Leave Encashment", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Leave Encashment - () => frappe.tests.make('Leave Encashment', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.py b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.py index cf130361810..33a6243e609 100644 --- a/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.py +++ b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.py @@ -185,4 +185,4 @@ def expire_carried_forward_allocation(allocation): from_date=allocation.to_date, to_date=allocation.to_date ) - create_leave_ledger_entry(allocation, args) \ No newline at end of file + create_leave_ledger_entry(allocation, args) diff --git a/erpnext/hr/doctype/leave_period/leave_period_dashboard.py b/erpnext/hr/doctype/leave_period/leave_period_dashboard.py index 1572de3cb72..7c2c9632d85 100644 --- a/erpnext/hr/doctype/leave_period/leave_period_dashboard.py +++ b/erpnext/hr/doctype/leave_period/leave_period_dashboard.py @@ -10,4 +10,4 @@ def get_data(): 'items': ['Leave Allocation'] } ] - } \ No newline at end of file + } diff --git a/erpnext/hr/doctype/leave_period/test_leave_period.js b/erpnext/hr/doctype/leave_period/test_leave_period.js deleted file mode 100644 index ec0a8096890..00000000000 --- a/erpnext/hr/doctype/leave_period/test_leave_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: Leave Period", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Leave Period - () => frappe.tests.make('Leave Period', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/hr/doctype/leave_period/test_leave_period.py b/erpnext/hr/doctype/leave_period/test_leave_period.py index b5857bcd8fe..cbb34371fc9 100644 --- a/erpnext/hr/doctype/leave_period/test_leave_period.py +++ b/erpnext/hr/doctype/leave_period/test_leave_period.py @@ -27,4 +27,4 @@ def create_leave_period(from_date, to_date, company=None): "to_date": to_date, "is_active": 1 }).insert() - return leave_period \ No newline at end of file + return leave_period diff --git a/erpnext/hr/doctype/leave_policy/leave_policy_dashboard.py b/erpnext/hr/doctype/leave_policy/leave_policy_dashboard.py index ff7f0422e03..474f3a77ad0 100644 --- a/erpnext/hr/doctype/leave_policy/leave_policy_dashboard.py +++ b/erpnext/hr/doctype/leave_policy/leave_policy_dashboard.py @@ -10,4 +10,4 @@ def get_data(): 'items': ['Leave Policy Assignment', 'Leave Allocation'] }, ] - } \ No newline at end of file + } diff --git a/erpnext/hr/doctype/leave_policy/test_leave_policy.js b/erpnext/hr/doctype/leave_policy/test_leave_policy.js deleted file mode 100644 index 5404a63bed1..00000000000 --- a/erpnext/hr/doctype/leave_policy/test_leave_policy.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: Leave Policy", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Leave Policy - () => frappe.tests.make('Leave Policy', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/hr/doctype/leave_policy/test_leave_policy.py b/erpnext/hr/doctype/leave_policy/test_leave_policy.py index fc868ea15a6..af7567b5bc7 100644 --- a/erpnext/hr/doctype/leave_policy/test_leave_policy.py +++ b/erpnext/hr/doctype/leave_policy/test_leave_policy.py @@ -28,4 +28,4 @@ def create_leave_policy(**args): "leave_type": args.leave_type or "_Test Leave Type", "annual_allocation": args.annual_allocation or 10 }] - }) \ No newline at end of file + }) diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_dashboard.py b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_dashboard.py index 4bb0535cf8c..a2f7f5866b7 100644 --- a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_dashboard.py +++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_dashboard.py @@ -10,4 +10,4 @@ def get_data(): 'items': ['Leave Allocation'] }, ] - } \ No newline at end of file + } diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_list.js b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_list.js index 8fe4b8f8efa..8b954c46a10 100644 --- a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_list.js +++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_list.js @@ -105,4 +105,4 @@ frappe.listview_settings['Leave Policy Assignment'] = { }); } } -}; \ No newline at end of file +}; diff --git a/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py b/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py index 9a14e3588d0..0089804f512 100644 --- a/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py +++ b/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py @@ -99,5 +99,3 @@ class TestLeavePolicyAssignment(unittest.TestCase): def tearDown(self): for doctype in ["Leave Application", "Leave Allocation", "Leave Policy Assignment", "Leave Ledger Entry"]: frappe.db.sql("delete from `tab{0}`".format(doctype)) #nosec - - diff --git a/erpnext/hr/doctype/leave_policy_detail/test_leave_policy_detail.js b/erpnext/hr/doctype/leave_policy_detail/test_leave_policy_detail.js deleted file mode 100644 index 1c8995b7967..00000000000 --- a/erpnext/hr/doctype/leave_policy_detail/test_leave_policy_detail.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: Leave Policy Detail", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Leave Policy Detail - () => frappe.tests.make('Leave Policy Detail', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/hr/doctype/leave_type/leave_type.json b/erpnext/hr/doctype/leave_type/leave_type.json index fc577ef1d3d..8f2ae6eb15d 100644 --- a/erpnext/hr/doctype/leave_type/leave_type.json +++ b/erpnext/hr/doctype/leave_type/leave_type.json @@ -214,7 +214,7 @@ "icon": "fa fa-flag", "idx": 1, "links": [], - "modified": "2021-03-02 11:22:33.776320", + "modified": "2021-08-12 16:10:36.464690", "modified_by": "Administrator", "module": "HR", "name": "Leave Type", @@ -248,5 +248,6 @@ } ], "sort_field": "modified", - "sort_order": "DESC" + "sort_order": "DESC", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/hr/doctype/leave_type/leave_type_dashboard.py b/erpnext/hr/doctype/leave_type/leave_type_dashboard.py index 5cae9a8809c..c8944fcb9e2 100644 --- a/erpnext/hr/doctype/leave_type/leave_type_dashboard.py +++ b/erpnext/hr/doctype/leave_type/leave_type_dashboard.py @@ -11,4 +11,4 @@ def get_data(): 'items': ['Attendance', 'Leave Encashment'] } ] - } \ No newline at end of file + } diff --git a/erpnext/hr/doctype/leave_type/test_leave_type.js b/erpnext/hr/doctype/leave_type/test_leave_type.js index d939a248102..db910cde512 100644 --- a/erpnext/hr/doctype/leave_type/test_leave_type.js +++ b/erpnext/hr/doctype/leave_type/test_leave_type.js @@ -19,4 +19,4 @@ QUnit.test("Test: Leave type [HR]", function (assert) { 'leave type correctly saved'), () => done() ]); -}); \ No newline at end of file +}); diff --git a/erpnext/hr/doctype/leave_type/test_leave_type.py b/erpnext/hr/doctype/leave_type/test_leave_type.py index 7fef2975c8a..048dddd3ef9 100644 --- a/erpnext/hr/doctype/leave_type/test_leave_type.py +++ b/erpnext/hr/doctype/leave_type/test_leave_type.py @@ -28,4 +28,4 @@ def create_leave_type(**args): if leave_type.is_ppl: leave_type.fraction_of_daily_salary_per_leave = args.fraction_of_daily_salary_per_leave or 0.5 - return leave_type \ No newline at end of file + return leave_type diff --git a/erpnext/hr/doctype/purpose_of_travel/test_purpose_of_travel.js b/erpnext/hr/doctype/purpose_of_travel/test_purpose_of_travel.js deleted file mode 100644 index 936c21ccf05..00000000000 --- a/erpnext/hr/doctype/purpose_of_travel/test_purpose_of_travel.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: Purpose of Travel", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Purpose of Travel - () => frappe.tests.make('Purpose of Travel', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/hr/doctype/shift_assignment/shift_assignment.py b/erpnext/hr/doctype/shift_assignment/shift_assignment.py index ab65260c091..89ae4d535d4 100644 --- a/erpnext/hr/doctype/shift_assignment/shift_assignment.py +++ b/erpnext/hr/doctype/shift_assignment/shift_assignment.py @@ -9,10 +9,12 @@ from frappe.model.document import Document from frappe.utils import cint, cstr, date_diff, flt, formatdate, getdate, now_datetime, nowdate from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday +from erpnext.hr.utils import validate_active_employee from datetime import timedelta, datetime class ShiftAssignment(Document): def validate(self): + validate_active_employee(self.employee) self.validate_overlapping_dates() if self.end_date and self.end_date <= self.start_date: diff --git a/erpnext/hr/doctype/shift_assignment/shift_assignment_calendar.js b/erpnext/hr/doctype/shift_assignment/shift_assignment_calendar.js index bb692e1402e..5d2360f10fa 100644 --- a/erpnext/hr/doctype/shift_assignment/shift_assignment_calendar.js +++ b/erpnext/hr/doctype/shift_assignment/shift_assignment_calendar.js @@ -10,4 +10,4 @@ frappe.views.calendar["Shift Assignment"] = { "allDay": "allDay", }, get_events_method: "erpnext.hr.doctype.shift_assignment.shift_assignment.get_events" -} \ No newline at end of file +} diff --git a/erpnext/hr/doctype/shift_assignment/test_shift_assignment.js b/erpnext/hr/doctype/shift_assignment/test_shift_assignment.js deleted file mode 100644 index 77272877423..00000000000 --- a/erpnext/hr/doctype/shift_assignment/test_shift_assignment.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: Shift Assignment", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Shift Assignment - () => frappe.tests.make('Shift Assignment', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/hr/doctype/shift_assignment/test_shift_assignment.py b/erpnext/hr/doctype/shift_assignment/test_shift_assignment.py index 4c3c1ed579e..07d92fe61d6 100644 --- a/erpnext/hr/doctype/shift_assignment/test_shift_assignment.py +++ b/erpnext/hr/doctype/shift_assignment/test_shift_assignment.py @@ -77,4 +77,4 @@ class TestShiftAssignment(unittest.TestCase): "status": 'Active' }) - self.assertRaises(frappe.ValidationError, shift_assignment_3.save) \ No newline at end of file + self.assertRaises(frappe.ValidationError, shift_assignment_3.save) diff --git a/erpnext/hr/doctype/shift_request/shift_request.py b/erpnext/hr/doctype/shift_request/shift_request.py index 177c45edc65..2731da125a8 100644 --- a/erpnext/hr/doctype/shift_request/shift_request.py +++ b/erpnext/hr/doctype/shift_request/shift_request.py @@ -7,12 +7,13 @@ import frappe from frappe import _ from frappe.model.document import Document from frappe.utils import formatdate, getdate -from erpnext.hr.utils import share_doc_with_approver +from erpnext.hr.utils import share_doc_with_approver, validate_active_employee class OverlapError(frappe.ValidationError): pass class ShiftRequest(Document): def validate(self): + validate_active_employee(self.employee) self.validate_dates() self.validate_shift_request_overlap_dates() self.validate_approver() @@ -93,4 +94,4 @@ class ShiftRequest(Document): msg = _("Employee {0} has already applied for {1} between {2} and {3} : ").format(self.employee, d['shift_type'], formatdate(d['from_date']), formatdate(d['to_date'])) \ + """ {0}""".format(d["name"]) - frappe.throw(msg, OverlapError) \ No newline at end of file + frappe.throw(msg, OverlapError) diff --git a/erpnext/hr/doctype/shift_request/shift_request_dashboard.py b/erpnext/hr/doctype/shift_request/shift_request_dashboard.py index e3bf5df9490..f70b61a20a6 100644 --- a/erpnext/hr/doctype/shift_request/shift_request_dashboard.py +++ b/erpnext/hr/doctype/shift_request/shift_request_dashboard.py @@ -9,4 +9,4 @@ def get_data(): 'items': ['Shift Assignment'] }, ], - } \ No newline at end of file + } diff --git a/erpnext/hr/doctype/shift_request/test_shift_request.js b/erpnext/hr/doctype/shift_request/test_shift_request.js deleted file mode 100644 index 9c8cd70020c..00000000000 --- a/erpnext/hr/doctype/shift_request/test_shift_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: Shift Request", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Shift Request - () => frappe.tests.make('Shift Request', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/hr/doctype/shift_request/test_shift_request.py b/erpnext/hr/doctype/shift_request/test_shift_request.py index 9c0d8e31985..60b7676e251 100644 --- a/erpnext/hr/doctype/shift_request/test_shift_request.py +++ b/erpnext/hr/doctype/shift_request/test_shift_request.py @@ -15,24 +15,35 @@ class TestShiftRequest(unittest.TestCase): for doctype in ["Shift Request", "Shift Assignment"]: frappe.db.sql("delete from `tab{doctype}`".format(doctype=doctype)) + def tearDown(self): + frappe.db.rollback() + def test_make_shift_request(self): + "Test creation/updation of Shift Assignment from Shift Request." department = frappe.get_value("Employee", "_T-Employee-00001", 'department') set_shift_approver(department) approver = frappe.db.sql("""select approver from `tabDepartment Approver` where parent= %s and parentfield = 'shift_request_approver'""", (department))[0][0] shift_request = make_shift_request(approver) - shift_assignments = frappe.db.sql(''' - SELECT shift_request, employee - FROM `tabShift Assignment` - WHERE shift_request = '{0}' - '''.format(shift_request.name), as_dict=1) - for d in shift_assignments: - employee = d.get('employee') - self.assertEqual(shift_request.employee, employee) - shift_request.cancel() - shift_assignment_doc = frappe.get_doc("Shift Assignment", {"shift_request": d.get('shift_request')}) - self.assertEqual(shift_assignment_doc.docstatus, 2) + # Only one shift assignment is created against a shift request + shift_assignment = frappe.db.get_value( + "Shift Assignment", + filters={"shift_request": shift_request.name}, + fieldname=["employee", "docstatus"], + as_dict=True + ) + self.assertEqual(shift_request.employee, shift_assignment.employee) + self.assertEqual(shift_assignment.docstatus, 1) + + shift_request.cancel() + + shift_assignment_docstatus = frappe.db.get_value( + "Shift Assignment", + filters={"shift_request": shift_request.name}, + fieldname="docstatus" + ) + self.assertEqual(shift_assignment_docstatus, 2) def test_shift_request_approver_perms(self): employee = frappe.get_doc("Employee", "_T-Employee-00001") @@ -95,4 +106,4 @@ def make_shift_request(approver, do_not_submit=0): return shift_request shift_request.submit() - return shift_request \ No newline at end of file + return shift_request diff --git a/erpnext/hr/doctype/shift_type/test_shift_type.js b/erpnext/hr/doctype/shift_type/test_shift_type.js deleted file mode 100644 index 846f9316f58..00000000000 --- a/erpnext/hr/doctype/shift_type/test_shift_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: Shift Type", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Shift Type - () => frappe.tests.make('Shift Type', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/hr/doctype/staffing_plan/staffing_plan.js b/erpnext/hr/doctype/staffing_plan/staffing_plan.js index 04af2323c72..597ecceaa0f 100644 --- a/erpnext/hr/doctype/staffing_plan/staffing_plan.js +++ b/erpnext/hr/doctype/staffing_plan/staffing_plan.js @@ -103,4 +103,4 @@ var set_total_estimated_budget = function(frm) { }) frm.set_value('total_estimated_budget', estimated_budget); } -} \ No newline at end of file +} diff --git a/erpnext/hr/doctype/staffing_plan/staffing_plan_dashboard.py b/erpnext/hr/doctype/staffing_plan/staffing_plan_dashboard.py index 35a303f0fb2..8e89d53c8e0 100644 --- a/erpnext/hr/doctype/staffing_plan/staffing_plan_dashboard.py +++ b/erpnext/hr/doctype/staffing_plan/staffing_plan_dashboard.py @@ -9,4 +9,4 @@ def get_data(): 'items': ['Job Opening'] } ], - } \ No newline at end of file + } diff --git a/erpnext/hr/doctype/staffing_plan/test_staffing_plan.js b/erpnext/hr/doctype/staffing_plan/test_staffing_plan.js deleted file mode 100644 index 64320bcd92b..00000000000 --- a/erpnext/hr/doctype/staffing_plan/test_staffing_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: Staffing Plan", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Staffing Plan - () => frappe.tests.make('Staffing Plan', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/hr/doctype/staffing_plan/test_staffing_plan.py b/erpnext/hr/doctype/staffing_plan/test_staffing_plan.py index 628255b11f5..1c6218e9a70 100644 --- a/erpnext/hr/doctype/staffing_plan/test_staffing_plan.py +++ b/erpnext/hr/doctype/staffing_plan/test_staffing_plan.py @@ -94,4 +94,4 @@ def make_company(): company.parent_company = "_Test Company 3" company.default_currency = "INR" company.country = "Pakistan" - company.insert() \ No newline at end of file + company.insert() diff --git a/erpnext/hr/doctype/training_event/test_training_event.py b/erpnext/hr/doctype/training_event/test_training_event.py index 313f90eba85..6a275b330c0 100644 --- a/erpnext/hr/doctype/training_event/test_training_event.py +++ b/erpnext/hr/doctype/training_event/test_training_event.py @@ -11,21 +11,34 @@ from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_ class TestTrainingEvent(unittest.TestCase): def setUp(self): create_training_program("Basic Training") - self.employee = make_employee("robert_loan@trainig.com") - self.employee2 = make_employee("suzie.tan@trainig.com") + employee = make_employee("robert_loan@trainig.com") + employee2 = make_employee("suzie.tan@trainig.com") + self.attendees = [ + {"employee": employee}, + {"employee": employee2} + ] + + def test_training_event_status_update(self): + training_event = create_training_event(self.attendees) + training_event.submit() + + training_event.event_status = "Completed" + training_event.save() + training_event.reload() + + for entry in training_event.employees: + self.assertEqual(entry.status, "Completed") + + training_event.event_status = "Scheduled" + training_event.save() + training_event.reload() + + for entry in training_event.employees: + self.assertEqual(entry.status, "Open") + + def tearDown(self): + frappe.db.rollback() - def test_create_training_event(self): - if not frappe.db.get_value("Training Event", "Basic Training Event"): - frappe.get_doc({ - "doctype": "Training Event", - "event_name": "Basic Training Event", - "training_program": "Basic Training", - "location": "Union Square", - "start_time": add_days(today(), 5), - "end_time": add_days(today(), 6), - "introduction": "Welcome to the Basic Training Event", - "employees": get_attendees(self.employee, self.employee2) - }).insert() def create_training_program(training_program): if not frappe.db.get_value("Training Program", training_program): @@ -35,8 +48,14 @@ def create_training_program(training_program): "description": training_program }).insert() -def get_attendees(employee, employee2): - return [ - {"employee": employee}, - {"employee": employee2} - ] \ No newline at end of file +def create_training_event(attendees): + return frappe.get_doc({ + "doctype": "Training Event", + "event_name": "Basic Training Event", + "training_program": "Basic Training", + "location": "Union Square", + "start_time": add_days(today(), 5), + "end_time": add_days(today(), 6), + "introduction": "Welcome to the Basic Training Event", + "employees": attendees + }).insert() diff --git a/erpnext/hr/doctype/training_event/tests/test_training_event.js b/erpnext/hr/doctype/training_event/tests/test_training_event.js index 8ff4fecd6ee..08031a1963e 100644 --- a/erpnext/hr/doctype/training_event/tests/test_training_event.js +++ b/erpnext/hr/doctype/training_event/tests/test_training_event.js @@ -56,4 +56,4 @@ QUnit.test("Test: Training Event [HR]", function (assert) { () => frappe.timeout(2), () => done() ]); -}); \ No newline at end of file +}); diff --git a/erpnext/hr/doctype/training_event/training_event.js b/erpnext/hr/doctype/training_event/training_event.js index 064dfb24557..642e6a1fd75 100644 --- a/erpnext/hr/doctype/training_event/training_event.js +++ b/erpnext/hr/doctype/training_event/training_event.js @@ -33,7 +33,8 @@ frappe.ui.form.on('Training Event', { frm.set_query("employee", "employees", function () { return { filters: { - name: ["NOT IN", emp] + name: ["NOT IN", emp], + status: "Active" } }; }); @@ -45,4 +46,3 @@ frappe.ui.form.on("Training Event Employee", { frm.events.set_employee_query(frm); } }); - diff --git a/erpnext/hr/doctype/training_event/training_event.py b/erpnext/hr/doctype/training_event/training_event.py index 5064f033081..e2c30cb3145 100644 --- a/erpnext/hr/doctype/training_event/training_event.py +++ b/erpnext/hr/doctype/training_event/training_event.py @@ -14,10 +14,25 @@ class TrainingEvent(Document): self.set_employee_emails() self.validate_period() + def on_update_after_submit(self): + self.set_status_for_attendees() + def set_employee_emails(self): self.employee_emails = ', '.join(get_employee_emails([d.employee for d in self.employees])) def validate_period(self): if time_diff_in_seconds(self.end_time, self.start_time) <= 0: - frappe.throw(_('End time cannot be before start time')) \ No newline at end of file + frappe.throw(_('End time cannot be before start time')) + + def set_status_for_attendees(self): + if self.event_status == 'Completed': + for employee in self.employees: + if employee.attendance == 'Present' and employee.status != 'Feedback Submitted': + employee.status = 'Completed' + + elif self.event_status == 'Scheduled': + for employee in self.employees: + employee.status = 'Open' + + self.db_update_all() diff --git a/erpnext/hr/doctype/training_event/training_event_dashboard.py b/erpnext/hr/doctype/training_event/training_event_dashboard.py index 1c1645c766f..19afd8dd6e1 100644 --- a/erpnext/hr/doctype/training_event/training_event_dashboard.py +++ b/erpnext/hr/doctype/training_event/training_event_dashboard.py @@ -9,4 +9,4 @@ def get_data(): 'items': ['Training Result', 'Training Feedback'] }, ], - } \ No newline at end of file + } diff --git a/erpnext/hr/doctype/training_feedback/test_training_feedback.js b/erpnext/hr/doctype/training_feedback/test_training_feedback.js index 9daa51f9275..5c825aea7fb 100644 --- a/erpnext/hr/doctype/training_feedback/test_training_feedback.js +++ b/erpnext/hr/doctype/training_feedback/test_training_feedback.js @@ -49,4 +49,3 @@ QUnit.test("Test: Training Feedback [HR]", function (assert) { () => done() ]); }); - diff --git a/erpnext/hr/doctype/training_feedback/test_training_feedback.py b/erpnext/hr/doctype/training_feedback/test_training_feedback.py index 34559982a24..4c0c18029d0 100644 --- a/erpnext/hr/doctype/training_feedback/test_training_feedback.py +++ b/erpnext/hr/doctype/training_feedback/test_training_feedback.py @@ -5,8 +5,63 @@ from __future__ import unicode_literals import frappe import unittest - -# test_records = frappe.get_test_records('Training Feedback') - +from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_employee +from erpnext.hr.doctype.training_event.test_training_event import create_training_program, create_training_event class TestTrainingFeedback(unittest.TestCase): - pass + def setUp(self): + create_training_program("Basic Training") + self.employee = make_employee("robert_loan@trainig.com") + self.employee2 = make_employee("suzie.tan@trainig.com") + self.attendees = [{"employee": self.employee}] + + def test_employee_validations_for_feedback(self): + training_event = create_training_event(self.attendees) + training_event.submit() + + training_event.event_status = "Completed" + training_event.save() + training_event.reload() + + # should not allow creating feedback since employee2 was not part of the event + feedback = create_training_feedback(training_event.name, self.employee2) + self.assertRaises(frappe.ValidationError, feedback.save) + + # cannot record feedback for absent employee + employee = frappe.db.get_value("Training Event Employee", { + "parent": training_event.name, + "employee": self.employee + }, "name") + + frappe.db.set_value("Training Event Employee", employee, "attendance", "Absent") + feedback = create_training_feedback(training_event.name, self.employee) + self.assertRaises(frappe.ValidationError, feedback.save) + + def test_training_feedback_status(self): + training_event = create_training_event(self.attendees) + training_event.submit() + + training_event.event_status = "Completed" + training_event.save() + training_event.reload() + + feedback = create_training_feedback(training_event.name, self.employee) + feedback.submit() + + status = frappe.db.get_value("Training Event Employee", { + "parent": training_event.name, + "employee": self.employee + }, "status") + + self.assertEqual(status, "Feedback Submitted") + + def tearDown(self): + frappe.db.rollback() + + +def create_training_feedback(event, employee): + return frappe.get_doc({ + "doctype": "Training Feedback", + "training_event": event, + "employee": employee, + "feedback": "Test" + }) diff --git a/erpnext/hr/doctype/training_feedback/training_feedback.js b/erpnext/hr/doctype/training_feedback/training_feedback.js index 0dea098a67e..5e875c1b434 100644 --- a/erpnext/hr/doctype/training_feedback/training_feedback.js +++ b/erpnext/hr/doctype/training_feedback/training_feedback.js @@ -7,4 +7,4 @@ frappe.ui.form.on('Training Feedback', { frm.add_fetch("training_event", "event_name", "event_name"); frm.add_fetch("training_event", "trainer_name", "trainer_name"); } -}); \ No newline at end of file +}); diff --git a/erpnext/hr/doctype/training_feedback/training_feedback.py b/erpnext/hr/doctype/training_feedback/training_feedback.py index 1a334507917..3d4b9b3ea96 100644 --- a/erpnext/hr/doctype/training_feedback/training_feedback.py +++ b/erpnext/hr/doctype/training_feedback/training_feedback.py @@ -11,15 +11,34 @@ class TrainingFeedback(Document): def validate(self): training_event = frappe.get_doc("Training Event", self.training_event) if training_event.docstatus != 1: - frappe.throw(_('{0} must be submitted').format(_('Training Event'))) + frappe.throw(_("{0} must be submitted").format(_("Training Event"))) + + emp_event_details = frappe.db.get_value("Training Event Employee", { + "parent": self.training_event, + "employee": self.employee + }, ["name", "attendance"], as_dict=True) + + if not emp_event_details: + frappe.throw(_("Employee {0} not found in Training Event Participants.").format( + frappe.bold(self.employee_name))) + + if emp_event_details.attendance == "Absent": + frappe.throw(_("Feedback cannot be recorded for an absent Employee.")) def on_submit(self): - training_event = frappe.get_doc("Training Event", self.training_event) - event_status = None - for e in training_event.employees: - if e.employee == self.employee: - event_status = 'Feedback Submitted' - break + employee = frappe.db.get_value("Training Event Employee", { + "parent": self.training_event, + "employee": self.employee + }) - if event_status: - frappe.db.set_value("Training Event", self.training_event, "event_status", event_status) + if employee: + frappe.db.set_value("Training Event Employee", employee, "status", "Feedback Submitted") + + def on_cancel(self): + employee = frappe.db.get_value("Training Event Employee", { + "parent": self.training_event, + "employee": self.employee + }) + + if employee: + frappe.db.set_value("Training Event Employee", employee, "status", "Completed") diff --git a/erpnext/hr/doctype/training_program/test_training_program.js b/erpnext/hr/doctype/training_program/test_training_program.js deleted file mode 100644 index 3a62b2fa221..00000000000 --- a/erpnext/hr/doctype/training_program/test_training_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: Training Program", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Training Program - () => frappe.tests.make('Training Program', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/hr/doctype/training_program/training_program.js b/erpnext/hr/doctype/training_program/training_program.js index 7d85cab59dc..a4ccf540636 100644 --- a/erpnext/hr/doctype/training_program/training_program.js +++ b/erpnext/hr/doctype/training_program/training_program.js @@ -2,4 +2,4 @@ // For license information, please see license.txt frappe.ui.form.on('Training Program', { -}); \ No newline at end of file +}); diff --git a/erpnext/hr/doctype/training_program/training_program_dashboard.py b/erpnext/hr/doctype/training_program/training_program_dashboard.py index 441a71bba77..0fc18a80298 100644 --- a/erpnext/hr/doctype/training_program/training_program_dashboard.py +++ b/erpnext/hr/doctype/training_program/training_program_dashboard.py @@ -10,4 +10,4 @@ def get_data(): 'items': ['Training Event'] }, ] - } \ No newline at end of file + } diff --git a/erpnext/hr/doctype/training_result/test_training_result.js b/erpnext/hr/doctype/training_result/test_training_result.js deleted file mode 100644 index cb1d7fb27a3..00000000000 --- a/erpnext/hr/doctype/training_result/test_training_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: Training Result", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Training Result - () => frappe.tests.make('Training Result', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/hr/doctype/training_result/training_result.js b/erpnext/hr/doctype/training_result/training_result.js index 62ac383ab78..5cdbcad8058 100644 --- a/erpnext/hr/doctype/training_result/training_result.js +++ b/erpnext/hr/doctype/training_result/training_result.js @@ -11,7 +11,7 @@ frappe.ui.form.on('Training Result', { }, training_event: function(frm) { - if (frm.doc.training_event && !frm.doc.docstatus && !frm.doc.employees) { + if (frm.doc.training_event && !frm.doc.docstatus && !frm.doc.employees) { frappe.call({ method: "erpnext.hr.doctype.training_result.training_result.get_employees", args: { diff --git a/erpnext/hr/doctype/training_result_employee/test_training_result.js b/erpnext/hr/doctype/training_result_employee/test_training_result.js index 2ebf8962ee2..3f397508357 100644 --- a/erpnext/hr/doctype/training_result_employee/test_training_result.js +++ b/erpnext/hr/doctype/training_result_employee/test_training_result.js @@ -50,4 +50,3 @@ QUnit.test("Test: Training Result [HR]", function (assert) { () => done() ]); }); - diff --git a/erpnext/hr/doctype/travel_request/test_travel_request.js b/erpnext/hr/doctype/travel_request/test_travel_request.js deleted file mode 100644 index 7e645918239..00000000000 --- a/erpnext/hr/doctype/travel_request/test_travel_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: Travel Request", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Travel Request - () => frappe.tests.make('Travel Request', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/hr/doctype/travel_request/travel_request.py b/erpnext/hr/doctype/travel_request/travel_request.py index 01d3f347061..60834d3f4a6 100644 --- a/erpnext/hr/doctype/travel_request/travel_request.py +++ b/erpnext/hr/doctype/travel_request/travel_request.py @@ -5,6 +5,8 @@ from __future__ import unicode_literals import frappe from frappe.model.document import Document +from erpnext.hr.utils import validate_active_employee class TravelRequest(Document): - pass + def validate(self): + validate_active_employee(self.employee) diff --git a/erpnext/hr/doctype/upload_attendance/upload_attendance.py b/erpnext/hr/doctype/upload_attendance/upload_attendance.py index 674c8e3eb45..9c765d73716 100644 --- a/erpnext/hr/doctype/upload_attendance/upload_attendance.py +++ b/erpnext/hr/doctype/upload_attendance/upload_attendance.py @@ -10,7 +10,7 @@ from frappe import _ from frappe.utils.csvutils import UnicodeWriter from frappe.model.document import Document from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee -from erpnext.hr.utils import get_holidays_for_employee +from erpnext.hr.utils import get_holiday_dates_for_employee class UploadAttendance(Document): pass @@ -94,7 +94,7 @@ def get_holidays_for_employees(employees, from_date, to_date): holidays = {} for employee in employees: holiday_list = get_holiday_list_for_employee(employee) - holiday = get_holidays_for_employee(employee, getdate(from_date), getdate(to_date)) + holiday = get_holiday_dates_for_employee(employee, getdate(from_date), getdate(to_date)) if holiday_list not in holidays: holidays[holiday_list] = holiday diff --git a/erpnext/hr/doctype/vehicle/test_vehicle.js b/erpnext/hr/doctype/vehicle/test_vehicle.js deleted file mode 100644 index 4d40cce086a..00000000000 --- a/erpnext/hr/doctype/vehicle/test_vehicle.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: Vehicle", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Vehicle - () => frappe.tests.make('Vehicle', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/hr/doctype/vehicle/vehicle_dashboard.py b/erpnext/hr/doctype/vehicle/vehicle_dashboard.py index 761c70182b2..628c8972cec 100644 --- a/erpnext/hr/doctype/vehicle/vehicle_dashboard.py +++ b/erpnext/hr/doctype/vehicle/vehicle_dashboard.py @@ -17,4 +17,4 @@ def get_data(): 'items': ['Delivery Trip'] } ] - } \ No newline at end of file + } diff --git a/erpnext/hr/doctype/vehicle_log/test_vehicle_log.py b/erpnext/hr/doctype/vehicle_log/test_vehicle_log.py index ed52c4e1222..ed02120cca3 100644 --- a/erpnext/hr/doctype/vehicle_log/test_vehicle_log.py +++ b/erpnext/hr/doctype/vehicle_log/test_vehicle_log.py @@ -115,4 +115,4 @@ def make_vehicle_log(license_plate, employee_id, with_services=False): vehicle_log.save() vehicle_log.submit() - return vehicle_log \ No newline at end of file + return vehicle_log diff --git a/erpnext/hr/doctype/vehicle_log/vehicle_log.js b/erpnext/hr/doctype/vehicle_log/vehicle_log.js index 6f3a0dc40eb..14fe9a02da2 100644 --- a/erpnext/hr/doctype/vehicle_log/vehicle_log.js +++ b/erpnext/hr/doctype/vehicle_log/vehicle_log.js @@ -24,4 +24,3 @@ frappe.ui.form.on("Vehicle Log", { }); } }); - diff --git a/erpnext/hr/notification/training_feedback/training_feedback.html b/erpnext/hr/notification/training_feedback/training_feedback.html index fd8fef9e82c..b49662a6eb9 100644 --- a/erpnext/hr/notification/training_feedback/training_feedback.html +++ b/erpnext/hr/notification/training_feedback/training_feedback.html @@ -3,4 +3,4 @@

You attended training {{ frappe.utils.get_link_to_form( "Training Event", doc.training_event) }}

-

{{ _("Please share your feedback to the training by clicking on 'Training Feedback' and then 'New'") }}

\ No newline at end of file +

{{ _("Please share your feedback to the training by clicking on 'Training Feedback' and then 'New'") }}

diff --git a/erpnext/hr/notification/training_scheduled/training_scheduled.html b/erpnext/hr/notification/training_scheduled/training_scheduled.html index 374038ac202..50f6d07a470 100644 --- a/erpnext/hr/notification/training_scheduled/training_scheduled.html +++ b/erpnext/hr/notification/training_scheduled/training_scheduled.html @@ -41,4 +41,4 @@ - \ No newline at end of file + diff --git a/erpnext/hr/page/organizational_chart/__init__.py b/erpnext/hr/page/organizational_chart/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/hr/page/organizational_chart/organizational_chart.js b/erpnext/hr/page/organizational_chart/organizational_chart.js new file mode 100644 index 00000000000..b6fcc6014bd --- /dev/null +++ b/erpnext/hr/page/organizational_chart/organizational_chart.js @@ -0,0 +1,21 @@ +frappe.pages['organizational-chart'].on_page_load = function(wrapper) { + frappe.ui.make_app_page({ + parent: wrapper, + title: __('Organizational Chart'), + single_column: true + }); + + $(wrapper).bind('show', () => { + frappe.require('/assets/js/hierarchy-chart.min.js', () => { + let organizational_chart = undefined; + let method = 'erpnext.hr.page.organizational_chart.organizational_chart.get_children'; + + if (frappe.is_mobile()) { + organizational_chart = new erpnext.HierarchyChartMobile('Employee', wrapper, method); + } else { + organizational_chart = new erpnext.HierarchyChart('Employee', wrapper, method); + } + organizational_chart.show(); + }); + }); +}; diff --git a/erpnext/hr/page/organizational_chart/organizational_chart.json b/erpnext/hr/page/organizational_chart/organizational_chart.json new file mode 100644 index 00000000000..d802781320a --- /dev/null +++ b/erpnext/hr/page/organizational_chart/organizational_chart.json @@ -0,0 +1,26 @@ +{ + "content": null, + "creation": "2021-05-25 10:53:10.107241", + "docstatus": 0, + "doctype": "Page", + "idx": 0, + "modified": "2021-05-25 10:53:18.201931", + "modified_by": "Administrator", + "module": "HR", + "name": "organizational-chart", + "owner": "Administrator", + "page_name": "Organizational Chart", + "roles": [ + { + "role": "HR User" + }, + { + "role": "HR Manager" + } + ], + "script": null, + "standard": "Yes", + "style": null, + "system_page": 0, + "title": "Organizational Chart" +} \ No newline at end of file diff --git a/erpnext/hr/page/organizational_chart/organizational_chart.py b/erpnext/hr/page/organizational_chart/organizational_chart.py new file mode 100644 index 00000000000..4423d29e402 --- /dev/null +++ b/erpnext/hr/page/organizational_chart/organizational_chart.py @@ -0,0 +1,48 @@ +from __future__ import unicode_literals +import frappe + +@frappe.whitelist() +def get_children(parent=None, company=None, exclude_node=None): + filters = [['status', '!=', 'Left']] + if company and company != 'All Companies': + filters.append(['company', '=', company]) + + if parent and company and parent != company: + filters.append(['reports_to', '=', parent]) + else: + filters.append(['reports_to', '=', '']) + + if exclude_node: + filters.append(['name', '!=', exclude_node]) + + employees = frappe.get_list('Employee', + fields=['employee_name as name', 'name as id', 'reports_to', 'image', 'designation as title'], + filters=filters, + order_by='name' + ) + + for employee in employees: + is_expandable = frappe.db.count('Employee', filters={'reports_to': employee.get('id')}) + employee.connections = get_connections(employee.id) + employee.expandable = 1 if is_expandable else 0 + + return employees + + +def get_connections(employee): + num_connections = 0 + + nodes_to_expand = frappe.get_list('Employee', filters=[ + ['reports_to', '=', employee] + ]) + num_connections += len(nodes_to_expand) + + while nodes_to_expand: + parent = nodes_to_expand.pop(0) + descendants = frappe.get_list('Employee', filters=[ + ['reports_to', '=', parent.name] + ]) + num_connections += len(descendants) + nodes_to_expand.extend(descendants) + + return num_connections diff --git a/erpnext/hr/page/team_updates/team_updates.py b/erpnext/hr/page/team_updates/team_updates.py index a6cf935985b..58cdc4b7e1d 100644 --- a/erpnext/hr/page/team_updates/team_updates.py +++ b/erpnext/hr/page/team_updates/team_updates.py @@ -17,4 +17,4 @@ def get_data(start=0): if d.text_content: d.content = frappe.utils.md_to_html(EmailReplyParser.parse_reply(d.text_content)) - return data \ No newline at end of file + return data diff --git a/erpnext/hr/print_format/job_offer/job_offer.json b/erpnext/hr/print_format/job_offer/job_offer.json index 3fc2bcf7d27..0a922306b46 100644 --- a/erpnext/hr/print_format/job_offer/job_offer.json +++ b/erpnext/hr/print_format/job_offer/job_offer.json @@ -15,7 +15,7 @@ "name": "Job Offer", "owner": "Administrator", "print_format_builder": 0, - "print_format_type": "Server", + "print_format_type": "Jinja", "show_section_headings": 0, "standard": "Yes" } \ No newline at end of file diff --git a/erpnext/hr/print_format/standard_appointment_letter/standard_appointment_letter.html b/erpnext/hr/print_format/standard_appointment_letter/standard_appointment_letter.html index d60582e1a18..87daafcaaee 100644 --- a/erpnext/hr/print_format/standard_appointment_letter/standard_appointment_letter.html +++ b/erpnext/hr/print_format/standard_appointment_letter/standard_appointment_letter.html @@ -35,4 +35,4 @@
________________
{{ doc.applicant_name }} -
\ No newline at end of file +
diff --git a/erpnext/hr/report/daily_work_summary_replies/daily_work_summary_replies.py b/erpnext/hr/report/daily_work_summary_replies/daily_work_summary_replies.py index aa8eea5d74e..d8691b4d025 100644 --- a/erpnext/hr/report/daily_work_summary_replies/daily_work_summary_replies.py +++ b/erpnext/hr/report/daily_work_summary_replies/daily_work_summary_replies.py @@ -54,4 +54,4 @@ def get_data(filters): user_name = frappe.get_value('User', user, 'full_name') count = len([d for d in replies if d.sender == user]) data.append([user_name, count, total]) - return data \ No newline at end of file + return data diff --git a/erpnext/hr/report/employee_advance_summary/employee_advance_summary.js b/erpnext/hr/report/employee_advance_summary/employee_advance_summary.js index 528ae4cea63..8de4af5d4fc 100644 --- a/erpnext/hr/report/employee_advance_summary/employee_advance_summary.js +++ b/erpnext/hr/report/employee_advance_summary/employee_advance_summary.js @@ -38,4 +38,3 @@ frappe.query_reports["Employee Advance Summary"] = { } ] }; - diff --git a/erpnext/hr/report/employee_analytics/employee_analytics.py b/erpnext/hr/report/employee_analytics/employee_analytics.py index 8f393889269..fe77b6abc96 100644 --- a/erpnext/hr/report/employee_analytics/employee_analytics.py +++ b/erpnext/hr/report/employee_analytics/employee_analytics.py @@ -81,4 +81,3 @@ def get_chart_data(parameters,employees, filters): } chart["type"] = "donut" return chart - diff --git a/erpnext/hr/report/employee_birthday/employee_birthday.js b/erpnext/hr/report/employee_birthday/employee_birthday.js index 60b69b409a0..bbe4a8d179e 100644 --- a/erpnext/hr/report/employee_birthday/employee_birthday.js +++ b/erpnext/hr/report/employee_birthday/employee_birthday.js @@ -8,7 +8,7 @@ frappe.query_reports["Employee Birthday"] = { "label": __("Month"), "fieldtype": "Select", "options": "Jan\nFeb\nMar\nApr\nMay\nJun\nJul\nAug\nSep\nOct\nNov\nDec", - "default": ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", + "default": ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"][frappe.datetime.str_to_obj(frappe.datetime.get_today()).getMonth()], }, { @@ -19,4 +19,4 @@ frappe.query_reports["Employee Birthday"] = { "default": frappe.defaults.get_user_default("Company") } ] -} \ No newline at end of file +} diff --git a/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.py b/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.py index 92715d34453..9dd7bcd8dd9 100644 --- a/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.py +++ b/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.py @@ -72,4 +72,4 @@ def get_data(filters, leave_types): data.append(row) - return data \ No newline at end of file + return data diff --git a/erpnext/hr/report/recruitment_analytics/recruitment_analytics.js b/erpnext/hr/report/recruitment_analytics/recruitment_analytics.js index 9620f520002..51dc7ff85b8 100644 --- a/erpnext/hr/report/recruitment_analytics/recruitment_analytics.js +++ b/erpnext/hr/report/recruitment_analytics/recruitment_analytics.js @@ -20,4 +20,4 @@ frappe.query_reports["Recruitment Analytics"] = { "reqd": 1, }, ] -}; \ No newline at end of file +}; diff --git a/erpnext/hr/report/vehicle_expenses/vehicle_expenses.js b/erpnext/hr/report/vehicle_expenses/vehicle_expenses.js index 879acd18ef4..2d0aa0f36d3 100644 --- a/erpnext/hr/report/vehicle_expenses/vehicle_expenses.js +++ b/erpnext/hr/report/vehicle_expenses/vehicle_expenses.js @@ -49,4 +49,3 @@ frappe.query_reports["Vehicle Expenses"] = { } ] }; - diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py index ebb17343471..9c936ab4ad0 100644 --- a/erpnext/hr/utils.py +++ b/erpnext/hr/utils.py @@ -3,13 +3,12 @@ import erpnext import frappe -from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee +from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee, InactiveEmployeeStatusError from frappe import _ from frappe.desk.form import assign_to from frappe.model.document import Document from frappe.utils import (add_days, cstr, flt, format_datetime, formatdate, - get_datetime, getdate, nowdate, today, unique) - + get_datetime, getdate, nowdate, today, unique, get_link_to_form) class DuplicateDeclarationError(frappe.ValidationError): pass @@ -20,6 +19,7 @@ class EmployeeBoardingController(Document): Assign to the concerned person and roles as per the onboarding/separation template ''' def validate(self): + validate_active_employee(self.employee) # remove the task if linked before submitting the form if self.amended_from: for activity in self.activities: @@ -448,21 +448,44 @@ def get_sal_slip_total_benefit_given(employee, payroll_period, component=False): total_given_benefit_amount = sum_of_given_benefit[0].total_amount return total_given_benefit_amount -def get_holidays_for_employee(employee, start_date, end_date): - holiday_list = get_holiday_list_for_employee(employee) +def get_holiday_dates_for_employee(employee, start_date, end_date): + """return a list of holiday dates for the given employee between start_date and end_date""" + # return only date + holidays = get_holidays_for_employee(employee, start_date, end_date) + + return [cstr(h.holiday_date) for h in holidays] - holidays = frappe.db.sql_list('''select holiday_date from `tabHoliday` - where - parent=%(holiday_list)s - and holiday_date >= %(start_date)s - and holiday_date <= %(end_date)s''', { - "holiday_list": holiday_list, - "start_date": start_date, - "end_date": end_date - }) - holidays = [cstr(i) for i in holidays] +def get_holidays_for_employee(employee, start_date, end_date, raise_exception=True, only_non_weekly=False): + """Get Holidays for a given employee + `employee` (str) + `start_date` (str or datetime) + `end_date` (str or datetime) + `raise_exception` (bool) + `only_non_weekly` (bool) + + return: list of dicts with `holiday_date` and `description` + """ + holiday_list = get_holiday_list_for_employee(employee, raise_exception=raise_exception) + + if not holiday_list: + return [] + + filters = { + 'parent': holiday_list, + 'holiday_date': ('between', [start_date, end_date]) + } + + if only_non_weekly: + filters['weekly_off'] = False + + holidays = frappe.get_all( + 'Holiday', + fields=['description', 'holiday_date'], + filters=filters + ) + return holidays @erpnext.allow_regional @@ -522,3 +545,8 @@ def share_doc_with_approver(doc, user): approver = approvers.get(doc.doctype) if doc_before_save.get(approver) != doc.get(approver): frappe.share.remove(doc.doctype, doc.name, doc_before_save.get(approver)) + +def validate_active_employee(employee): + if frappe.db.get_value("Employee", employee, "status") == "Inactive": + frappe.throw(_("Transactions cannot be created for an Inactive Employee {0}.").format( + get_link_to_form("Employee", employee)), InactiveEmployeeStatusError) diff --git a/erpnext/hr/web_form/job_application/job_application.js b/erpnext/hr/web_form/job_application/job_application.js index 699703c5792..ffc5e984253 100644 --- a/erpnext/hr/web_form/job_application/job_application.js +++ b/erpnext/hr/web_form/job_application/job_application.js @@ -1,3 +1,3 @@ frappe.ready(function() { // bind events here -}) \ No newline at end of file +}) diff --git a/erpnext/hub_node/doctype/marketplace_settings/test_marketplace_settings.js b/erpnext/hub_node/doctype/marketplace_settings/test_marketplace_settings.js deleted file mode 100644 index fba3e098d4b..00000000000 --- a/erpnext/hub_node/doctype/marketplace_settings/test_marketplace_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: Marketplace Settings", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Marketplace Settings - () => frappe.tests.make('Marketplace Settings', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/top_10_pledged_loan_securities.js b/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/top_10_pledged_loan_securities.js index cf75cc8e41a..58179416b1a 100644 --- a/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/top_10_pledged_loan_securities.js +++ b/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/top_10_pledged_loan_securities.js @@ -11,4 +11,4 @@ frappe.dashboards.chart_sources["Top 10 Pledged Loan Securities"] = { default: frappe.defaults.get_user_default("Company") } ] -}; \ No newline at end of file +}; diff --git a/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/top_10_pledged_loan_securities.py b/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/top_10_pledged_loan_securities.py index 6bb04401bed..6ce2a54b190 100644 --- a/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/top_10_pledged_loan_securities.py +++ b/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/top_10_pledged_loan_securities.py @@ -73,4 +73,4 @@ def get_data(chart_name = None, chart = None, no_cache = None, filters = None, f 'chartType': 'bar', 'values': values }] - } \ No newline at end of file + } diff --git a/erpnext/loan_management/doctype/loan/loan.js b/erpnext/loan_management/doctype/loan/loan.js index 28af3a9c415..f9c201ab603 100644 --- a/erpnext/loan_management/doctype/loan/loan.js +++ b/erpnext/loan_management/doctype/loan/loan.js @@ -28,7 +28,8 @@ frappe.ui.form.on('Loan', { frm.set_query("loan_type", function () { return { "filters": { - "docstatus": 1 + "docstatus": 1, + "company": frm.doc.company } }; }); diff --git a/erpnext/loan_management/doctype/loan/loan_dashboard.py b/erpnext/loan_management/doctype/loan/loan_dashboard.py index 7a8190f7450..711a7829baf 100644 --- a/erpnext/loan_management/doctype/loan/loan_dashboard.py +++ b/erpnext/loan_management/doctype/loan/loan_dashboard.py @@ -16,4 +16,4 @@ def get_data(): 'items': ['Loan Repayment', 'Loan Interest Accrual', 'Loan Write Off', 'Loan Security Unpledge'] } ] - } \ No newline at end of file + } diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py index 314f58dd15e..122d7236051 100644 --- a/erpnext/loan_management/doctype/loan/test_loan.py +++ b/erpnext/loan_management/doctype/loan/test_loan.py @@ -988,4 +988,4 @@ def create_demand_loan(applicant, loan_type, loan_application, posting_date=None loan.save() - return loan \ No newline at end of file + return loan diff --git a/erpnext/loan_management/doctype/loan_application/loan_application.js b/erpnext/loan_management/doctype/loan_application/loan_application.js index 13652749711..eccbdc3e919 100644 --- a/erpnext/loan_management/doctype/loan_application/loan_application.js +++ b/erpnext/loan_management/doctype/loan_application/loan_application.js @@ -14,6 +14,13 @@ frappe.ui.form.on('Loan Application', { refresh: function(frm) { frm.trigger("toggle_fields"); frm.trigger("add_toolbar_buttons"); + frm.set_query('loan_type', () => { + return { + filters: { + company: frm.doc.company + } + }; + }); }, repayment_method: function(frm) { frm.doc.repayment_amount = frm.doc.repayment_periods = "" diff --git a/erpnext/loan_management/doctype/loan_application/loan_application_dashboard.py b/erpnext/loan_management/doctype/loan_application/loan_application_dashboard.py index bf3f58b83ef..3975adf4431 100644 --- a/erpnext/loan_management/doctype/loan_application/loan_application_dashboard.py +++ b/erpnext/loan_management/doctype/loan_application/loan_application_dashboard.py @@ -9,4 +9,4 @@ def get_data(): 'items': ['Loan', 'Loan Security Pledge'] }, ], - } \ No newline at end of file + } diff --git a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py index f341e81065f..f113c10ef71 100644 --- a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py +++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py @@ -203,5 +203,3 @@ def get_disbursal_amount(loan, on_current_security_price=0): disbursal_amount = loan_details.loan_amount - loan_details.disbursed_amount return disbursal_amount - - diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py index 7978350adf8..d75213ce78d 100644 --- a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py +++ b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py @@ -247,4 +247,3 @@ def get_per_day_interest(principal_amount, rate_of_interest, posting_date=None): posting_date = getdate() return flt((principal_amount * rate_of_interest) / (days_in_year(get_datetime(posting_date).year) * 100)) - diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index b8b1a40b5fd..b67b4c4842b 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -107,12 +107,13 @@ class LoanRepayment(AccountsController): lia = frappe.db.get_value('Loan Interest Accrual', {'process_loan_interest_accrual': process}, ['name', 'interest_amount', 'payable_principal_amount'], as_dict=1) - self.append('repayment_details', { - 'loan_interest_accrual': lia.name, - 'paid_interest_amount': flt(self.total_interest_paid - self.interest_payable, precision), - 'paid_principal_amount': 0.0, - 'accrual_type': 'Repayment' - }) + if lia: + self.append('repayment_details', { + 'loan_interest_accrual': lia.name, + 'paid_interest_amount': flt(self.total_interest_paid - self.interest_payable, precision), + 'paid_principal_amount': 0.0, + 'accrual_type': 'Repayment' + }) def update_paid_amount(self): loan = frappe.get_doc("Loan", self.against_loan) @@ -455,6 +456,3 @@ def calculate_amounts(against_loan, posting_date, payment_type=''): amounts['payable_amount'] = amounts['payable_principal_amount'] + amounts['interest_amount'] return amounts - - - diff --git a/erpnext/loan_management/doctype/loan_security/loan_security_dashboard.py b/erpnext/loan_management/doctype/loan_security/loan_security_dashboard.py index 878b3fd051e..3eec5660ac1 100644 --- a/erpnext/loan_management/doctype/loan_security/loan_security_dashboard.py +++ b/erpnext/loan_management/doctype/loan_security/loan_security_dashboard.py @@ -12,4 +12,4 @@ def get_data(): 'items': ['Loan Security Pledge', 'Loan Security Unpledge'] } ] - } \ No newline at end of file + } diff --git a/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.js b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.js index 11c932ff1c1..48ca392edf7 100644 --- a/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.js +++ b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.js @@ -40,4 +40,4 @@ frappe.ui.form.on("Pledge", { qty: function(frm, cdt, cdn) { frm.events.calculate_amounts(frm, cdt, cdn); }, -}); \ No newline at end of file +}); diff --git a/erpnext/loan_management/doctype/loan_security_price/loan_security_price.py b/erpnext/loan_management/doctype/loan_security_price/loan_security_price.py index 32d81afed5d..9fc1fda53f4 100644 --- a/erpnext/loan_management/doctype/loan_security_price/loan_security_price.py +++ b/erpnext/loan_management/doctype/loan_security_price/loan_security_price.py @@ -40,12 +40,3 @@ def get_loan_security_price(loan_security, valid_time=None): frappe.throw(_("No valid Loan Security Price found for {0}").format(frappe.bold(loan_security))) else: return loan_security_price - - - - - - - - - diff --git a/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py index 8233b7b297a..61f17780481 100644 --- a/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py +++ b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py @@ -71,7 +71,7 @@ def check_for_ltv_shortfall(process_loan_security_shortfall): - flt(loan.total_principal_paid) pledged_securities = get_pledged_security_qty(loan.name) - ltv_ratio = '' + ltv_ratio = 0.0 security_value = 0.0 for security, qty in pledged_securities.items(): @@ -122,4 +122,3 @@ def update_pending_shortfall(shortfall): "shortfall_amount": 0, "shortfall_percentage": 0 }) - diff --git a/erpnext/loan_management/doctype/loan_security_type/loan_security_type_dashboard.py b/erpnext/loan_management/doctype/loan_security_type/loan_security_type_dashboard.py index ac33589b549..17de8c1da4d 100644 --- a/erpnext/loan_management/doctype/loan_security_type/loan_security_type_dashboard.py +++ b/erpnext/loan_management/doctype/loan_security_type/loan_security_type_dashboard.py @@ -12,4 +12,4 @@ def get_data(): 'items': ['Loan Security Pledge', 'Loan Security Unpledge'] } ] - } \ No newline at end of file + } diff --git a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py index b24dc2f7c28..4f936dd7c11 100644 --- a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py +++ b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py @@ -147,8 +147,3 @@ def get_pledged_security_qty(loan): current_pledges[security] -= unpledges.get(security, 0.0) return current_pledges - - - - - diff --git a/erpnext/loan_management/doctype/loan_type/loan_type.py b/erpnext/loan_management/doctype/loan_type/loan_type.py index 208cb19c88e..50ef930dbbe 100644 --- a/erpnext/loan_management/doctype/loan_type/loan_type.py +++ b/erpnext/loan_management/doctype/loan_type/loan_type.py @@ -21,4 +21,3 @@ class LoanType(Document): if self.get('loan_account') == self.get('payment_account'): frappe.throw(_('Loan Account and Payment Account cannot be same')) - diff --git a/erpnext/loan_management/doctype/loan_type/loan_type_dashboard.py b/erpnext/loan_management/doctype/loan_type/loan_type_dashboard.py index 58c668948c2..95d97fdf9b0 100644 --- a/erpnext/loan_management/doctype/loan_type/loan_type_dashboard.py +++ b/erpnext/loan_management/doctype/loan_type/loan_type_dashboard.py @@ -12,4 +12,4 @@ def get_data(): 'items': ['Loan Application'] } ] - } \ No newline at end of file + } diff --git a/erpnext/loan_management/doctype/loan_write_off/loan_write_off.py b/erpnext/loan_management/doctype/loan_write_off/loan_write_off.py index 54a3f2cbb1f..676df701cc3 100644 --- a/erpnext/loan_management/doctype/loan_write_off/loan_write_off.py +++ b/erpnext/loan_management/doctype/loan_write_off/loan_write_off.py @@ -84,5 +84,3 @@ class LoanWriteOff(AccountsController): ) make_gl_entries(gl_entries, cancel=cancel, merge_entries=False) - - diff --git a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.py b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.py index 11333dc2aaf..8c67c0affee 100644 --- a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.py +++ b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.py @@ -61,4 +61,3 @@ def term_loan_accrual_pending(date): }) return pending_accrual - diff --git a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual_dashboard.py b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual_dashboard.py index 243a7a3ba66..e104c6646b0 100644 --- a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual_dashboard.py +++ b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual_dashboard.py @@ -9,4 +9,4 @@ def get_data(): 'items': ['Loan Interest Accrual'] } ] - } \ No newline at end of file + } diff --git a/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall_dashboard.py b/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall_dashboard.py index dc9bd81a1dd..e67e4d4738f 100644 --- a/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall_dashboard.py +++ b/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall_dashboard.py @@ -9,4 +9,4 @@ def get_data(): 'items': ['Loan Security Shortfall'] } ] - } \ No newline at end of file + } diff --git a/erpnext/loan_management/loan_common.js b/erpnext/loan_management/loan_common.js index 50b68da30e3..43980ffef48 100644 --- a/erpnext/loan_management/loan_common.js +++ b/erpnext/loan_management/loan_common.js @@ -40,4 +40,4 @@ frappe.ui.form.on(cur_frm.doctype, { frm.set_value("applicant_name", null); } } -}); \ No newline at end of file +}); diff --git a/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.py b/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.py index 0ccd149e5fb..f2cbbb469f0 100644 --- a/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.py +++ b/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.py @@ -136,4 +136,4 @@ def get_applicant_wise_total_loan_security_qty(filters, loan_security_details): total_value_map[security.applicant] += current_pledges.get((security.applicant, security.loan_security)) \ * loan_security_details.get(security.loan_security, {}).get('latest_price', 0) - return current_pledges, total_value_map, applicant_type_map \ No newline at end of file + return current_pledges, total_value_map, applicant_type_map diff --git a/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py index 2a74a1eb858..a505e72c4d9 100644 --- a/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py +++ b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py @@ -182,4 +182,4 @@ def get_loan_wise_security_value(filters, current_pledges): loan_wise_security_value[key[0]] += \ flt(qty * loan_security_details.get(key[1], {}).get('latest_price', 0)) - return loan_wise_security_value \ No newline at end of file + return loan_wise_security_value diff --git a/erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.py b/erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.py index c6f6b990cc5..65910770881 100644 --- a/erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.py +++ b/erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.py @@ -126,4 +126,4 @@ def get_data(filters): data.append(row) - return data \ No newline at end of file + return data diff --git a/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py b/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py index 887a86a46c5..34bbe5a4503 100644 --- a/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py +++ b/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py @@ -79,6 +79,3 @@ def get_company_wise_loan_security_details(filters, loan_security_details): total_portfolio_value += flt(qty * loan_security_details.get(key[1], {}).get('latest_price', 0)) return security_wise_map, total_portfolio_value - - - diff --git a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.js b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.js index 44712d543b7..0868187e09e 100644 --- a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.js +++ b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.js @@ -69,10 +69,10 @@ erpnext.maintenance.MaintenanceSchedule = frappe.ui.form.Controller.extend({ if (flag) { this.frm.add_custom_button(__('Maintenance Visit'), function () { let options = ""; - + me.frm.call('get_pending_data', {data_type: "items"}).then(r => { options = r.message; - + let schedule_id = ""; let d = new frappe.ui.Dialog({ title: __("Enter Visit Details"), @@ -86,7 +86,7 @@ erpnext.maintenance.MaintenanceSchedule = frappe.ui.form.Controller.extend({ let field = d.get_field("scheduled_date"); me.frm.call('get_pending_data', { - item_name: this.value, + item_name: this.value, data_type: "date" }).then(r => { field.df.options = r.message; @@ -161,10 +161,9 @@ erpnext.maintenance.MaintenanceSchedule = frappe.ui.form.Controller.extend({ let me = this; if (item.start_date && item.periodicity) { me.frm.call('validate_end_date_visits'); - + } }, }); $.extend(cur_frm.cscript, new erpnext.maintenance.MaintenanceSchedule({ frm: cur_frm })); - diff --git a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py index d6e42f3ee1c..97289032d70 100644 --- a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py +++ b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py @@ -52,15 +52,15 @@ class MaintenanceSchedule(TransactionBase): item.end_date = add_days(item.start_date, item.no_of_visits * days_in_period[item.periodicity]) else: item.end_date = add_days(item.start_date, days_in_period[item.periodicity]) - + diff = date_diff(item.end_date, item.start_date) + 1 no_of_visits = cint(diff / days_in_period[item.periodicity]) - + if not item.no_of_visits or item.no_of_visits == 0: item.end_date = add_days(item.start_date, days_in_period[item.periodicity]) diff = date_diff(item.end_date, item.start_date) + 1 item.no_of_visits = cint(diff / days_in_period[item.periodicity]) - + elif item.no_of_visits > no_of_visits: item.end_date = add_days(item.start_date, item.no_of_visits * days_in_period[item.periodicity]) @@ -207,7 +207,7 @@ class MaintenanceSchedule(TransactionBase): def on_update(self): frappe.db.set(self, 'status', 'Draft') - + def update_amc_date(self, serial_nos, amc_expiry_date=None): for serial_no in serial_nos: serial_no_doc = frappe.get_doc("Serial No", serial_no) @@ -300,7 +300,7 @@ class MaintenanceSchedule(TransactionBase): for schedule in self.schedules: if schedule.item_name == item_name and s_date == formatdate(schedule.scheduled_date, "dd-mm-yyyy"): return schedule.name - + @frappe.whitelist() def update_serial_nos(s_id): serial_nos = frappe.db.get_value('Maintenance Schedule Detail', s_id, 'serial_no') @@ -318,12 +318,12 @@ def make_maintenance_visit(source_name, target_doc=None, item_name=None, s_id=No target.maintenance_type = "Scheduled" target.maintenance_schedule = source.name target.maintenance_schedule_detail = s_id - + def update_sales(source, target, parent): sales_person = frappe.db.get_value('Maintenance Schedule Detail', s_id, 'sales_person') target.service_person = sales_person target.serial_no = '' - + doclist = get_mapped_doc("Maintenance Schedule", source_name, { "Maintenance Schedule": { "doctype": "Maintenance Visit", diff --git a/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.js b/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.js deleted file mode 100644 index e0f05b1abbd..00000000000 --- a/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_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: Maintenance Schedule", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Maintenance Schedule - () => frappe.tests.make('Maintenance Schedule', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py b/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py index 09981bad05f..c733dd0c92c 100644 --- a/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py +++ b/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py @@ -22,7 +22,7 @@ class TestMaintenanceSchedule(unittest.TestCase): ms.cancel() events_after_cancel = get_events(ms) self.assertTrue(len(events_after_cancel) == 0) - + def test_make_schedule(self): ms = make_maintenance_schedule() ms.save() @@ -72,7 +72,7 @@ class TestMaintenanceSchedule(unittest.TestCase): #checks if visit status is back updated in schedule self.assertTrue(ms.schedules[1].completion_status, "Partially Completed") - + def get_events(ms): return frappe.get_all("Event Participants", filters={ "reference_doctype": ms.doctype, diff --git a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js index d6105c657ef..9a90f49d855 100644 --- a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js +++ b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js @@ -73,12 +73,16 @@ erpnext.maintenance.MaintenanceVisit = frappe.ui.form.Controller.extend({ if (this.frm.doc.docstatus === 0) { this.frm.add_custom_button(__('Maintenance Schedule'), function () { + if (!me.frm.doc.customer) { + frappe.msgprint(__('Please select Customer first')); + return; + } erpnext.utils.map_current_doc({ method: "erpnext.maintenance.doctype.maintenance_schedule.maintenance_schedule.make_maintenance_visit", source_doctype: "Maintenance Schedule", target: me.frm, setters: { - customer: me.frm.doc.customer || undefined, + customer: me.frm.doc.customer, }, get_query_filters: { docstatus: 1, @@ -104,12 +108,16 @@ erpnext.maintenance.MaintenanceVisit = frappe.ui.form.Controller.extend({ }, __("Get Items From")); this.frm.add_custom_button(__('Sales Order'), function () { + if (!me.frm.doc.customer) { + frappe.msgprint(__('Please select Customer first')); + return; + } erpnext.utils.map_current_doc({ method: "erpnext.selling.doctype.sales_order.sales_order.make_maintenance_visit", source_doctype: "Sales Order", target: me.frm, setters: { - customer: me.frm.doc.customer || undefined, + customer: me.frm.doc.customer, }, get_query_filters: { docstatus: 1, @@ -122,4 +130,4 @@ erpnext.maintenance.MaintenanceVisit = frappe.ui.form.Controller.extend({ }, }); -$.extend(cur_frm.cscript, new erpnext.maintenance.MaintenanceVisit({ frm: cur_frm })); \ No newline at end of file +$.extend(cur_frm.cscript, new erpnext.maintenance.MaintenanceVisit({ frm: cur_frm })); diff --git a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.py b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.py index 7fffc942a03..d63c7003870 100644 --- a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.py +++ b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.py @@ -28,11 +28,11 @@ class MaintenanceVisit(TransactionBase): def validate(self): self.validate_serial_no() self.validate_maintenance_date() - + def update_completion_status(self): if self.maintenance_schedule_detail: frappe.db.set_value('Maintenance Schedule Detail', self.maintenance_schedule_detail, 'completion_status', self.completion_status) - + def update_actual_date(self): if self.maintenance_schedule_detail: frappe.db.set_value('Maintenance Schedule Detail', self.maintenance_schedule_detail, 'actual_date', self.mntc_date) diff --git a/erpnext/manufacturing/doctype/blanket_order/blanket_order.js b/erpnext/manufacturing/doctype/blanket_order/blanket_order.js index f19a1b08681..d3bb33e86e0 100644 --- a/erpnext/manufacturing/doctype/blanket_order/blanket_order.js +++ b/erpnext/manufacturing/doctype/blanket_order/blanket_order.js @@ -85,5 +85,3 @@ frappe.ui.form.on('Blanket Order', { frm.trigger('set_tc_name_filter'); } }); - - diff --git a/erpnext/manufacturing/doctype/blanket_order/blanket_order.py b/erpnext/manufacturing/doctype/blanket_order/blanket_order.py index d7556add805..1aedb1e590f 100644 --- a/erpnext/manufacturing/doctype/blanket_order/blanket_order.py +++ b/erpnext/manufacturing/doctype/blanket_order/blanket_order.py @@ -76,4 +76,4 @@ def make_order(source_name): "postprocess": update_item } }) - return target_doc \ No newline at end of file + return target_doc diff --git a/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.js b/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.js deleted file mode 100644 index 51a0d948419..00000000000 --- a/erpnext/manufacturing/doctype/blanket_order/test_blanket_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: Blanket Order", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Blanket Order - () => frappe.tests.make('Blanket Order', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py b/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py index 3171defdaea..9a0a72fb475 100644 --- a/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py +++ b/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py @@ -88,4 +88,4 @@ def make_blanket_order(**args): bo.insert() bo.submit() - return bo \ No newline at end of file + return bo diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index 15a7c316c91..ef074052620 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -83,7 +83,7 @@ frappe.ui.form.on("BOM", { if (!frm.doc.__islocal && frm.doc.docstatus<2) { frm.add_custom_button(__("Update Cost"), function() { - frm.events.update_cost(frm); + frm.events.update_cost(frm, true); }); frm.add_custom_button(__("Browse BOM"), function() { frappe.route_options = { @@ -235,7 +235,7 @@ frappe.ui.form.on("BOM", { reqd: 1, }, { - fieldname: "varint_item_code", + fieldname: "variant_item_code", options: "Item", label: __("Variant Item"), fieldtype: "Link", @@ -287,7 +287,7 @@ frappe.ui.form.on("BOM", { let variant_items = data.items || []; variant_items.forEach(d => { - if (!d.varint_item_code) { + if (!d.variant_item_code) { frappe.throw(__("Select variant item code for the template item {0}", [d.item_code])); } }) @@ -299,7 +299,7 @@ frappe.ui.form.on("BOM", { has_template_rm.forEach(d => { dialog.fields_dict.items.df.data.push({ "item_code": d.item_code, - "varint_item_code": "", + "variant_item_code": "", "qty": d.qty, "source_warehouse": d.source_warehouse, "operation": d.operation @@ -318,14 +318,15 @@ frappe.ui.form.on("BOM", { }) }, - update_cost: function(frm) { + update_cost: function(frm, save_doc=false) { return frappe.call({ doc: frm.doc, method: "update_cost", freeze: true, args: { update_parent: true, - from_child_bom:false + save: save_doc, + from_child_bom: false }, callback: function(r) { refresh_field("items"); @@ -445,6 +446,11 @@ var get_bom_material_detail = function(doc, cdt, cdn, scrap_items) { }, callback: function(r) { d = locals[cdt][cdn]; + if (d.is_process_loss) { + r.message.rate = 0; + r.message.base_rate = 0; + } + $.extend(d, r.message); refresh_field("items"); refresh_field("scrap_items"); @@ -653,4 +659,36 @@ frappe.ui.form.on("BOM", "with_operations", function(frm) { if(!cint(frm.doc.with_operations)) { frm.set_value("operations", []); } -}); \ No newline at end of file +}); + +frappe.ui.form.on("BOM Scrap Item", { + item_code(frm, cdt, cdn) { + const { item_code } = locals[cdt][cdn]; + if (item_code === frm.doc.item) { + locals[cdt][cdn].is_process_loss = 1; + trigger_process_loss_qty_prompt(frm, cdt, cdn, item_code); + } + }, +}); + +function trigger_process_loss_qty_prompt(frm, cdt, cdn, item_code) { + frappe.prompt( + { + fieldname: "percent", + fieldtype: "Percent", + label: __("% Finished Item Quantity"), + description: + __("Set quantity of process loss item:") + + ` ${item_code} ` + + __("as a percentage of finished item quantity"), + }, + (data) => { + const row = locals[cdt][cdn]; + row.stock_qty = (frm.doc.quantity * data.percent) / 100; + row.qty = row.stock_qty / (row.conversion_factor || 1); + refresh_field("scrap_items"); + }, + __("Set Process Loss Item Quantity"), + __("Set Quantity") + ); +} diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index c32a8a95a17..47d94fbce31 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -148,14 +148,17 @@ class BOM(WebsiteGenerator): self.set_plc_conversion_rate() self.validate_uom_is_interger() self.set_bom_material_details() + self.set_bom_scrap_items_detail() self.validate_materials() self.set_routing_operations() self.validate_operations() self.calculate_cost() self.update_stock_qty() + self.validate_scrap_items() self.update_cost(update_parent=False, from_child_bom=True, update_hour_rate = False, save=False) self.set_bom_level() + def get_context(self, context): context.parents = [{'name': 'boms', 'title': _('All BOMs') }] @@ -200,7 +203,7 @@ class BOM(WebsiteGenerator): def set_bom_material_details(self): for item in self.get("items"): - self.validate_bom_currecny(item) + self.validate_bom_currency(item) ret = self.get_bom_material_detail({ "company": self.company, @@ -219,6 +222,19 @@ class BOM(WebsiteGenerator): if not item.get(r): item.set(r, ret[r]) + def set_bom_scrap_items_detail(self): + for item in self.get("scrap_items"): + args = { + "item_code": item.item_code, + "company": self.company, + "scrap_items": True, + "bom_no": '', + } + ret = self.get_bom_material_detail(args) + for key, value in ret.items(): + if item.get(key) is None: + item.set(key, value) + @frappe.whitelist() def get_bom_material_detail(self, args=None): """ Get raw material details like uom, desc and rate""" @@ -255,7 +271,7 @@ class BOM(WebsiteGenerator): return ret_item - def validate_bom_currecny(self, item): + def validate_bom_currency(self, item): if item.get('bom_no') and frappe.db.get_value('BOM', item.get('bom_no'), 'currency') != self.currency: frappe.throw(_("Row {0}: Currency of the BOM #{1} should be equal to the selected currency {2}") .format(item.idx, item.bom_no, self.currency)) @@ -330,7 +346,7 @@ class BOM(WebsiteGenerator): frappe.get_doc("BOM", bom).update_cost(from_child_bom=True) if not from_child_bom: - frappe.msgprint(_("Cost Updated")) + frappe.msgprint(_("Cost Updated"), alert=True) def update_parent_cost(self): if self.total_cost: @@ -432,25 +448,29 @@ class BOM(WebsiteGenerator): frappe.throw(_("Quantity required for Item {0} in row {1}").format(m.item_code, m.idx)) check_list.append(m) - def check_recursion(self, bom_list=[]): + def check_recursion(self, bom_list=None): """ Check whether recursion occurs in any bom""" + def _throw_error(bom_name): + frappe.throw(_("BOM recursion: {0} cannot be parent or child of {0}").format(bom_name)) + bom_list = self.traverse_tree() - bom_nos = frappe.get_all('BOM Item', fields=["bom_no"], - filters={'parent': ('in', bom_list), 'parenttype': 'BOM'}) + child_items = frappe.get_all('BOM Item', fields=["bom_no", "item_code"], + filters={'parent': ('in', bom_list), 'parenttype': 'BOM'}) or [] - raise_exception = False - if bom_nos and self.name in [d.bom_no for d in bom_nos]: - raise_exception = True + child_bom = {d.bom_no for d in child_items} + child_items_codes = {d.item_code for d in child_items} - if not raise_exception: - bom_nos = frappe.get_all('BOM Item', fields=["parent"], - filters={'bom_no': self.name, 'parenttype': 'BOM'}) + if self.name in child_bom: + _throw_error(self.name) - if self.name in [d.parent for d in bom_nos]: - raise_exception = True + if self.item in child_items_codes: + _throw_error(self.item) - if raise_exception: - frappe.throw(_("BOM recursion: {0} cannot be parent or child of {1}").format(self.name, self.name)) + bom_nos = frappe.get_all('BOM Item', fields=["parent"], + filters={'bom_no': self.name, 'parenttype': 'BOM'}) or [] + + if self.name in {d.parent for d in bom_nos}: + _throw_error(self.name) def traverse_tree(self, bom_list=None): def _get_children(bom_no): @@ -498,17 +518,21 @@ class BOM(WebsiteGenerator): def update_rate_and_time(self, row, update_hour_rate = False): if not row.hour_rate or update_hour_rate: hour_rate = flt(frappe.get_cached_value("Workstation", row.workstation, "hour_rate")) - row.hour_rate = (hour_rate / flt(self.conversion_rate) - if self.conversion_rate and hour_rate else hour_rate) + + if hour_rate: + row.hour_rate = (hour_rate / flt(self.conversion_rate) + if self.conversion_rate and hour_rate else hour_rate) if self.routing: - row.time_in_mins = flt(frappe.db.get_value("BOM Operation", { + time_in_mins = flt(frappe.db.get_value("BOM Operation", { "workstation": row.workstation, "operation": row.operation, - "sequence_id": row.sequence_id, "parent": self.routing }, ["time_in_mins"])) + if time_in_mins: + row.time_in_mins = time_in_mins + if row.hour_rate and row.time_in_mins: row.base_hour_rate = flt(row.hour_rate) * flt(self.conversion_rate) row.operating_cost = flt(row.hour_rate) * flt(row.time_in_mins) / 60.0 @@ -673,6 +697,33 @@ class BOM(WebsiteGenerator): if not d.batch_size or d.batch_size <= 0: d.batch_size = 1 + + def validate_scrap_items(self): + for item in self.scrap_items: + msg = "" + if item.item_code == self.item and not item.is_process_loss: + msg = _('Scrap/Loss Item: {0} should have Is Process Loss checked as it is the same as the item to be manufactured or repacked.') \ + .format(frappe.bold(item.item_code)) + elif item.item_code != self.item and item.is_process_loss: + msg = _('Scrap/Loss Item: {0} should not have Is Process Loss checked as it is different from the item to be manufactured or repacked') \ + .format(frappe.bold(item.item_code)) + + must_be_whole_number = frappe.get_value("UOM", item.stock_uom, "must_be_whole_number") + if item.is_process_loss and must_be_whole_number: + msg = _("Item: {0} with Stock UOM: {1} cannot be a Scrap/Loss Item as {1} is a whole UOM.") \ + .format(frappe.bold(item.item_code), frappe.bold(item.stock_uom)) + + if item.is_process_loss and (item.stock_qty >= self.quantity): + msg = _("Scrap/Loss Item: {0} should have Qty less than finished goods Quantity.") \ + .format(frappe.bold(item.item_code)) + + if item.is_process_loss and (item.rate > 0): + msg = _("Scrap/Loss Item: {0} should have Rate set to 0 because Is Process Loss is checked.") \ + .format(frappe.bold(item.item_code)) + + if msg: + frappe.throw(msg, title=_("Note")) + def get_tree_representation(self) -> BOMTree: """Get a complete tree representation preserving order of child items.""" return BOMTree(self.name) @@ -713,12 +764,12 @@ def get_bom_item_rate(args, bom_doc): "conversion_rate": 1, # Passed conversion rate as 1 purposefully, as conversion rate is applied at the end of the function "conversion_factor": args.get("conversion_factor") or 1, "plc_conversion_rate": 1, - "ignore_party": True + "ignore_party": True, + "ignore_conversion_rate": True }) item_doc = frappe.get_cached_doc("Item", args.get("item_code")) - out = frappe._dict() - get_price_list_rate(bom_args, item_doc, out) - rate = out.price_list_rate + price_list_data = get_price_list_rate(bom_args, item_doc) + rate = price_list_data.price_list_rate return rate @@ -747,7 +798,7 @@ def get_valuation_rate(args): if valuation_rate <= 0: last_valuation_rate = frappe.db.sql("""select valuation_rate from `tabStock Ledger Entry` - where item_code = %s and valuation_rate > 0 + where item_code = %s and valuation_rate > 0 and is_cancelled = 0 order by posting_date desc, posting_time desc, creation desc limit 1""", args['item_code']) valuation_rate = flt(last_valuation_rate[0][0]) if last_valuation_rate else 0 @@ -773,7 +824,7 @@ def get_bom_items_as_dict(bom, company, qty=1, fetch_exploded=1, fetch_scrap_ite item.image, bom.project, bom_item.rate, - bom_item.amount, + sum(bom_item.{qty_field}/ifnull(bom.quantity, 1)) * bom_item.rate * %(qty)s as amount, item.stock_uom, item.item_group, item.allow_alternative_item, @@ -808,8 +859,11 @@ def get_bom_items_as_dict(bom, company, qty=1, fetch_exploded=1, fetch_scrap_ite items = frappe.db.sql(query, { "parent": bom, "qty": qty, "bom": bom, "company": company }, as_dict=True) elif fetch_scrap_items: - query = query.format(table="BOM Scrap Item", where_conditions="", - select_columns=", bom_item.idx, item.description", is_stock_item=is_stock_item, qty_field="stock_qty") + query = query.format( + table="BOM Scrap Item", where_conditions="", + select_columns=", bom_item.idx, item.description, is_process_loss", + is_stock_item=is_stock_item, qty_field="stock_qty" + ) items = frappe.db.sql(query, { "qty": qty, "bom": bom, "company": company }, as_dict=True) else: @@ -1068,13 +1122,6 @@ def item_query(doctype, txt, searchfield, start, page_len, filters): if barcodes: or_cond_filters["name"] = ("in", barcodes) - for cond in get_match_cond(doctype, as_condition=False): - for key, value in cond.items(): - if key == doctype: - key = "name" - - query_filters[key] = ("in", value) - if filters and filters.get("item_code"): has_variants = frappe.get_cached_value("Item", filters.get("item_code"), "has_variants") if not has_variants: @@ -1083,7 +1130,7 @@ def item_query(doctype, txt, searchfield, start, page_len, filters): if filters and filters.get("is_stock_item"): query_filters["is_stock_item"] = 1 - return frappe.get_all("Item", + return frappe.get_list("Item", fields = fields, filters=query_filters, or_filters = or_cond_filters, order_by=order_by, limit_start=start, limit_page_length=page_len, as_list=1) diff --git a/erpnext/manufacturing/doctype/bom/bom_item_preview.html b/erpnext/manufacturing/doctype/bom/bom_item_preview.html index 6088e46265b..e614a7ebaa1 100644 --- a/erpnext/manufacturing/doctype/bom/bom_item_preview.html +++ b/erpnext/manufacturing/doctype/bom/bom_item_preview.html @@ -38,4 +38,4 @@ {{ __("Open Item {0}", [data.item_code.bold()]) }} {% endif %}

-
\ No newline at end of file +
diff --git a/erpnext/manufacturing/doctype/bom/bom_tree.js b/erpnext/manufacturing/doctype/bom/bom_tree.js index 60fb377f476..6e2599e41bc 100644 --- a/erpnext/manufacturing/doctype/bom/bom_tree.js +++ b/erpnext/manufacturing/doctype/bom/bom_tree.js @@ -70,4 +70,4 @@ frappe.treeview_settings["BOM"] = { } }, view_template: 'bom_item_preview' -} \ No newline at end of file +} diff --git a/erpnext/manufacturing/doctype/bom/test_bom.js b/erpnext/manufacturing/doctype/bom/test_bom.js index 5044a284444..98a9198b79b 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.js +++ b/erpnext/manufacturing/doctype/bom/test_bom.js @@ -60,4 +60,4 @@ QUnit.test("test: item", function (assert) { () => done() ]); -}); \ No newline at end of file +}); diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index 57a54587269..a904d7f7be0 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -226,6 +226,80 @@ class TestBOM(unittest.TestCase): supplied_items = sorted([d.rm_item_code for d in po.supplied_items]) self.assertEqual(bom_items, supplied_items) + + def test_bom_recursion_1st_level(self): + """BOM should not allow BOM item again in child""" + item_code = "_Test BOM Recursion" + make_item(item_code, {'is_stock_item': 1}) + + bom = frappe.new_doc("BOM") + bom.item = item_code + bom.append("items", frappe._dict(item_code=item_code)) + with self.assertRaises(frappe.ValidationError) as err: + bom.save() + + self.assertTrue("recursion" in str(err.exception).lower()) + frappe.delete_doc("BOM", bom.name, ignore_missing=True) + + def test_bom_recursion_transitive(self): + item1 = "_Test BOM Recursion" + item2 = "_Test BOM Recursion 2" + make_item(item1, {'is_stock_item': 1}) + make_item(item2, {'is_stock_item': 1}) + + bom1 = frappe.new_doc("BOM") + bom1.item = item1 + bom1.append("items", frappe._dict(item_code=item2)) + bom1.save() + bom1.submit() + + bom2 = frappe.new_doc("BOM") + bom2.item = item2 + bom2.append("items", frappe._dict(item_code=item1)) + + with self.assertRaises(frappe.ValidationError) as err: + bom2.save() + bom2.submit() + + self.assertTrue("recursion" in str(err.exception).lower()) + + bom1.cancel() + frappe.delete_doc("BOM", bom1.name, ignore_missing=True, force=True) + frappe.delete_doc("BOM", bom2.name, ignore_missing=True, force=True) + + def test_bom_with_process_loss_item(self): + fg_item_non_whole, fg_item_whole, bom_item = create_process_loss_bom_items() + + if not frappe.db.exists("BOM", f"BOM-{fg_item_non_whole.item_code}-001"): + bom_doc = create_bom_with_process_loss_item( + fg_item_non_whole, bom_item, scrap_qty=0.25, scrap_rate=0, fg_qty=1 + ) + bom_doc.submit() + + bom_doc = create_bom_with_process_loss_item( + fg_item_non_whole, bom_item, scrap_qty=2, scrap_rate=0 + ) + # PL Item qty can't be >= FG Item qty + self.assertRaises(frappe.ValidationError, bom_doc.submit) + + bom_doc = create_bom_with_process_loss_item( + fg_item_non_whole, bom_item, scrap_qty=1, scrap_rate=100 + ) + # PL Item rate has to be 0 + self.assertRaises(frappe.ValidationError, bom_doc.submit) + + bom_doc = create_bom_with_process_loss_item( + fg_item_whole, bom_item, scrap_qty=0.25, scrap_rate=0 + ) + # Items with whole UOMs can't be PL Items + self.assertRaises(frappe.ValidationError, bom_doc.submit) + + bom_doc = create_bom_with_process_loss_item( + fg_item_non_whole, bom_item, scrap_qty=0.25, scrap_rate=0, is_process_loss=0 + ) + # FG Items in Scrap/Loss Table should have Is Process Loss set + self.assertRaises(frappe.ValidationError, bom_doc.submit) + def test_bom_tree_representation(self): bom_tree = { "Assembly": { @@ -248,13 +322,9 @@ class TestBOM(unittest.TestCase): for reqd_item, created_item in zip(reqd_order, created_order): self.assertEqual(reqd_item, created_item.item_code) - def get_default_bom(item_code="_Test FG Item 2"): return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1}) - - - def level_order_traversal(node): traversal = [] q = deque() @@ -300,6 +370,7 @@ def create_nested_bom(tree, prefix="_Test bom "): bom = frappe.get_doc(doctype="BOM", item=bom_item_code) for child_item in child_items.keys(): bom.append("items", {"item_code": prefix + child_item}) + bom.currency = "INR" bom.insert() bom.submit() @@ -321,3 +392,45 @@ def reset_item_valuation_rate(item_code, warehouse_list=None, qty=None, rate=Non for warehouse in warehouse_list: create_stock_reconciliation(item_code=item_code, warehouse=warehouse, qty=qty, rate=rate) + +def create_bom_with_process_loss_item( + fg_item, bom_item, scrap_qty, scrap_rate, fg_qty=2, is_process_loss=1): + bom_doc = frappe.new_doc("BOM") + bom_doc.item = fg_item.item_code + bom_doc.quantity = fg_qty + bom_doc.append("items", { + "item_code": bom_item.item_code, + "qty": 1, + "uom": bom_item.stock_uom, + "stock_uom": bom_item.stock_uom, + "rate": 100.0 + }) + bom_doc.append("scrap_items", { + "item_code": fg_item.item_code, + "qty": scrap_qty, + "stock_qty": scrap_qty, + "uom": fg_item.stock_uom, + "stock_uom": fg_item.stock_uom, + "rate": scrap_rate, + "is_process_loss": is_process_loss + }) + bom_doc.currency = "INR" + return bom_doc + +def create_process_loss_bom_items(): + item_list = [ + ("_Test Item - Non Whole UOM", "Kg"), + ("_Test Item - Whole UOM", "Unit"), + ("_Test PL BOM Item", "Unit") + ] + return [create_process_loss_bom_item(it) for it in item_list] + +def create_process_loss_bom_item(item_tuple): + item_code, stock_uom = item_tuple + if frappe.db.exists("Item", item_code) is None: + return make_item( + item_code, + {'stock_uom':stock_uom, 'valuation_rate':100} + ) + else: + return frappe.get_doc("Item", item_code) diff --git a/erpnext/manufacturing/doctype/bom_explosion_item/bom_explosion_item.py b/erpnext/manufacturing/doctype/bom_explosion_item/bom_explosion_item.py index cc5a3f8cb1a..39ccbddbea2 100644 --- a/erpnext/manufacturing/doctype/bom_explosion_item/bom_explosion_item.py +++ b/erpnext/manufacturing/doctype/bom_explosion_item/bom_explosion_item.py @@ -7,4 +7,4 @@ import frappe from frappe.model.document import Document class BOMExplosionItem(Document): - pass \ No newline at end of file + pass diff --git a/erpnext/manufacturing/doctype/bom_item/bom_item.py b/erpnext/manufacturing/doctype/bom_item/bom_item.py index e7cdea290b8..220c73e1493 100644 --- a/erpnext/manufacturing/doctype/bom_item/bom_item.py +++ b/erpnext/manufacturing/doctype/bom_item/bom_item.py @@ -7,4 +7,4 @@ import frappe from frappe.model.document import Document class BOMItem(Document): - pass \ No newline at end of file + pass diff --git a/erpnext/manufacturing/doctype/bom_operation/bom_operation.py b/erpnext/manufacturing/doctype/bom_operation/bom_operation.py index ee3f877da3c..e3501eb9cf6 100644 --- a/erpnext/manufacturing/doctype/bom_operation/bom_operation.py +++ b/erpnext/manufacturing/doctype/bom_operation/bom_operation.py @@ -7,4 +7,4 @@ import frappe from frappe.model.document import Document class BOMOperation(Document): - pass \ No newline at end of file + pass diff --git a/erpnext/manufacturing/doctype/bom_scrap_item/bom_scrap_item.json b/erpnext/manufacturing/doctype/bom_scrap_item/bom_scrap_item.json index 9f7091dd8d7..7018082e402 100644 --- a/erpnext/manufacturing/doctype/bom_scrap_item/bom_scrap_item.json +++ b/erpnext/manufacturing/doctype/bom_scrap_item/bom_scrap_item.json @@ -1,345 +1,112 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2016-09-26 02:19:21.642081", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, + "actions": [], + "creation": "2016-09-26 02:19:21.642081", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "item_code", + "column_break_2", + "item_name", + "is_process_loss", + "quantity_and_rate", + "stock_qty", + "rate", + "amount", + "column_break_6", + "stock_uom", + "base_rate", + "base_amount" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "item_code", - "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": "Item Code", - "length": 0, - "no_copy": 0, - "options": "Item", - "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, - "unique": 0 - }, + "fieldname": "item_code", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Item Code", + "options": "Item", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "item_name", - "fieldtype": "Data", - "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": "Item Name", - "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, - "unique": 0 - }, + "fieldname": "item_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Item Name" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "quantity_and_rate", - "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": "Quantity and Rate", - "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, - "unique": 0 - }, + "fieldname": "quantity_and_rate", + "fieldtype": "Section Break", + "label": "Quantity and Rate" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "stock_qty", - "fieldtype": "Float", - "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": "Qty", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "stock_qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Qty", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "rate", - "fieldtype": "Currency", - "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": "Rate", - "length": 0, - "no_copy": 0, - "options": "currency", - "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, - "unique": 0 - }, + "fieldname": "rate", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Rate", + "options": "currency" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "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": "Amount", - "length": 0, - "no_copy": 0, - "options": "currency", - "permlevel": 0, - "precision": "", - "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, - "unique": 0 - }, + "fieldname": "amount", + "fieldtype": "Currency", + "label": "Amount", + "options": "currency", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_6", - "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, - "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, - "unique": 0 - }, + "fieldname": "column_break_6", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "stock_uom", - "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": "Stock UOM", - "length": 0, - "no_copy": 0, - "options": "UOM", - "permlevel": 0, - "precision": "", - "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, - "unique": 0 - }, + "fieldname": "stock_uom", + "fieldtype": "Link", + "label": "Stock UOM", + "options": "UOM", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "base_rate", - "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": "Basic Rate (Company Currency)", - "length": 0, - "no_copy": 0, - "options": "Company:company:default_currency", - "permlevel": 0, - "precision": "", - "print_hide": 1, - "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, - "unique": 0 - }, + "fieldname": "base_rate", + "fieldtype": "Currency", + "label": "Basic Rate (Company Currency)", + "options": "Company:company:default_currency", + "print_hide": 1, + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "base_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": "Basic Amount (Company Currency)", - "length": 0, - "no_copy": 0, - "options": "Company:company:default_currency", - "permlevel": 0, - "precision": "", - "print_hide": 1, - "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, - "unique": 0 + "fieldname": "base_amount", + "fieldtype": "Currency", + "label": "Basic Amount (Company Currency)", + "options": "Company:company:default_currency", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "is_process_loss", + "fieldtype": "Check", + "label": "Is Process Loss" } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2017-07-04 16:04:32.442287", - "modified_by": "Administrator", - "module": "Manufacturing", - "name": "BOM Scrap Item", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "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 + ], + "istable": 1, + "links": [], + "modified": "2021-06-22 16:46:12.153311", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "BOM Scrap Item", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.js b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.js index e4b8a202882..bf5fe2e18de 100644 --- a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.js +++ b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.js @@ -46,4 +46,4 @@ frappe.ui.form.on('BOM Update Tool', { } }); } -}); \ No newline at end of file +}); diff --git a/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.js b/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.js deleted file mode 100644 index d220df2824f..00000000000 --- a/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_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: BOM Update Tool", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially('BOM Update Tool', [ - // insert a new BOM Update Tool - () => frappe.tests.make([ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js index 81860c9fbcf..91eb4a0fa90 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.js +++ b/erpnext/manufacturing/doctype/job_card/job_card.js @@ -367,4 +367,4 @@ frappe.ui.form.on('Job Card Time Log', { to_time: function(frm) { frm.set_value('started_time', ''); } -}) \ No newline at end of file +}) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index 420bb008039..3efbe88adaf 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -26,17 +26,17 @@ class JobCard(Document): self.set_status() self.validate_operation_id() self.validate_sequence_id() - self.get_sub_operations() + self.set_sub_operations() self.update_sub_operation_status() - def get_sub_operations(self): + def set_sub_operations(self): if self.operation: self.sub_operations = [] - for row in frappe.get_all("Sub Operation", - filters = {"parent": self.operation}, fields=["operation", "idx"]): - row.status = "Pending" + for row in frappe.get_all('Sub Operation', + filters = {'parent': self.operation}, fields=['operation', 'idx'], order_by='idx'): + row.status = 'Pending' row.sub_operation = row.operation - self.append("sub_operations", row) + self.append('sub_operations', row) def validate_time_logs(self): self.total_time_in_mins = 0.0 @@ -192,11 +192,11 @@ class JobCard(Document): "completed_qty": args.get("completed_qty") or 0.0 }) elif args.get("start_time"): - new_args = { + new_args = frappe._dict({ "from_time": get_datetime(args.get("start_time")), "operation": args.get("sub_operation"), "completed_qty": 0.0 - } + }) if employees: for name in employees: @@ -608,6 +608,11 @@ def make_stock_entry(source_name, target_doc=None): target.set_missing_values() target.set_stock_entry_type() + wo_allows_alternate_item = frappe.db.get_value("Work Order", target.work_order, "allow_alternative_item") + for item in target.items: + item.allow_alternative_item = int(wo_allows_alternate_item and + frappe.get_cached_value("Item", item.item_code, "allow_alternative_item")) + doclist = get_mapped_doc("Job Card", source_name, { "Job Card": { "doctype": "Stock Entry", @@ -685,7 +690,7 @@ def make_corrective_job_card(source_name, operation=None, for_operation=None, ta target.set('time_logs', []) target.set('employee', []) target.set('items', []) - target.get_sub_operations() + target.set_sub_operations() target.get_required_items() target.validate_time_logs() @@ -698,4 +703,4 @@ def make_corrective_job_card(source_name, operation=None, for_operation=None, ta } }, target_doc, set_missing_values) - return doclist \ No newline at end of file + return doclist diff --git a/erpnext/manufacturing/doctype/job_card/job_card_list.js b/erpnext/manufacturing/doctype/job_card/job_card_list.js index ed851ebc83b..8017209e7de 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card_list.js +++ b/erpnext/manufacturing/doctype/job_card/job_card_list.js @@ -12,4 +12,4 @@ frappe.listview_settings['Job Card'] = { return [__("Open"), "red", "status,=,Open"]; } } -}; \ No newline at end of file +}; diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.js b/erpnext/manufacturing/doctype/job_card/test_job_card.js deleted file mode 100644 index 5dc7805d226..00000000000 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.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: Job Card", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Job Card - () => frappe.tests.make('Job Card', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index b6a6c33d37f..8fa0b27fcb8 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -72,4 +72,4 @@ class TestJobCard(unittest.TestCase): doc.cancel() for d in job_cards: - frappe.delete_doc("Job Card", d.name) \ No newline at end of file + frappe.delete_doc("Job Card", d.name) diff --git a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.js b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.js index 668e981d188..a0122a47385 100644 --- a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.js +++ b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.js @@ -30,4 +30,4 @@ frappe.tour["Manufacturing Settings"] = [ title: __("Update BOM Cost Automatically"), description: __("If ticked, the BOM cost will be automatically updated based on Valuation Rate / Price List Rate / last purchase rate of raw materials.") } -]; \ No newline at end of file +]; diff --git a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.py b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.py index e88164f9178..149fe3e22b8 100644 --- a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.py +++ b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.py @@ -20,4 +20,4 @@ def is_material_consumption_enabled(): frappe.local.material_consumption = cint(frappe.db.get_single_value('Manufacturing Settings', 'material_consumption')) - return frappe.local.material_consumption \ No newline at end of file + return frappe.local.material_consumption diff --git a/erpnext/manufacturing/doctype/manufacturing_settings/test_manufacturing_settings.js b/erpnext/manufacturing/doctype/manufacturing_settings/test_manufacturing_settings.js deleted file mode 100644 index 2b2589eddd8..00000000000 --- a/erpnext/manufacturing/doctype/manufacturing_settings/test_manufacturing_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: Manufacturing Settings", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially('Manufacturing Settings', [ - // insert a new Manufacturing Settings - () => frappe.tests.make([ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/manufacturing/doctype/material_request_plan_item/test_material_request_plan_item.js b/erpnext/manufacturing/doctype/material_request_plan_item/test_material_request_plan_item.js deleted file mode 100644 index 14c6e393847..00000000000 --- a/erpnext/manufacturing/doctype/material_request_plan_item/test_material_request_plan_item.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: Material Request Plan Item", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Material Request Plan Item - () => frappe.tests.make('Material Request Plan Item', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/manufacturing/doctype/operation/operation.js b/erpnext/manufacturing/doctype/operation/operation.js index 102b6780e5f..2936e33b118 100644 --- a/erpnext/manufacturing/doctype/operation/operation.js +++ b/erpnext/manufacturing/doctype/operation/operation.js @@ -11,4 +11,4 @@ frappe.ui.form.on('Operation', { }; }); } -}); \ No newline at end of file +}); diff --git a/erpnext/manufacturing/doctype/operation/test_operation.py b/erpnext/manufacturing/doctype/operation/test_operation.py index 00672317018..8e7e7237263 100644 --- a/erpnext/manufacturing/doctype/operation/test_operation.py +++ b/erpnext/manufacturing/doctype/operation/test_operation.py @@ -28,4 +28,4 @@ def make_operation(*args, **kwargs): return doc except frappe.DuplicateEntryError: - return frappe.get_doc("Operation", args.operation) \ No newline at end of file + return frappe.get_doc("Operation", args.operation) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 38a0ee77ad7..6b61c6d330f 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -109,6 +109,15 @@ class ProductionPlan(Document): so_mr_list = [d.get(field) for d in self.get(table) if d.get(field)] return so_mr_list + def get_bom_item(self): + """Check if Item or if its Template has a BOM.""" + bom_item = None + has_bom = frappe.db.exists({'doctype': 'BOM', 'item': self.item_code, 'docstatus': 1}) + if not has_bom: + template_item = frappe.db.get_value('Item', self.item_code, ['variant_of']) + bom_item = "bom.item = {0}".format(frappe.db.escape(template_item)) if template_item else bom_item + return bom_item + def get_so_items(self): # Check for empty table or empty rows if not self.get("sales_orders") or not self.get_so_mr_list("sales_order", "sales_orders"): @@ -117,16 +126,26 @@ class ProductionPlan(Document): so_list = self.get_so_mr_list("sales_order", "sales_orders") item_condition = "" - if self.item_code: + bom_item = "bom.item = so_item.item_code" + if self.item_code and frappe.db.exists('Item', self.item_code): + bom_item = self.get_bom_item() or bom_item item_condition = ' and so_item.item_code = {0}'.format(frappe.db.escape(self.item_code)) - items = frappe.db.sql("""select distinct parent, item_code, warehouse, - (qty - work_order_qty) * conversion_factor as pending_qty, description, name - from `tabSales Order Item` so_item - where parent in (%s) and docstatus = 1 and qty > work_order_qty - and exists (select name from `tabBOM` bom where bom.item=so_item.item_code - and bom.is_active = 1) %s""" % \ - (", ".join(["%s"] * len(so_list)), item_condition), tuple(so_list), as_dict=1) + items = frappe.db.sql(""" + select + distinct parent, item_code, warehouse, + (qty - work_order_qty) * conversion_factor as pending_qty, + description, name + from + `tabSales Order Item` so_item + where + parent in (%s) and docstatus = 1 and qty > work_order_qty + and exists (select name from `tabBOM` bom where %s + and bom.is_active = 1) %s""" % + (", ".join(["%s"] * len(so_list)), + bom_item, + item_condition), + tuple(so_list), as_dict=1) if self.item_code: item_condition = ' and so_item.item_code = {0}'.format(frappe.db.escape(self.item_code)) @@ -194,7 +213,6 @@ class ProductionPlan(Document): }) pi = self.append('po_items', { - 'include_exploded_items': 1, 'warehouse': data.warehouse, 'item_code': data.item_code, 'description': data.description or item_details.description, @@ -205,6 +223,7 @@ class ProductionPlan(Document): 'planned_start_date': now_datetime(), 'product_bundle_item': data.parent_item }) + pi._set_defaults() if self.get_items_from == "Sales Order": pi.sales_order = data.parent @@ -278,7 +297,7 @@ class ProductionPlan(Document): if self.total_produced_qty > 0: self.status = "In Process" - if self.total_produced_qty == self.total_planned_qty: + if self.total_produced_qty >= self.total_planned_qty: self.status = "Completed" if self.status != 'Completed': @@ -327,6 +346,7 @@ class ProductionPlan(Document): "production_plan" : self.name, "production_plan_item" : d.name, "product_bundle_item" : d.product_bundle_item, + "planned_start_date" : d.planned_start_date, "make_work_order_for_sub_assembly_items": d.get("make_work_order_for_sub_assembly_items", 0) } @@ -683,6 +703,7 @@ def get_material_request_items(row, sales_order, company, def get_sales_orders(self): so_filter = item_filter = "" + bom_item = "bom.item = so_item.item_code" if self.from_date: so_filter += " and so.transaction_date >= %(from_date)s" if self.to_date: @@ -694,7 +715,8 @@ def get_sales_orders(self): if self.sales_order_status: so_filter += "and so.status = %(sales_order_status)s" - if self.item_code: + if self.item_code and frappe.db.exists('Item', self.item_code): + bom_item = self.get_bom_item() or bom_item item_filter += " and so_item.item_code = %(item)s" open_so = frappe.db.sql(""" @@ -704,13 +726,13 @@ def get_sales_orders(self): and so.docstatus = 1 and so.status not in ("Stopped", "Closed") and so.company = %(company)s and so_item.qty > so_item.work_order_qty {0} {1} - and (exists (select name from `tabBOM` bom where bom.item=so_item.item_code + and (exists (select name from `tabBOM` bom where {2} and bom.is_active = 1) or exists (select name from `tabPacked Item` pi where pi.parent = so.name and pi.parent_item = so_item.item_code and exists (select name from `tabBOM` bom where bom.item=pi.item_code and bom.is_active = 1))) - """.format(so_filter, item_filter), { + """.format(so_filter, item_filter, bom_item), { "from_date": self.from_date, "to_date": self.to_date, "customer": self.customer, @@ -747,9 +769,8 @@ def get_bin_details(row, company, for_warehouse=None, all_warehouse=False): group by item_code, warehouse """.format(conditions=conditions), { "item_code": row['item_code'] }, as_dict=1) -def get_warehouse_list(warehouses, warehouse_list=None): - if not warehouse_list: - warehouse_list = [] +def get_warehouse_list(warehouses): + warehouse_list = [] if isinstance(warehouses, str): warehouses = json.loads(warehouses) @@ -761,23 +782,19 @@ def get_warehouse_list(warehouses, warehouse_list=None): else: warehouse_list.append(row.get("warehouse")) + return warehouse_list + @frappe.whitelist() def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_data=None): if isinstance(doc, str): doc = frappe._dict(json.loads(doc)) - warehouse_list = [] if warehouses: - get_warehouse_list(warehouses, warehouse_list) - - if warehouse_list: - warehouses = list(set(warehouse_list)) + warehouses = list(set(get_warehouse_list(warehouses))) if doc.get("for_warehouse") and not get_parent_warehouse_data and doc.get("for_warehouse") in warehouses: warehouses.remove(doc.get("for_warehouse")) - warehouse_list = None - doc['mr_items'] = [] po_items = doc.get('po_items') if doc.get('po_items') else doc.get('items') diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan_dashboard.py b/erpnext/manufacturing/doctype/production_plan/production_plan_dashboard.py index ca597f63278..52a56af7bce 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan_dashboard.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan_dashboard.py @@ -14,4 +14,4 @@ def get_data(): 'items': ['Purchase Order'] }, ] - } \ No newline at end of file + } diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan_list.js b/erpnext/manufacturing/doctype/production_plan/production_plan_list.js index c2e3e6d7124..8f946866247 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan_list.js +++ b/erpnext/manufacturing/doctype/production_plan/production_plan_list.js @@ -1,4 +1,5 @@ frappe.listview_settings['Production Plan'] = { + hide_name_column: true, add_fields: ["status"], filters: [["status", "!=", "Closed"]], get_indicator: function (doc) { diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.js b/erpnext/manufacturing/doctype/production_plan/test_production_plan.js deleted file mode 100644 index ef7d64c92da..00000000000 --- a/erpnext/manufacturing/doctype/production_plan/test_production_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: Production Plan", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Production Plan - () => frappe.tests.make('Production Plan', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index cce1bb61b6b..78028039c48 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -5,12 +5,13 @@ from __future__ import unicode_literals import frappe import unittest -from frappe.utils import nowdate, now_datetime, flt +from frappe.utils import nowdate, now_datetime, flt, add_to_date from erpnext.stock.doctype.item.test_item import create_item from erpnext.manufacturing.doctype.production_plan.production_plan import get_sales_orders from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order -from erpnext.manufacturing.doctype.production_plan.production_plan import get_items_for_material_requests +from erpnext.manufacturing.doctype.production_plan.production_plan import get_items_for_material_requests, get_warehouse_list +from erpnext.controllers.item_variant import create_variant class TestProductionPlan(unittest.TestCase): def setUp(self): @@ -60,6 +61,21 @@ class TestProductionPlan(unittest.TestCase): pln = frappe.get_doc('Production Plan', pln.name) pln.cancel() + def test_production_plan_start_date(self): + planned_date = add_to_date(date=None, days=3) + plan = create_production_plan(item_code='Test Production Item 1', planned_start_date=planned_date) + plan.make_work_order() + + work_orders = frappe.get_all('Work Order', fields = ['name', 'planned_start_date'], + filters = {'production_plan': plan.name}) + + self.assertEqual(work_orders[0].planned_start_date, planned_date) + + for wo in work_orders: + frappe.delete_doc('Work Order', wo.name) + + frappe.get_doc('Production Plan', plan.name).cancel() + def test_production_plan_for_existing_ordered_qty(self): sr1 = create_stock_reconciliation(item_code="Raw Material Item 1", target="_Test Warehouse - _TC", qty=1, rate=110) @@ -251,6 +267,82 @@ class TestProductionPlan(unittest.TestCase): pln.cancel() frappe.delete_doc("Production Plan", pln.name) + def test_get_warehouse_list_group(self): + """Check if required warehouses are returned""" + warehouse_json = '[{\"warehouse\":\"_Test Warehouse Group - _TC\"}]' + + warehouses = set(get_warehouse_list(warehouse_json)) + expected_warehouses = {"_Test Warehouse Group-C1 - _TC", "_Test Warehouse Group-C2 - _TC"} + + missing_warehouse = expected_warehouses - warehouses + + self.assertTrue(len(missing_warehouse) == 0, + msg=f"Following warehouses were expected {', '.join(missing_warehouse)}") + + def test_get_warehouse_list_single(self): + warehouse_json = '[{\"warehouse\":\"_Test Scrap Warehouse - _TC\"}]' + + warehouses = set(get_warehouse_list(warehouse_json)) + expected_warehouses = {"_Test Scrap Warehouse - _TC", } + + self.assertEqual(warehouses, expected_warehouses) + + def test_get_sales_order_with_variant(self): + rm_item = create_item('PIV_RM', valuation_rate = 100) + if not frappe.db.exists('Item', {"item_code": 'PIV'}): + item = create_item('PIV', valuation_rate = 100) + variant_settings = { + "attributes": [ + { + "attribute": "Colour" + }, + ], + "has_variants": 1 + } + item.update(variant_settings) + item.save() + parent_bom = make_bom(item = 'PIV', raw_materials = [rm_item.item_code]) + if not frappe.db.exists('BOM', {"item": 'PIV'}): + parent_bom = make_bom(item = 'PIV', raw_materials = [rm_item.item_code]) + else: + parent_bom = frappe.get_doc('BOM', {"item": 'PIV'}) + + if not frappe.db.exists('Item', {"item_code": 'PIV-RED'}): + variant = create_variant("PIV", {"Colour": "Red"}) + variant.save() + variant_bom = make_bom(item = variant.item_code, raw_materials = [rm_item.item_code]) + else: + variant = frappe.get_doc('Item', 'PIV-RED') + if not frappe.db.exists('BOM', {"item": 'PIV-RED'}): + variant_bom = make_bom(item = variant.item_code, raw_materials = [rm_item.item_code]) + + """Testing when item variant has a BOM""" + so = make_sales_order(item_code="PIV-RED", qty=5) + pln = frappe.new_doc('Production Plan') + pln.company = so.company + pln.get_items_from = 'Sales Order' + pln.item_code = 'PIV-RED' + pln.get_open_sales_orders() + self.assertEqual(pln.sales_orders[0].sales_order, so.name) + pln.get_so_items() + self.assertEqual(pln.po_items[0].item_code, 'PIV-RED') + self.assertEqual(pln.po_items[0].bom_no, variant_bom.name) + so.cancel() + frappe.delete_doc('Sales Order', so.name) + variant_bom.cancel() + frappe.delete_doc('BOM', variant_bom.name) + + """Testing when item variant doesn't have a BOM""" + so = make_sales_order(item_code="PIV-RED", qty=5) + pln.get_open_sales_orders() + self.assertEqual(pln.sales_orders[0].sales_order, so.name) + pln.po_items = [] + pln.get_so_items() + self.assertEqual(pln.po_items[0].item_code, 'PIV-RED') + self.assertEqual(pln.po_items[0].bom_no, parent_bom.name) + + frappe.db.rollback() + def create_production_plan(**args): args = frappe._dict(args) diff --git a/erpnext/manufacturing/doctype/production_plan_item/production_plan_item.py b/erpnext/manufacturing/doctype/production_plan_item/production_plan_item.py index 8b570422ddc..37cf5a49dc9 100644 --- a/erpnext/manufacturing/doctype/production_plan_item/production_plan_item.py +++ b/erpnext/manufacturing/doctype/production_plan_item/production_plan_item.py @@ -7,4 +7,4 @@ import frappe from frappe.model.document import Document class ProductionPlanItem(Document): - pass \ No newline at end of file + pass diff --git a/erpnext/manufacturing/doctype/production_plan_sales_order/production_plan_sales_order.py b/erpnext/manufacturing/doctype/production_plan_sales_order/production_plan_sales_order.py index ef7f79e8d2c..99c7273a640 100644 --- a/erpnext/manufacturing/doctype/production_plan_sales_order/production_plan_sales_order.py +++ b/erpnext/manufacturing/doctype/production_plan_sales_order/production_plan_sales_order.py @@ -7,4 +7,4 @@ import frappe from frappe.model.document import Document class ProductionPlanSalesOrder(Document): - pass \ No newline at end of file + pass diff --git a/erpnext/manufacturing/doctype/routing/routing_dashboard.py b/erpnext/manufacturing/doctype/routing/routing_dashboard.py index ab309cc9d50..50a3fe62da5 100644 --- a/erpnext/manufacturing/doctype/routing/routing_dashboard.py +++ b/erpnext/manufacturing/doctype/routing/routing_dashboard.py @@ -9,4 +9,4 @@ def get_data(): 'items': ['BOM'] } ] - } \ No newline at end of file + } diff --git a/erpnext/manufacturing/doctype/routing/test_routing.js b/erpnext/manufacturing/doctype/routing/test_routing.js deleted file mode 100644 index 6cb65494af2..00000000000 --- a/erpnext/manufacturing/doctype/routing/test_routing.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: Routing", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Routing - () => frappe.tests.make('Routing', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/manufacturing/doctype/sub_operation/sub_operation.json b/erpnext/manufacturing/doctype/sub_operation/sub_operation.json index f63d2b98641..10cee32398a 100644 --- a/erpnext/manufacturing/doctype/sub_operation/sub_operation.json +++ b/erpnext/manufacturing/doctype/sub_operation/sub_operation.json @@ -19,6 +19,7 @@ "options": "Operation" }, { + "default": "0", "description": "Time in mins", "fieldname": "time_in_mins", "fieldtype": "Float", @@ -38,7 +39,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-12-07 18:09:18.005578", + "modified": "2021-07-15 16:39:41.635362", "modified_by": "Administrator", "module": "Manufacturing", "name": "Sub Operation", diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 68de0b29d3e..c0ed6115340 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -513,6 +513,60 @@ class TestWorkOrder(unittest.TestCase): work_order1.save() self.assertEqual(work_order1.operations[0].time_in_mins, 40.0) + def test_batch_size_for_fg_item(self): + fg_item = "Test Batch Size Item For BOM 3" + rm1 = "Test Batch Size Item RM 1 For BOM 3" + + frappe.db.set_value('Manufacturing Settings', None, 'make_serial_no_batch_from_work_order', 0) + for item in ["Test Batch Size Item For BOM 3", "Test Batch Size Item RM 1 For BOM 3"]: + item_args = { + "include_item_in_manufacturing": 1, + "is_stock_item": 1 + } + + if item == fg_item: + item_args['has_batch_no'] = 1 + item_args['create_new_batch'] = 1 + item_args['batch_number_series'] = 'TBSI3.#####' + + make_item(item, item_args) + + bom_name = frappe.db.get_value("BOM", + {"item": fg_item, "is_active": 1, "with_operations": 1}, "name") + + if not bom_name: + bom = make_bom(item=fg_item, rate=1000, raw_materials = [rm1], do_not_save=True) + bom.save() + bom.submit() + bom_name = bom.name + + work_order = make_wo_order_test_record(item=fg_item, skip_transfer=True, planned_start_date=now(), qty=1) + ste1 = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 1)) + for row in ste1.get('items'): + if row.is_finished_item: + self.assertEqual(row.item_code, fg_item) + + work_order = make_wo_order_test_record(item=fg_item, skip_transfer=True, planned_start_date=now(), qty=1) + frappe.db.set_value('Manufacturing Settings', None, 'make_serial_no_batch_from_work_order', 1) + ste1 = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 1)) + for row in ste1.get('items'): + if row.is_finished_item: + self.assertEqual(row.item_code, fg_item) + + work_order = make_wo_order_test_record(item=fg_item, skip_transfer=True, planned_start_date=now(), + qty=30, do_not_save = True) + work_order.batch_size = 10 + work_order.insert() + work_order.submit() + self.assertEqual(work_order.has_batch_no, 1) + ste1 = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 30)) + for row in ste1.get('items'): + if row.is_finished_item: + self.assertEqual(row.item_code, fg_item) + self.assertEqual(row.qty, 10) + + frappe.db.set_value('Manufacturing Settings', None, 'make_serial_no_batch_from_work_order', 0) + def test_partial_material_consumption(self): frappe.db.set_value("Manufacturing Settings", None, "material_consumption", 1) wo_order = make_wo_order_test_record(planned_start_date=now(), qty=4) @@ -621,13 +675,18 @@ class TestWorkOrder(unittest.TestCase): def test_valuation_rate_missing_on_make_stock_entry(self): item_name = 'Test Valuation Rate Missing' + rm_item = '_Test raw material item' make_item(item_name, { "is_stock_item": 1, "include_item_in_manufacturing": 1, }) + make_item('_Test raw material item', { + "is_stock_item": 1, + "include_item_in_manufacturing": 1, + }) if not frappe.db.get_value('BOM', {'item': item_name}): - make_bom(item=item_name, raw_materials=[item_name], rm_qty=1) + make_bom(item=item_name, raw_materials=[rm_item], rm_qty=1) company = "_Test Company with perpetual inventory" source_warehouse = create_warehouse("Test Valuation Rate Missing Warehouse", company=company) @@ -636,6 +695,71 @@ class TestWorkOrder(unittest.TestCase): self.assertRaises(frappe.ValidationError, make_stock_entry, wo.name, 'Material Transfer for Manufacture') + def test_wo_completion_with_pl_bom(self): + from erpnext.manufacturing.doctype.bom.test_bom import create_process_loss_bom_items + from erpnext.manufacturing.doctype.bom.test_bom import create_bom_with_process_loss_item + + qty = 4 + scrap_qty = 0.25 # bom item qty = 1, consider as 25% of FG + source_warehouse = "Stores - _TC" + wip_warehouse = "_Test Warehouse - _TC" + fg_item_non_whole, _, bom_item = create_process_loss_bom_items() + + test_stock_entry.make_stock_entry(item_code=bom_item.item_code, + target=source_warehouse, qty=4, basic_rate=100) + + bom_no = f"BOM-{fg_item_non_whole.item_code}-001" + if not frappe.db.exists("BOM", bom_no): + bom_doc = create_bom_with_process_loss_item( + fg_item_non_whole, bom_item, scrap_qty=scrap_qty, + scrap_rate=0, fg_qty=1, is_process_loss=1 + ) + bom_doc.submit() + + wo = make_wo_order_test_record( + production_item=fg_item_non_whole.item_code, + bom_no=bom_no, + wip_warehouse=wip_warehouse, + qty=qty, + skip_transfer=1, + stock_uom=fg_item_non_whole.stock_uom, + ) + + se = frappe.get_doc( + make_stock_entry(wo.name, "Material Transfer for Manufacture", qty) + ) + se.get("items")[0].s_warehouse = "Stores - _TC" + se.insert() + se.submit() + + se = frappe.get_doc( + make_stock_entry(wo.name, "Manufacture", qty) + ) + se.insert() + se.submit() + + # Testing stock entry values + items = se.get("items") + self.assertEqual(len(items), 3, "There should be 3 items including process loss.") + + source_item, fg_item, pl_item = items + + total_pl_qty = qty * scrap_qty + actual_fg_qty = qty - total_pl_qty + + self.assertEqual(pl_item.qty, total_pl_qty) + self.assertEqual(fg_item.qty, actual_fg_qty) + + # Testing Work Order values + self.assertEqual( + frappe.db.get_value("Work Order", wo.name, "produced_qty"), + qty + ) + self.assertEqual( + frappe.db.get_value("Work Order", wo.name, "process_loss_qty"), + total_pl_qty + ) + def get_scrap_item_details(bom_no): scrap_items = {} for item in frappe.db.sql("""select item_code, stock_qty from `tabBOM Scrap Item` diff --git a/erpnext/manufacturing/doctype/work_order/work_order.json b/erpnext/manufacturing/doctype/work_order/work_order.json index 3b56854aaf3..913fc85af61 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.json +++ b/erpnext/manufacturing/doctype/work_order/work_order.json @@ -19,6 +19,7 @@ "qty", "material_transferred_for_manufacturing", "produced_qty", + "process_loss_qty", "sales_order", "project", "serial_no_and_batch_for_finished_good_section", @@ -64,16 +65,12 @@ "description", "stock_uom", "column_break2", - "references_section", "material_request", "material_request_item", "sales_order_item", - "column_break_61", "production_plan", "production_plan_item", "production_plan_sub_assembly_item", - "parent_work_order", - "bom_level", "product_bundle_item", "amended_from" ], @@ -553,20 +550,29 @@ "read_only": 1 }, { - "fieldname": "production_plan_sub_assembly_item", - "fieldtype": "Data", - "label": "Production Plan Sub-assembly Item", - "no_copy": 1, - "print_hide": 1, - "read_only": 1 - } + "fieldname": "production_plan_sub_assembly_item", + "fieldtype": "Data", + "label": "Production Plan Sub-assembly Item", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, + { + "depends_on": "eval: doc.process_loss_qty", + "fieldname": "process_loss_qty", + "fieldtype": "Float", + "label": "Process Loss Qty", + "no_copy": 1, + "non_negative": 1, + "read_only": 1 + } ], "icon": "fa fa-cogs", "idx": 1, "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2021-06-28 16:19:14.902699", + "modified": "2021-08-24 15:14:03.844937", "modified_by": "Administrator", "module": "Manufacturing", "name": "Work Order", diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 779ae42d653..24b33d523e4 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -214,6 +214,7 @@ class WorkOrder(Document): self.meta.get_label(fieldname), qty, completed_qty, self.name), StockOverProductionError) self.db_set(fieldname, qty) + self.set_process_loss_qty() from erpnext.selling.doctype.sales_order.sales_order import update_produced_qty_in_so_item @@ -223,6 +224,22 @@ class WorkOrder(Document): if self.production_plan: self.update_production_plan_status() + def set_process_loss_qty(self): + process_loss_qty = flt(frappe.db.sql(""" + SELECT sum(qty) FROM `tabStock Entry Detail` + WHERE + is_process_loss=1 + AND parent IN ( + SELECT name FROM `tabStock Entry` + WHERE + work_order=%s + AND purpose='Manufacture' + AND docstatus=1 + ) + """, (self.name, ))[0][0]) + if process_loss_qty is not None: + self.db_set('process_loss_qty', process_loss_qty) + def update_production_plan_status(self): production_plan = frappe.get_doc('Production Plan', self.production_plan) produced_qty = 0 @@ -239,7 +256,7 @@ class WorkOrder(Document): self.create_serial_no_batch_no() def on_submit(self): - if not self.wip_warehouse: + if not self.wip_warehouse and not self.skip_transfer: frappe.throw(_("Work-in-Progress Warehouse is required before Submit")) if not self.fg_warehouse: frappe.throw(_("For Warehouse is required before Submit")) @@ -487,21 +504,20 @@ class WorkOrder(Document): return operations = [] - if not self.use_multi_level_bom: - bom_qty = frappe.db.get_value("BOM", self.bom_no, "quantity") - operations.extend(_get_operations(self.bom_no, qty=1.0/bom_qty)) - else: + + if self.use_multi_level_bom: bom_tree = frappe.get_doc("BOM", self.bom_no).get_tree_representation() - bom_traversal = list(reversed(bom_tree.level_order_traversal())) - bom_traversal.append(bom_tree) # add operation on top level item last + bom_traversal = reversed(bom_tree.level_order_traversal()) - for d in bom_traversal: - if d.is_bom: - operations.extend(_get_operations(d.name, qty=d.exploded_qty)) + for node in bom_traversal: + if node.is_bom: + operations.extend(_get_operations(node.name, qty=node.exploded_qty)) - for correct_index, operation in enumerate(operations, start=1): - operation.idx = correct_index + bom_qty = frappe.db.get_value("BOM", self.bom_no, "quantity") + operations.extend(_get_operations(self.bom_no, qty=1.0/bom_qty)) + for correct_index, operation in enumerate(operations, start=1): + operation.idx = correct_index self.set('operations', operations) self.calculate_time() @@ -603,7 +619,7 @@ class WorkOrder(Document): if self.docstatus==1: # calculate transferred qty based on submitted stock entries - self.update_transaferred_qty_for_required_items() + self.update_transferred_qty_for_required_items() # update in bin self.update_reserved_qty_for_production() @@ -656,7 +672,7 @@ class WorkOrder(Document): for item in sorted(item_dict.values(), key=lambda d: d['idx'] or 9999): self.append('required_items', { 'rate': item.rate, - 'amount': item.amount, + 'amount': item.rate * item.qty, 'operation': item.operation or operation, 'item_code': item.item_code, 'item_name': item.item_name, @@ -672,7 +688,7 @@ class WorkOrder(Document): self.set_available_qty() - def update_transaferred_qty_for_required_items(self): + def update_transferred_qty_for_required_items(self): '''update transferred qty from submitted stock entries for that item against the work order''' @@ -839,7 +855,7 @@ def add_variant_item(variant_items, wo_doc, bom_no, table_name="items"): for item in variant_items: args = frappe._dict({ - "item_code": item.get("varint_item_code"), + "item_code": item.get("variant_item_code"), "required_qty": item.get("qty"), "qty": item.get("qty"), # for bom "source_warehouse": item.get("source_warehouse"), @@ -860,7 +876,7 @@ def add_variant_item(variant_items, wo_doc, bom_no, table_name="items"): }, bom_doc) if not args.source_warehouse: - args["source_warehouse"] = get_item_defaults(item.get("varint_item_code"), + args["source_warehouse"] = get_item_defaults(item.get("variant_item_code"), wo_doc.company).default_warehouse args["amount"] = flt(args.get("required_qty")) * flt(args.get("rate")) diff --git a/erpnext/manufacturing/doctype/work_order/work_order_dashboard.py b/erpnext/manufacturing/doctype/work_order/work_order_dashboard.py index 9aa0715e7ff..403d46d8d42 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order_dashboard.py +++ b/erpnext/manufacturing/doctype/work_order/work_order_dashboard.py @@ -17,4 +17,4 @@ def get_data(): 'items': ['Serial No', 'Batch'] } ] - } \ No newline at end of file + } diff --git a/erpnext/manufacturing/doctype/work_order_item/work_order_item.py b/erpnext/manufacturing/doctype/work_order_item/work_order_item.py index d18f028fc6e..9aa53b5e3c3 100644 --- a/erpnext/manufacturing/doctype/work_order_item/work_order_item.py +++ b/erpnext/manufacturing/doctype/work_order_item/work_order_item.py @@ -10,4 +10,4 @@ class WorkOrderItem(Document): pass def on_doctype_update(): - frappe.db.add_index("Work Order Item", ["item_code", "source_warehouse"]) \ No newline at end of file + frappe.db.add_index("Work Order Item", ["item_code", "source_warehouse"]) diff --git a/erpnext/manufacturing/doctype/workstation/workstation.js b/erpnext/manufacturing/doctype/workstation/workstation.js index ba8e30cba07..d8d25fc6f83 100644 --- a/erpnext/manufacturing/doctype/workstation/workstation.js +++ b/erpnext/manufacturing/doctype/workstation/workstation.js @@ -16,4 +16,4 @@ frappe.ui.form.on("Workstation", { }) } } -}) \ No newline at end of file +}) diff --git a/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.py b/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.py index e7d92658f7d..8778d9ba557 100644 --- a/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.py +++ b/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.py @@ -108,5 +108,3 @@ def get_columns(filters): "fieldtype": "Int", "width": 180 }] - - diff --git a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.html b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.html index 119a4fc6292..2ae8848cc03 100644 --- a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.html +++ b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.html @@ -24,4 +24,4 @@ {% } %} - \ No newline at end of file + diff --git a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py index 9f81e7d26a1..b4db98c3d7e 100644 --- a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py +++ b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py @@ -124,4 +124,4 @@ def get_columns(filters): "fieldname": "total_time_in_mins", "width": "100" } - ] \ No newline at end of file + ] diff --git a/erpnext/manufacturing/report/downtime_analysis/downtime_analysis.py b/erpnext/manufacturing/report/downtime_analysis/downtime_analysis.py index 093309a005b..74c794b5dd0 100644 --- a/erpnext/manufacturing/report/downtime_analysis/downtime_analysis.py +++ b/erpnext/manufacturing/report/downtime_analysis/downtime_analysis.py @@ -110,4 +110,4 @@ def get_columns(filters): "fieldtype": "Text", "width": 100 } - ] \ No newline at end of file + ] diff --git a/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.py b/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.py index fc27d355984..9a6c764c609 100644 --- a/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.py +++ b/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.py @@ -239,4 +239,4 @@ class ForecastingReport(ExponentialSmoothingForecast): "currency": self.company_currency, "datatype": self.fieldtype } - ] \ No newline at end of file + ] diff --git a/erpnext/manufacturing/report/job_card_summary/job_card_summary.py b/erpnext/manufacturing/report/job_card_summary/job_card_summary.py index b1bff3500c6..a8939051523 100644 --- a/erpnext/manufacturing/report/job_card_summary/job_card_summary.py +++ b/erpnext/manufacturing/report/job_card_summary/job_card_summary.py @@ -201,4 +201,4 @@ def get_columns(filters): } ]) - return columns \ No newline at end of file + return columns diff --git a/erpnext/manufacturing/report/production_analytics/production_analytics.py b/erpnext/manufacturing/report/production_analytics/production_analytics.py index 79af8a1e39b..42c9d97cb5e 100644 --- a/erpnext/manufacturing/report/production_analytics/production_analytics.py +++ b/erpnext/manufacturing/report/production_analytics/production_analytics.py @@ -139,7 +139,3 @@ def get_chart_data(periodic_data, columns): chart["type"] = "line" return chart - - - - diff --git a/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.py b/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.py index 6192632bda6..a12ac7f9d91 100644 --- a/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.py +++ b/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.py @@ -129,4 +129,4 @@ def get_columns(filters): } ]) - return columns \ No newline at end of file + return columns diff --git a/erpnext/manufacturing/report/work_order_stock_report/work_order_stock_report.py b/erpnext/manufacturing/report/work_order_stock_report/work_order_stock_report.py index 97553e699d8..599a738f6f6 100644 --- a/erpnext/manufacturing/report/work_order_stock_report/work_order_stock_report.py +++ b/erpnext/manufacturing/report/work_order_stock_report/work_order_stock_report.py @@ -10,10 +10,10 @@ def execute(filters=None): data = get_item_list(wo_list, filters) columns = get_columns() return columns, data - + def get_item_list(wo_list, filters): out = [] - + #Add a row for each item/qty for wo_details in wo_list: desc = frappe.db.get_value("BOM", wo_details.bom_no, "description") @@ -70,13 +70,13 @@ def get_item_list(wo_list, filters): out.append(row) return out - + def get_work_orders(): out = frappe.get_all("Work Order", filters={"docstatus": 1, "status": ( "!=","Completed")}, fields=["name","status", "bom_no", "qty", "produced_qty"], order_by='name') return out - + def get_columns(): columns = [{ "fieldname": "work_order", diff --git a/erpnext/manufacturing/report/work_order_summary/work_order_summary.py b/erpnext/manufacturing/report/work_order_summary/work_order_summary.py index 612dad0bf51..d0766f9abe5 100644 --- a/erpnext/manufacturing/report/work_order_summary/work_order_summary.py +++ b/erpnext/manufacturing/report/work_order_summary/work_order_summary.py @@ -265,4 +265,4 @@ def get_columns(filters): }, ]) - return columns \ No newline at end of file + return columns diff --git a/erpnext/non_profit/doctype/certification_application/test_certification_application.js b/erpnext/non_profit/doctype/certification_application/test_certification_application.js deleted file mode 100644 index 40e94864d49..00000000000 --- a/erpnext/non_profit/doctype/certification_application/test_certification_application.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: Certification Application", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Certification Application - () => frappe.tests.make('Certification Application', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/non_profit/doctype/certified_consultant/test_certified_consultant.js b/erpnext/non_profit/doctype/certified_consultant/test_certified_consultant.js deleted file mode 100644 index f6a72a43271..00000000000 --- a/erpnext/non_profit/doctype/certified_consultant/test_certified_consultant.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: Certified Consultant", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Certified Consultant - () => frappe.tests.make('Certified Consultant', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/non_profit/doctype/chapter/test_chapter.js b/erpnext/non_profit/doctype/chapter/test_chapter.js deleted file mode 100644 index e30d6a5bf9b..00000000000 --- a/erpnext/non_profit/doctype/chapter/test_chapter.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: Chapter", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Chapter - () => frappe.tests.make('Chapter', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/non_profit/doctype/chapter_member/chapter_member.py b/erpnext/non_profit/doctype/chapter_member/chapter_member.py index c4b899913b9..a1b25f2d4e2 100644 --- a/erpnext/non_profit/doctype/chapter_member/chapter_member.py +++ b/erpnext/non_profit/doctype/chapter_member/chapter_member.py @@ -7,5 +7,3 @@ from frappe.model.document import Document class ChapterMember(Document): pass - - diff --git a/erpnext/non_profit/doctype/donation/donation.py b/erpnext/non_profit/doctype/donation/donation.py index 4fd1a30ab9e..9aa7e13433c 100644 --- a/erpnext/non_profit/doctype/donation/donation.py +++ b/erpnext/non_profit/doctype/donation/donation.py @@ -217,4 +217,3 @@ def notify_failure(log): sendmail_to_system_managers(_('[Important] [ERPNext] Razorpay donation webhook failed, please check.'), content) except Exception: pass - diff --git a/erpnext/non_profit/doctype/donation/donation_dashboard.py b/erpnext/non_profit/doctype/donation/donation_dashboard.py index 7e25c8d2173..3da89423d37 100644 --- a/erpnext/non_profit/doctype/donation/donation_dashboard.py +++ b/erpnext/non_profit/doctype/donation/donation_dashboard.py @@ -13,4 +13,4 @@ def get_data(): 'items': ['Payment Entry'] } ] - } \ No newline at end of file + } diff --git a/erpnext/non_profit/doctype/donation/test_donation.py b/erpnext/non_profit/doctype/donation/test_donation.py index bbe9bf5228d..b206f54523e 100644 --- a/erpnext/non_profit/doctype/donation/test_donation.py +++ b/erpnext/non_profit/doctype/donation/test_donation.py @@ -73,4 +73,4 @@ def create_mode_of_payment(): 'company': '_Test Company', 'default_account': 'Cash - _TC' }] - }).insert() \ No newline at end of file + }).insert() diff --git a/erpnext/non_profit/doctype/donor/donor.py b/erpnext/non_profit/doctype/donor/donor.py index fb70e59575b..ab6a197ed51 100644 --- a/erpnext/non_profit/doctype/donor/donor.py +++ b/erpnext/non_profit/doctype/donor/donor.py @@ -15,4 +15,3 @@ class Donor(Document): from frappe.utils import validate_email_address if self.email: validate_email_address(self.email.strip(), True) - diff --git a/erpnext/non_profit/doctype/donor_type/test_donor_type.js b/erpnext/non_profit/doctype/donor_type/test_donor_type.js deleted file mode 100644 index 22dc18ed76a..00000000000 --- a/erpnext/non_profit/doctype/donor_type/test_donor_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: Donor Type", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Member - () => frappe.tests.make('Donor Type', [ - // values to be set - {donor_type: 'Test Organization'}, - ]), - () => { - assert.equal(cur_frm.doc.donor_type, 'Test Organization'); - }, - () => done() - ]); - -}); diff --git a/erpnext/non_profit/doctype/grant_application/grant_application.py b/erpnext/non_profit/doctype/grant_application/grant_application.py index f0123b2e494..b810fd027af 100644 --- a/erpnext/non_profit/doctype/grant_application/grant_application.py +++ b/erpnext/non_profit/doctype/grant_application/grant_application.py @@ -55,4 +55,4 @@ def send_grant_review_emails(grant_application): grant.save() frappe.db.commit() - frappe.msgprint(_("Review Invitation Sent")) \ No newline at end of file + frappe.msgprint(_("Review Invitation Sent")) diff --git a/erpnext/non_profit/doctype/member/member.js b/erpnext/non_profit/doctype/member/member.js index 6b8f1b1deb6..e58ec0f5eea 100644 --- a/erpnext/non_profit/doctype/member/member.js +++ b/erpnext/non_profit/doctype/member/member.js @@ -61,4 +61,4 @@ frappe.ui.form.on('Member', { } }); } -}); \ No newline at end of file +}); diff --git a/erpnext/non_profit/doctype/member/member.json b/erpnext/non_profit/doctype/member/member.json index f190cfae755..7c1baf1a8d1 100644 --- a/erpnext/non_profit/doctype/member/member.json +++ b/erpnext/non_profit/doctype/member/member.json @@ -26,7 +26,7 @@ "razorpay_details_section", "subscription_id", "customer_id", - "subscription_activated", + "subscription_status", "column_break_21", "subscription_start", "subscription_end" @@ -151,12 +151,6 @@ "fieldname": "column_break_21", "fieldtype": "Column Break" }, - { - "default": "0", - "fieldname": "subscription_activated", - "fieldtype": "Check", - "label": "Subscription Activated" - }, { "fieldname": "subscription_start", "fieldtype": "Date", @@ -166,11 +160,17 @@ "fieldname": "subscription_end", "fieldtype": "Date", "label": "Subscription End" + }, + { + "fieldname": "subscription_status", + "fieldtype": "Select", + "label": "Subscription Status", + "options": "\nActive\nHalted" } ], "image_field": "image", "links": [], - "modified": "2020-11-09 12:12:10.174647", + "modified": "2021-07-11 14:27:26.368039", "modified_by": "Administrator", "module": "Non Profit", "name": "Member", diff --git a/erpnext/non_profit/doctype/member/member.py b/erpnext/non_profit/doctype/member/member.py index 30be585e9a7..67828d6efc8 100644 --- a/erpnext/non_profit/doctype/member/member.py +++ b/erpnext/non_profit/doctype/member/member.py @@ -84,7 +84,9 @@ def create_member(user_details): "email_id": user_details.email, "pan_number": user_details.pan or None, "membership_type": user_details.plan_id, - "subscription_id": user_details.subscription_id or None + "customer_id": user_details.customer_id or None, + "subscription_id": user_details.subscription_id or None, + "subscription_status": user_details.subscription_status or "" }) member.insert(ignore_permissions=True) diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py index e8ae6187b7e..b584116df3c 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -196,11 +196,14 @@ def make_invoice(membership, member, plan, settings): return invoice -def get_member_based_on_subscription(subscription_id, email): - members = frappe.get_all("Member", filters={ - "subscription_id": subscription_id, - "email_id": email - }, order_by="creation desc") +def get_member_based_on_subscription(subscription_id, email=None, customer_id=None): + filters = {"subscription_id": subscription_id} + if email: + filters.update({"email_id": email}) + if customer_id: + filters.update({"customer_id": customer_id}) + + members = frappe.get_all("Member", filters=filters, order_by="creation desc") try: return frappe.get_doc("Member", members[0]["name"]) @@ -209,8 +212,6 @@ def get_member_based_on_subscription(subscription_id, email): def verify_signature(data, endpoint="Membership"): - if frappe.flags.in_test or os.environ.get("CI"): - return True signature = frappe.request.headers.get("X-Razorpay-Signature") settings = frappe.get_doc("Non Profit Settings") @@ -225,16 +226,7 @@ def verify_signature(data, endpoint="Membership"): @frappe.whitelist(allow_guest=True) def trigger_razorpay_subscription(*args, **kwargs): data = frappe.request.get_data(as_text=True) - try: - verify_signature(data) - except Exception as e: - log = frappe.log_error(e, "Membership Webhook Verification Error") - notify_failure(log) - return { "status": "Failed", "reason": e} - - if isinstance(data, six.string_types): - data = json.loads(data) - data = frappe._dict(data) + data = process_request_data(data) subscription = data.payload.get("subscription", {}).get("entity", {}) subscription = frappe._dict(subscription) @@ -281,7 +273,7 @@ def trigger_razorpay_subscription(*args, **kwargs): # Update membership values member.subscription_start = datetime.fromtimestamp(subscription.start_at) member.subscription_end = datetime.fromtimestamp(subscription.end_at) - member.subscription_activated = 1 + member.subscription_status = "Active" member.flags.ignore_mandatory = True member.save() @@ -294,9 +286,67 @@ def trigger_razorpay_subscription(*args, **kwargs): message = "{0}\n\n{1}\n\n{2}: {3}".format(e, frappe.get_traceback(), _("Payment ID"), payment.id) log = frappe.log_error(message, _("Error creating membership entry for {0}").format(member.name)) notify_failure(log) - return { "status": "Failed", "reason": e} + return {"status": "Failed", "reason": e} - return { "status": "Success" } + return {"status": "Success"} + + +@frappe.whitelist(allow_guest=True) +def update_halted_razorpay_subscription(*args, **kwargs): + """ + When all retries have been exhausted, Razorpay moves the subscription to the halted state. + The customer has to manually retry the charge or change the card linked to the subscription, + for the subscription to move back to the active state. + """ + if frappe.request: + data = frappe.request.get_data(as_text=True) + data = process_request_data(data) + elif frappe.flags.in_test: + data = kwargs.get("data") + data = frappe._dict(data) + else: + return + + if not data.event == "subscription.halted": + return + + subscription = data.payload.get("subscription", {}).get("entity", {}) + subscription = frappe._dict(subscription) + + try: + member = get_member_based_on_subscription(subscription.id, customer_id=subscription.customer_id) + if not member: + frappe.throw(_("Member with Razorpay Subscription ID {0} not found").format(subscription.id)) + + member.subscription_status = "Halted" + member.flags.ignore_mandatory = True + member.save() + + if subscription.get("notes"): + member = get_additional_notes(member, subscription) + + except Exception as e: + message = "{0}\n\n{1}".format(e, frappe.get_traceback()) + log = frappe.log_error(message, _("Error updating halted status for member {0}").format(member.name)) + notify_failure(log) + return {"status": "Failed", "reason": e} + + return {"status": "Success"} + + +def process_request_data(data): + try: + verify_signature(data) + except Exception as e: + log = frappe.log_error(e, "Membership Webhook Verification Error") + notify_failure(log) + return {"status": "Failed", "reason": e} + + if isinstance(data, six.string_types): + data = json.loads(data) + data = frappe._dict(data) + + return data def get_company_for_memberships(): @@ -362,4 +412,4 @@ def set_expired_status(): `tabMembership` SET `status` = 'Expired' WHERE `status` not in ('Cancelled') AND `to_date` < %s - """, (nowdate())) \ No newline at end of file + """, (nowdate())) diff --git a/erpnext/non_profit/doctype/membership/test_membership.js b/erpnext/non_profit/doctype/membership/test_membership.js deleted file mode 100644 index 24c85c61576..00000000000 --- a/erpnext/non_profit/doctype/membership/test_membership.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: Membership", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Membership - () => frappe.tests.make('Membership', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/non_profit/doctype/membership/test_membership.py b/erpnext/non_profit/doctype/membership/test_membership.py index 31da792e534..5ad2088fc31 100644 --- a/erpnext/non_profit/doctype/membership/test_membership.py +++ b/erpnext/non_profit/doctype/membership/test_membership.py @@ -6,6 +6,7 @@ import unittest import frappe import erpnext from erpnext.non_profit.doctype.member.member import create_member +from erpnext.non_profit.doctype.membership.membership import update_halted_razorpay_subscription from frappe.utils import nowdate, add_months class TestMembership(unittest.TestCase): @@ -13,11 +14,16 @@ class TestMembership(unittest.TestCase): plan = setup_membership() # make test member - self.member_doc = create_member(frappe._dict({ - 'fullname': "_Test_Member", - 'email': "_test_member_erpnext@example.com", - 'plan_id': plan.name - })) + self.member_doc = create_member( + frappe._dict({ + "fullname": "_Test_Member", + "email": "_test_member_erpnext@example.com", + "plan_id": plan.name, + "subscription_id": "sub_DEX6xcJ1HSW4CR", + "customer_id": "cust_C0WlbKhp3aLA7W", + "subscription_status": "Active" + }) + ) self.member_doc.make_customer_and_link() self.member = self.member_doc.name @@ -51,6 +57,20 @@ class TestMembership(unittest.TestCase): "to_date": add_months(nowdate(), 3), }) + def test_halted_memberships(self): + make_membership(self.member, { + "from_date": add_months(nowdate(), 2), + "to_date": add_months(nowdate(), 3) + }) + + self.assertEqual(frappe.db.get_value("Member", self.member, "subscription_status"), "Active") + payload = get_subscription_payload() + update_halted_razorpay_subscription(data=payload) + self.assertEqual(frappe.db.get_value("Member", self.member, "subscription_status"), "Halted") + + def tearDown(self): + frappe.db.rollback() + def set_config(key, value): frappe.db.set_value("Non Profit Settings", None, key, value) @@ -115,4 +135,28 @@ def setup_membership(): else: plan = frappe.get_doc("Membership Type", "_rzpy_test_milythm") - return plan \ No newline at end of file + return plan + +def get_subscription_payload(): + return { + "entity": "event", + "account_id": "acc_BFQ7uQEaa7j2z7", + "event": "subscription.halted", + "contains": [ + "subscription" + ], + "payload": { + "subscription": { + "entity": { + "id": "sub_DEX6xcJ1HSW4CR", + "entity": "subscription", + "plan_id": "_rzpy_test_milythm", + "customer_id": "cust_C0WlbKhp3aLA7W", + "status": "halted", + "notes": { + "Important": "Notes for Internal Reference" + }, + } + } + } + } diff --git a/erpnext/non_profit/doctype/membership_type/membership_type.py b/erpnext/non_profit/doctype/membership_type/membership_type.py index 022829bd3a6..c712b99c3b8 100644 --- a/erpnext/non_profit/doctype/membership_type/membership_type.py +++ b/erpnext/non_profit/doctype/membership_type/membership_type.py @@ -15,4 +15,4 @@ class MembershipType(Document): frappe.throw(_("The Linked Item should be a service item")) def get_membership_type(razorpay_id): - return frappe.db.exists("Membership Type", {"razorpay_plan_id": razorpay_id}) \ No newline at end of file + return frappe.db.exists("Membership Type", {"razorpay_plan_id": razorpay_id}) diff --git a/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.py b/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.py index a84cc2cdb53..50c93516adc 100644 --- a/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.py +++ b/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.py @@ -35,4 +35,4 @@ class NonProfitSettings(Document): def get_plans_for_membership(*args, **kwargs): controller = get_payment_gateway_controller("Razorpay") plans = controller.get_plans() - return [plan.get("item") for plan in plans.get("items")] \ No newline at end of file + return [plan.get("item") for plan in plans.get("items")] diff --git a/erpnext/non_profit/web_form/grant_application/grant_application.js b/erpnext/non_profit/web_form/grant_application/grant_application.js index 7da3f1fb41c..f09e5409192 100644 --- a/erpnext/non_profit/web_form/grant_application/grant_application.js +++ b/erpnext/non_profit/web_form/grant_application/grant_application.js @@ -1,3 +1,3 @@ frappe.ready(function() { // bind events here -}); \ No newline at end of file +}); diff --git a/erpnext/non_profit/web_form/grant_application/grant_application.py b/erpnext/non_profit/web_form/grant_application/grant_application.py index 7666ef6b616..186722a8bf0 100644 --- a/erpnext/non_profit/web_form/grant_application/grant_application.py +++ b/erpnext/non_profit/web_form/grant_application/grant_application.py @@ -4,5 +4,3 @@ def get_context(context): context.no_cache = True context.parents = [dict(label='View All ', route='grant-application', title='View All')] - - diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 31297d5b4be..4c361f634ca 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -217,6 +217,7 @@ execute:frappe.delete_doc_if_exists("DocType", "Bank Reconciliation") erpnext.patches.v13_0.move_doctype_reports_and_notification_from_hr_to_payroll #22-06-2020 erpnext.patches.v13_0.move_payroll_setting_separately_from_hr_settings #22-06-2020 execute:frappe.reload_doc("regional", "doctype", "e_invoice_settings") +erpnext.patches.v12_0.create_itc_reversal_custom_fields erpnext.patches.v13_0.check_is_income_tax_component #22-06-2020 erpnext.patches.v13_0.loyalty_points_entry_for_pos_invoice #22-07-2020 erpnext.patches.v12_0.add_taxjar_integration_field @@ -274,7 +275,6 @@ erpnext.patches.v13_0.rename_discharge_date_in_ip_record erpnext.patches.v12_0.create_taxable_value_field erpnext.patches.v12_0.add_gst_category_in_delivery_note erpnext.patches.v12_0.purchase_receipt_status -erpnext.patches.v12_0.create_itc_reversal_custom_fields erpnext.patches.v13_0.fix_non_unique_represents_company erpnext.patches.v12_0.add_document_type_field_for_italy_einvoicing erpnext.patches.v13_0.make_non_standard_user_type #13-04-2021 @@ -284,6 +284,7 @@ erpnext.patches.v12_0.add_ewaybill_validity_field erpnext.patches.v13_0.germany_make_custom_fields erpnext.patches.v13_0.germany_fill_debtor_creditor_number erpnext.patches.v13_0.set_pos_closing_as_failed +erpnext.patches.v13_0.rename_stop_to_send_birthday_reminders execute:frappe.rename_doc("Workspace", "Loan Management", "Loans", force=True) erpnext.patches.v13_0.update_timesheet_changes erpnext.patches.v13_0.set_training_event_attendance @@ -292,3 +293,18 @@ erpnext.patches.v13_0.bill_for_rejected_quantity_in_purchase_invoice erpnext.patches.v13_0.update_job_card_details erpnext.patches.v13_0.update_level_in_bom #1234sswef erpnext.patches.v13_0.create_gst_payment_entry_fields +erpnext.patches.v13_0.add_missing_fg_item_for_stock_entry +erpnext.patches.v13_0.update_subscription_status_in_memberships +erpnext.patches.v13_0.update_amt_in_work_order_required_items +erpnext.patches.v12_0.show_einvoice_irn_cancelled_field +erpnext.patches.v13_0.delete_orphaned_tables +erpnext.patches.v13_0.update_export_type_for_gst #2021-08-16 +erpnext.patches.v13_0.update_tds_check_field #3 +erpnext.patches.v13_0.update_recipient_email_digest +erpnext.patches.v13_0.shopify_deprecation_warning +erpnext.patches.v13_0.add_custom_field_for_south_africa #2 +erpnext.patches.v13_0.rename_discharge_ordered_date_in_ip_record +erpnext.patches.v13_0.migrate_stripe_api +erpnext.patches.v13_0.reset_clearance_date_for_intracompany_payment_entries +erpnext.patches.v13_0.set_operation_time_based_on_operating_cost +erpnext.patches.v13_0.validate_options_for_data_field diff --git a/erpnext/patches/v10_0/migrate_daily_work_summary_settings_to_daily_work_summary_group.py b/erpnext/patches/v10_0/migrate_daily_work_summary_settings_to_daily_work_summary_group.py index 102b6da8757..daa258e8825 100644 --- a/erpnext/patches/v10_0/migrate_daily_work_summary_settings_to_daily_work_summary_group.py +++ b/erpnext/patches/v10_0/migrate_daily_work_summary_settings_to_daily_work_summary_group.py @@ -48,4 +48,4 @@ def get_previous_setting(): return obj def get_setting_companies(): - return frappe.db.sql("select * from `tabDaily Work Summary Settings Company`", as_dict=True) \ No newline at end of file + return frappe.db.sql("select * from `tabDaily Work Summary Settings Company`", as_dict=True) diff --git a/erpnext/patches/v10_0/rename_offer_letter_to_job_offer.py b/erpnext/patches/v10_0/rename_offer_letter_to_job_offer.py index 2e3095153a5..f832936b10a 100644 --- a/erpnext/patches/v10_0/rename_offer_letter_to_job_offer.py +++ b/erpnext/patches/v10_0/rename_offer_letter_to_job_offer.py @@ -7,4 +7,4 @@ def execute(): frappe.rename_doc("DocType", "Offer Letter Term", "Job Offer Term", force=True) frappe.reload_doc("hr", "doctype", "job_offer") frappe.reload_doc("hr", "doctype", "job_offer_term") - frappe.delete_doc("Print Format", "Offer Letter") \ No newline at end of file + frappe.delete_doc("Print Format", "Offer Letter") diff --git a/erpnext/patches/v10_0/rename_price_to_rate_in_pricing_rule.py b/erpnext/patches/v10_0/rename_price_to_rate_in_pricing_rule.py index 48fa22204d1..a9dd3103100 100644 --- a/erpnext/patches/v10_0/rename_price_to_rate_in_pricing_rule.py +++ b/erpnext/patches/v10_0/rename_price_to_rate_in_pricing_rule.py @@ -11,4 +11,4 @@ def execute(): except Exception as e: if e.args[0]!=1054: - raise \ No newline at end of file + raise diff --git a/erpnext/patches/v11_0/add_default_email_template_for_leave.py b/erpnext/patches/v11_0/add_default_email_template_for_leave.py index f722be26b41..0f1e4966231 100644 --- a/erpnext/patches/v11_0/add_default_email_template_for_leave.py +++ b/erpnext/patches/v11_0/add_default_email_template_for_leave.py @@ -27,4 +27,3 @@ def execute(): 'subject': _("Leave Status Notification"), 'owner': frappe.session.user, }).insert(ignore_permissions=True) - diff --git a/erpnext/patches/v11_0/add_expense_claim_default_account.py b/erpnext/patches/v11_0/add_expense_claim_default_account.py index eecf75568a4..a613bd88497 100644 --- a/erpnext/patches/v11_0/add_expense_claim_default_account.py +++ b/erpnext/patches/v11_0/add_expense_claim_default_account.py @@ -8,4 +8,4 @@ def execute(): for company in companies: if company.default_payable_account is not None: - frappe.db.set_value("Company", company.name, "default_expense_claim_payable_account", company.default_payable_account) \ No newline at end of file + frappe.db.set_value("Company", company.name, "default_expense_claim_payable_account", company.default_payable_account) diff --git a/erpnext/patches/v11_0/add_healthcare_service_unit_tree_root.py b/erpnext/patches/v11_0/add_healthcare_service_unit_tree_root.py index d956052f1a6..a45f39d4340 100644 --- a/erpnext/patches/v11_0/add_healthcare_service_unit_tree_root.py +++ b/erpnext/patches/v11_0/add_healthcare_service_unit_tree_root.py @@ -18,4 +18,3 @@ def execute(): 'is_group': 1, 'company': company }).insert(ignore_permissions=True) - diff --git a/erpnext/patches/v11_0/add_index_on_nestedset_doctypes.py b/erpnext/patches/v11_0/add_index_on_nestedset_doctypes.py index 5a30c780f8c..0243dfb38ed 100644 --- a/erpnext/patches/v11_0/add_index_on_nestedset_doctypes.py +++ b/erpnext/patches/v11_0/add_index_on_nestedset_doctypes.py @@ -8,4 +8,4 @@ def execute(): frappe.reload_doc("assets", "doctype", "Location") for dt in ("Account", "Cost Center", "File", "Employee", "Location", "Task", "Customer Group", "Sales Person", "Territory"): frappe.reload_doctype(dt) - frappe.get_doc("DocType", dt).run_module_method("on_doctype_update") \ No newline at end of file + frappe.get_doc("DocType", dt).run_module_method("on_doctype_update") diff --git a/erpnext/patches/v11_0/add_market_segments.py b/erpnext/patches/v11_0/add_market_segments.py index ed47d4293f7..a8841ef3a44 100644 --- a/erpnext/patches/v11_0/add_market_segments.py +++ b/erpnext/patches/v11_0/add_market_segments.py @@ -9,4 +9,4 @@ def execute(): frappe.local.lang = frappe.db.get_default("lang") or 'en' - add_market_segments() \ No newline at end of file + add_market_segments() diff --git a/erpnext/patches/v11_0/add_sales_stages.py b/erpnext/patches/v11_0/add_sales_stages.py index ac2ae1511ae..d06c6889ff7 100644 --- a/erpnext/patches/v11_0/add_sales_stages.py +++ b/erpnext/patches/v11_0/add_sales_stages.py @@ -8,4 +8,4 @@ def execute(): frappe.local.lang = frappe.db.get_default("lang") or 'en' - add_sale_stages() \ No newline at end of file + add_sale_stages() diff --git a/erpnext/patches/v11_0/check_buying_selling_in_currency_exchange.py b/erpnext/patches/v11_0/check_buying_selling_in_currency_exchange.py index 462f830c183..0a1a36007e5 100644 --- a/erpnext/patches/v11_0/check_buying_selling_in_currency_exchange.py +++ b/erpnext/patches/v11_0/check_buying_selling_in_currency_exchange.py @@ -3,4 +3,4 @@ import frappe def execute(): frappe.reload_doc('setup', 'doctype', 'currency_exchange') - frappe.db.sql("""update `tabCurrency Exchange` set for_buying = 1, for_selling = 1""") \ No newline at end of file + frappe.db.sql("""update `tabCurrency Exchange` set for_buying = 1, for_selling = 1""") diff --git a/erpnext/patches/v11_0/create_salary_structure_assignments.py b/erpnext/patches/v11_0/create_salary_structure_assignments.py index a908c16715a..d3ea7a3c1c0 100644 --- a/erpnext/patches/v11_0/create_salary_structure_assignments.py +++ b/erpnext/patches/v11_0/create_salary_structure_assignments.py @@ -69,4 +69,4 @@ def execute(): except DuplicateAssignment: pass - frappe.db.sql("update `tabSalary Structure` set docstatus=1") \ No newline at end of file + frappe.db.sql("update `tabSalary Structure` set docstatus=1") diff --git a/erpnext/patches/v11_0/drop_column_max_days_allowed.py b/erpnext/patches/v11_0/drop_column_max_days_allowed.py index 591c521efbe..029f75a2258 100644 --- a/erpnext/patches/v11_0/drop_column_max_days_allowed.py +++ b/erpnext/patches/v11_0/drop_column_max_days_allowed.py @@ -4,4 +4,4 @@ import frappe def execute(): if frappe.db.exists("DocType", "Leave Type"): if 'max_days_allowed' in frappe.db.get_table_columns("Leave Type"): - frappe.db.sql("alter table `tabLeave Type` drop column max_days_allowed") \ No newline at end of file + frappe.db.sql("alter table `tabLeave Type` drop column max_days_allowed") diff --git a/erpnext/patches/v11_0/ewaybill_fields_gst_india.py b/erpnext/patches/v11_0/ewaybill_fields_gst_india.py index 9925b70a963..4247c788e33 100644 --- a/erpnext/patches/v11_0/ewaybill_fields_gst_india.py +++ b/erpnext/patches/v11_0/ewaybill_fields_gst_india.py @@ -7,4 +7,4 @@ def execute(): if not company: return - make_custom_fields() \ No newline at end of file + make_custom_fields() diff --git a/erpnext/patches/v11_0/hr_ux_cleanups.py b/erpnext/patches/v11_0/hr_ux_cleanups.py index 80476c8a74c..8d187965011 100644 --- a/erpnext/patches/v11_0/hr_ux_cleanups.py +++ b/erpnext/patches/v11_0/hr_ux_cleanups.py @@ -10,4 +10,3 @@ def execute(): for holiday_list in frappe.get_all('Holiday List'): holiday_list = frappe.get_doc('Holiday List', holiday_list.name) holiday_list.db_set('total_holidays', len(holiday_list.holidays), update_modified = False) - diff --git a/erpnext/patches/v11_0/make_asset_finance_book_against_old_entries.py b/erpnext/patches/v11_0/make_asset_finance_book_against_old_entries.py index ee709ac2d49..dfcf5ab2886 100644 --- a/erpnext/patches/v11_0/make_asset_finance_book_against_old_entries.py +++ b/erpnext/patches/v11_0/make_asset_finance_book_against_old_entries.py @@ -42,4 +42,4 @@ def execute(): 'frequency_of_depreciation': asset_category_doc.frequency_of_depreciation }) - row.db_update() \ No newline at end of file + row.db_update() diff --git a/erpnext/patches/v11_0/make_location_from_warehouse.py b/erpnext/patches/v11_0/make_location_from_warehouse.py index a307e8c3659..8c92b5180d9 100644 --- a/erpnext/patches/v11_0/make_location_from_warehouse.py +++ b/erpnext/patches/v11_0/make_location_from_warehouse.py @@ -28,4 +28,3 @@ def execute(): def get_parent_warehouse_name(warehouse): return frappe.db.get_value('Warehouse', warehouse, 'warehouse_name') - \ No newline at end of file diff --git a/erpnext/patches/v11_0/move_item_defaults_to_child_table_for_multicompany.py b/erpnext/patches/v11_0/move_item_defaults_to_child_table_for_multicompany.py index c7c76355400..6da70b4ce38 100644 --- a/erpnext/patches/v11_0/move_item_defaults_to_child_table_for_multicompany.py +++ b/erpnext/patches/v11_0/move_item_defaults_to_child_table_for_multicompany.py @@ -93,4 +93,4 @@ def execute(): `expense_account`, `income_account`, `buying_cost_center`, `selling_cost_center` ) VALUES {} - '''.format(', '.join(['%s'] * len(to_insert_data))), tuple(to_insert_data)) \ No newline at end of file + '''.format(', '.join(['%s'] * len(to_insert_data))), tuple(to_insert_data)) diff --git a/erpnext/patches/v11_0/move_leave_approvers_from_employee.py b/erpnext/patches/v11_0/move_leave_approvers_from_employee.py index edab34cc58a..ef703d0ea71 100644 --- a/erpnext/patches/v11_0/move_leave_approvers_from_employee.py +++ b/erpnext/patches/v11_0/move_leave_approvers_from_employee.py @@ -31,4 +31,4 @@ def execute(): if not len(department.leave_approvers): department.append("leave_approvers",{ "approver": record.leave_approver - }).db_insert() \ No newline at end of file + }).db_insert() diff --git a/erpnext/patches/v11_0/refactor_autoname_naming.py b/erpnext/patches/v11_0/refactor_autoname_naming.py index b997ba2db22..dd5cb639b1b 100644 --- a/erpnext/patches/v11_0/refactor_autoname_naming.py +++ b/erpnext/patches/v11_0/refactor_autoname_naming.py @@ -117,4 +117,4 @@ def get_series(): def get_series_to_preserve(doctype): series_to_preserve = frappe.db.get_value('DocType', doctype, 'autoname') - return series_to_preserve \ No newline at end of file + return series_to_preserve diff --git a/erpnext/patches/v11_0/refactor_naming_series.py b/erpnext/patches/v11_0/refactor_naming_series.py index b85ab66f148..9f231edea73 100644 --- a/erpnext/patches/v11_0/refactor_naming_series.py +++ b/erpnext/patches/v11_0/refactor_naming_series.py @@ -132,4 +132,4 @@ def get_series_to_preserve(doctype): def get_default_series(doctype): field = frappe.get_meta(doctype).get_field("naming_series") default_series = field.get('default', '') if field else '' - return default_series \ No newline at end of file + return default_series diff --git a/erpnext/patches/v11_0/rename_asset_adjustment_doctype.py b/erpnext/patches/v11_0/rename_asset_adjustment_doctype.py index fad0cf7a45e..923b23048df 100644 --- a/erpnext/patches/v11_0/rename_asset_adjustment_doctype.py +++ b/erpnext/patches/v11_0/rename_asset_adjustment_doctype.py @@ -8,4 +8,4 @@ import frappe def execute(): if frappe.db.table_exists("Asset Adjustment") and not frappe.db.table_exists("Asset Value Adjustment"): frappe.rename_doc('DocType', 'Asset Adjustment', 'Asset Value Adjustment', force=True) - frappe.reload_doc('assets', 'doctype', 'asset_value_adjustment') \ No newline at end of file + frappe.reload_doc('assets', 'doctype', 'asset_value_adjustment') diff --git a/erpnext/patches/v11_0/rename_bom_wo_fields.py b/erpnext/patches/v11_0/rename_bom_wo_fields.py index 882ec84e644..0e6036b0740 100644 --- a/erpnext/patches/v11_0/rename_bom_wo_fields.py +++ b/erpnext/patches/v11_0/rename_bom_wo_fields.py @@ -30,4 +30,4 @@ def execute(): else: frappe.db.sql(""" UPDATE `tab%s` SET transfer_material_against = 'Work Order' - WHERE docstatus < 2""" % (doctype)) \ No newline at end of file + WHERE docstatus < 2""" % (doctype)) diff --git a/erpnext/patches/v11_0/rename_health_insurance.py b/erpnext/patches/v11_0/rename_health_insurance.py index e605071a297..06fc6151675 100644 --- a/erpnext/patches/v11_0/rename_health_insurance.py +++ b/erpnext/patches/v11_0/rename_health_insurance.py @@ -6,4 +6,4 @@ import frappe def execute(): frappe.rename_doc('DocType', 'Health Insurance', 'Employee Health Insurance', force=True) - frappe.reload_doc('hr', 'doctype', 'employee_health_insurance') \ No newline at end of file + frappe.reload_doc('hr', 'doctype', 'employee_health_insurance') diff --git a/erpnext/patches/v11_0/rename_overproduction_percent_field.py b/erpnext/patches/v11_0/rename_overproduction_percent_field.py index 077829f4818..fbf925d955c 100644 --- a/erpnext/patches/v11_0/rename_overproduction_percent_field.py +++ b/erpnext/patches/v11_0/rename_overproduction_percent_field.py @@ -7,4 +7,4 @@ import frappe def execute(): frappe.reload_doc('manufacturing', 'doctype', 'manufacturing_settings') - rename_field('Manufacturing Settings', 'over_production_allowance_percentage', 'overproduction_percentage_for_sales_order') \ No newline at end of file + rename_field('Manufacturing Settings', 'over_production_allowance_percentage', 'overproduction_percentage_for_sales_order') diff --git a/erpnext/patches/v11_0/renamed_from_to_fields_in_project.py b/erpnext/patches/v11_0/renamed_from_to_fields_in_project.py index 4f684400025..d5ca4cc5749 100644 --- a/erpnext/patches/v11_0/renamed_from_to_fields_in_project.py +++ b/erpnext/patches/v11_0/renamed_from_to_fields_in_project.py @@ -10,4 +10,4 @@ def execute(): if frappe.db.has_column('Project', 'from'): rename_field('Project', 'from', 'from_time') - rename_field('Project', 'to', 'to_time') \ No newline at end of file + rename_field('Project', 'to', 'to_time') diff --git a/erpnext/patches/v11_0/set_missing_gst_hsn_code.py b/erpnext/patches/v11_0/set_missing_gst_hsn_code.py index 4353ef80e24..8f8a545c410 100644 --- a/erpnext/patches/v11_0/set_missing_gst_hsn_code.py +++ b/erpnext/patches/v11_0/set_missing_gst_hsn_code.py @@ -41,4 +41,4 @@ def execute(): for t in list(parent): trans_doc = frappe.get_doc(dt, t) hsnwise_tax = get_itemised_tax_breakup_html(trans_doc) - frappe.db.set_value(dt, t, "other_charges_calculation", hsnwise_tax, update_modified=False) \ No newline at end of file + frappe.db.set_value(dt, t, "other_charges_calculation", hsnwise_tax, update_modified=False) diff --git a/erpnext/patches/v11_0/set_salary_component_properties.py b/erpnext/patches/v11_0/set_salary_component_properties.py index 2498888273d..d8ce31f3076 100644 --- a/erpnext/patches/v11_0/set_salary_component_properties.py +++ b/erpnext/patches/v11_0/set_salary_component_properties.py @@ -13,4 +13,4 @@ def execute(): frappe.db.sql("""update `tabSalary Detail` set is_tax_applicable=1 where parentfield='earnings' and statistical_component=0""") frappe.db.sql("""update `tabSalary Detail` set variable_based_on_taxable_salary=1 - where parentfield='deductions' and salary_component in ('TDS', 'Tax Deducted at Source')""") \ No newline at end of file + where parentfield='deductions' and salary_component in ('TDS', 'Tax Deducted at Source')""") diff --git a/erpnext/patches/v11_0/set_user_permissions_for_department.py b/erpnext/patches/v11_0/set_user_permissions_for_department.py index 7bd8577f9c5..2f90f14db3e 100644 --- a/erpnext/patches/v11_0/set_user_permissions_for_department.py +++ b/erpnext/patches/v11_0/set_user_permissions_for_department.py @@ -6,7 +6,7 @@ def execute(): where allow='Department'""", as_dict=1) for d in user_permissions: user_permission = frappe.get_doc("User Permission", d.name) - for new_dept in frappe.db.sql("""select name from tabDepartment + for new_dept in frappe.db.sql("""select name from tabDepartment where ifnull(company, '') != '' and department_name=%s""", d.for_value): try: new_user_permission = frappe.copy_doc(user_permission) @@ -16,4 +16,4 @@ def execute(): pass frappe.reload_doc("hr", "doctype", "department") - frappe.db.sql("update tabDepartment set disabled=1 where ifnull(company, '') = ''") \ No newline at end of file + frappe.db.sql("update tabDepartment set disabled=1 where ifnull(company, '') = ''") diff --git a/erpnext/patches/v11_0/skip_user_permission_check_for_department.py b/erpnext/patches/v11_0/skip_user_permission_check_for_department.py index 0f7fad7e497..4e72917547b 100644 --- a/erpnext/patches/v11_0/skip_user_permission_check_for_department.py +++ b/erpnext/patches/v11_0/skip_user_permission_check_for_department.py @@ -58,4 +58,4 @@ def execute(): if user_permissions_to_delete: frappe.db.sql('DELETE FROM `tabUser Permission` WHERE `name` IN ({})'.format( # nosec ','.join(['%s'] * len(user_permissions_to_delete)) - ), tuple(user_permissions_to_delete)) \ No newline at end of file + ), tuple(user_permissions_to_delete)) diff --git a/erpnext/patches/v11_0/update_account_type_in_party_type.py b/erpnext/patches/v11_0/update_account_type_in_party_type.py index efa04fd2cec..dabaeffc94a 100644 --- a/erpnext/patches/v11_0/update_account_type_in_party_type.py +++ b/erpnext/patches/v11_0/update_account_type_in_party_type.py @@ -10,4 +10,4 @@ def execute(): 'Employee': 'Payable', 'Member': 'Receivable', 'Shareholder': 'Payable', 'Student': 'Receivable'} for party_type, account_type in party_types.items(): - frappe.db.set_value('Party Type', party_type, 'account_type', account_type) \ No newline at end of file + frappe.db.set_value('Party Type', party_type, 'account_type', account_type) diff --git a/erpnext/patches/v11_0/update_allow_transfer_for_manufacture.py b/erpnext/patches/v11_0/update_allow_transfer_for_manufacture.py index 1b58c97ea4d..799e91a3e22 100644 --- a/erpnext/patches/v11_0/update_allow_transfer_for_manufacture.py +++ b/erpnext/patches/v11_0/update_allow_transfer_for_manufacture.py @@ -17,4 +17,4 @@ def execute(): child.include_item_in_manufacturing = 1 where child.item_code = item.name and ifnull(item.is_stock_item, 0) = 1 - """.format(doctype)) \ No newline at end of file + """.format(doctype)) diff --git a/erpnext/patches/v11_0/update_backflush_subcontract_rm_based_on_bom.py b/erpnext/patches/v11_0/update_backflush_subcontract_rm_based_on_bom.py index f2eeadac600..37a616c7021 100644 --- a/erpnext/patches/v11_0/update_backflush_subcontract_rm_based_on_bom.py +++ b/erpnext/patches/v11_0/update_backflush_subcontract_rm_based_on_bom.py @@ -16,4 +16,4 @@ def execute(): where se.purpose = 'Send to Subcontractor' and sed.parent = se.name and pois.rm_item_code = sed.item_code and se.docstatus = 1 - and pois.parenttype = 'Purchase Order'""") \ No newline at end of file + and pois.parenttype = 'Purchase Order'""") diff --git a/erpnext/patches/v11_0/update_brand_in_item_price.py b/erpnext/patches/v11_0/update_brand_in_item_price.py index a8d3fab4812..977d84fefe8 100644 --- a/erpnext/patches/v11_0/update_brand_in_item_price.py +++ b/erpnext/patches/v11_0/update_brand_in_item_price.py @@ -12,4 +12,4 @@ def execute(): `tabItem Price`.brand = `tabItem`.brand where `tabItem Price`.item_code = `tabItem`.name - and `tabItem`.brand is not null and `tabItem`.brand != ''""") \ No newline at end of file + and `tabItem`.brand is not null and `tabItem`.brand != ''""") diff --git a/erpnext/patches/v11_0/update_department_lft_rgt.py b/erpnext/patches/v11_0/update_department_lft_rgt.py index b2f407b18ef..2b382037109 100644 --- a/erpnext/patches/v11_0/update_department_lft_rgt.py +++ b/erpnext/patches/v11_0/update_department_lft_rgt.py @@ -17,4 +17,4 @@ def execute(): frappe.db.sql("""update `tabDepartment` set parent_department = '{0}' where is_group = 0""".format(_('All Departments'))) - rebuild_tree("Department", "parent_department") \ No newline at end of file + rebuild_tree("Department", "parent_department") diff --git a/erpnext/patches/v11_1/delete_bom_browser.py b/erpnext/patches/v11_1/delete_bom_browser.py index 457f5116670..2892674d374 100644 --- a/erpnext/patches/v11_1/delete_bom_browser.py +++ b/erpnext/patches/v11_1/delete_bom_browser.py @@ -5,4 +5,4 @@ from __future__ import unicode_literals import frappe def execute(): - frappe.delete_doc_if_exists('Page', 'bom-browser') \ No newline at end of file + frappe.delete_doc_if_exists('Page', 'bom-browser') diff --git a/erpnext/patches/v11_1/make_job_card_time_logs.py b/erpnext/patches/v11_1/make_job_card_time_logs.py index 6e708df48d8..b706e5c1ffb 100644 --- a/erpnext/patches/v11_1/make_job_card_time_logs.py +++ b/erpnext/patches/v11_1/make_job_card_time_logs.py @@ -26,4 +26,4 @@ def execute(): frappe.reload_doc('manufacturing', 'doctype', 'job_card') frappe.db.sql(""" update `tabJob Card` set total_completed_qty = for_quantity, - total_time_in_mins = time_in_mins where docstatus < 2 """) \ No newline at end of file + total_time_in_mins = time_in_mins where docstatus < 2 """) diff --git a/erpnext/patches/v11_1/move_customer_lead_to_dynamic_column.py b/erpnext/patches/v11_1/move_customer_lead_to_dynamic_column.py index 5b1251c31cf..fc3ec74083a 100644 --- a/erpnext/patches/v11_1/move_customer_lead_to_dynamic_column.py +++ b/erpnext/patches/v11_1/move_customer_lead_to_dynamic_column.py @@ -11,4 +11,4 @@ def execute(): frappe.reload_doctype("Opportunity") frappe.db.sql(""" UPDATE `tabOpportunity` set party_name = lead WHERE opportunity_from = 'Lead' """) - frappe.db.sql(""" UPDATE `tabOpportunity` set party_name = customer WHERE opportunity_from = 'Customer' """) \ No newline at end of file + frappe.db.sql(""" UPDATE `tabOpportunity` set party_name = customer WHERE opportunity_from = 'Customer' """) diff --git a/erpnext/patches/v11_1/rename_depends_on_lwp.py b/erpnext/patches/v11_1/rename_depends_on_lwp.py index a0f2536f7d8..4c4b14fd4e7 100644 --- a/erpnext/patches/v11_1/rename_depends_on_lwp.py +++ b/erpnext/patches/v11_1/rename_depends_on_lwp.py @@ -10,4 +10,4 @@ def execute(): for doctype in ("Salary Component", "Salary Detail"): if "depends_on_lwp" in frappe.db.get_table_columns(doctype): frappe.reload_doc("Payroll", "doctype", scrub(doctype)) - rename_field(doctype, "depends_on_lwp", "depends_on_payment_days") \ No newline at end of file + rename_field(doctype, "depends_on_lwp", "depends_on_payment_days") diff --git a/erpnext/patches/v11_1/renamed_delayed_item_report.py b/erpnext/patches/v11_1/renamed_delayed_item_report.py index 222b9a0b170..8e8725c8af6 100644 --- a/erpnext/patches/v11_1/renamed_delayed_item_report.py +++ b/erpnext/patches/v11_1/renamed_delayed_item_report.py @@ -7,4 +7,4 @@ import frappe def execute(): for report in ["Delayed Order Item Summary", "Delayed Order Summary"]: if frappe.db.exists("Report", report): - frappe.delete_doc("Report", report) \ No newline at end of file + frappe.delete_doc("Report", report) diff --git a/erpnext/patches/v11_1/set_status_for_material_request_type_manufacture.py b/erpnext/patches/v11_1/set_status_for_material_request_type_manufacture.py index d41cff523d5..ec01fbb642e 100644 --- a/erpnext/patches/v11_1/set_status_for_material_request_type_manufacture.py +++ b/erpnext/patches/v11_1/set_status_for_material_request_type_manufacture.py @@ -6,4 +6,4 @@ def execute(): update `tabMaterial Request` set status='Manufactured' where docstatus=1 and material_request_type='Manufacture' and per_ordered=100 and status != 'Stopped' - """) \ No newline at end of file + """) diff --git a/erpnext/patches/v11_1/set_variant_based_on.py b/erpnext/patches/v11_1/set_variant_based_on.py index 019eefd68f4..49a9a177246 100644 --- a/erpnext/patches/v11_1/set_variant_based_on.py +++ b/erpnext/patches/v11_1/set_variant_based_on.py @@ -8,4 +8,4 @@ def execute(): frappe.db.sql("""update tabItem set variant_based_on = 'Item Attribute' where ifnull(variant_based_on, '') = '' and (has_variants=1 or ifnull(variant_of, '') != '') - """) \ No newline at end of file + """) diff --git a/erpnext/patches/v11_1/update_bank_transaction_status.py b/erpnext/patches/v11_1/update_bank_transaction_status.py index 544bc5e6911..354e636c9b0 100644 --- a/erpnext/patches/v11_1/update_bank_transaction_status.py +++ b/erpnext/patches/v11_1/update_bank_transaction_status.py @@ -23,4 +23,4 @@ def execute(): WHERE status = 'Settled' and (deposit = allocated_amount or withdrawal = allocated_amount) and ifnull(allocated_amount, 0) > 0 - """) \ No newline at end of file + """) diff --git a/erpnext/patches/v11_1/update_default_supplier_in_item_defaults.py b/erpnext/patches/v11_1/update_default_supplier_in_item_defaults.py index 347dec1f74d..8c360ad9353 100644 --- a/erpnext/patches/v11_1/update_default_supplier_in_item_defaults.py +++ b/erpnext/patches/v11_1/update_default_supplier_in_item_defaults.py @@ -22,4 +22,4 @@ def execute(): SET `tabItem Default`.default_supplier = `tabItem`.default_supplier WHERE `tabItem Default`.parent = `tabItem`.name and `tabItem Default`.default_supplier is null - and `tabItem`.default_supplier is not null and `tabItem`.default_supplier != '' """) \ No newline at end of file + and `tabItem`.default_supplier is not null and `tabItem`.default_supplier != '' """) diff --git a/erpnext/patches/v11_1/woocommerce_set_creation_user.py b/erpnext/patches/v11_1/woocommerce_set_creation_user.py index 5ccdec6d262..074b904002c 100644 --- a/erpnext/patches/v11_1/woocommerce_set_creation_user.py +++ b/erpnext/patches/v11_1/woocommerce_set_creation_user.py @@ -8,4 +8,4 @@ def execute(): if cint(doc.enable_sync): doc.creation_user = doc.modified_by - doc.save(ignore_permissions=True) \ No newline at end of file + doc.save(ignore_permissions=True) diff --git a/erpnext/patches/v12_0/add_company_link_to_einvoice_settings.py b/erpnext/patches/v12_0/add_company_link_to_einvoice_settings.py index b6bd5fa311c..712eb4f61c2 100644 --- a/erpnext/patches/v12_0/add_company_link_to_einvoice_settings.py +++ b/erpnext/patches/v12_0/add_company_link_to_einvoice_settings.py @@ -3,14 +3,18 @@ import frappe def execute(): company = frappe.get_all('Company', filters = {'country': 'India'}) - if not company or not frappe.db.count('E Invoice User'): + + if not company: return frappe.reload_doc("regional", "doctype", "e_invoice_user") + if not frappe.db.count('E Invoice User'): + return + for creds in frappe.db.get_all('E Invoice User', fields=['name', 'gstin']): company_name = frappe.db.sql(""" select dl.link_name from `tabAddress` a, `tabDynamic Link` dl where a.gstin = %s and dl.parent = a.name and dl.link_doctype = 'Company' """, (creds.get('gstin'))) if company_name and len(company_name) > 0: - frappe.db.set_value('E Invoice User', creds.get('name'), 'company', company_name[0][0]) \ No newline at end of file + frappe.db.set_value('E Invoice User', creds.get('name'), 'company', company_name[0][0]) diff --git a/erpnext/patches/v12_0/add_default_buying_selling_terms_in_company.py b/erpnext/patches/v12_0/add_default_buying_selling_terms_in_company.py index 484f81a7aca..855d21dd992 100644 --- a/erpnext/patches/v12_0/add_default_buying_selling_terms_in_company.py +++ b/erpnext/patches/v12_0/add_default_buying_selling_terms_in_company.py @@ -14,6 +14,6 @@ def execute(): for company in frappe.get_all("Company", ["name", "default_selling_terms", "default_buying_terms"]): if company.default_selling_terms and not company.default_buying_terms: frappe.db.set_value("Company", company.name, "default_buying_terms", company.default_selling_terms) - + frappe.reload_doc("setup", "doctype", "terms_and_conditions") frappe.db.sql("update `tabTerms and Conditions` set selling=1, buying=1, hr=1") diff --git a/erpnext/patches/v12_0/add_document_type_field_for_italy_einvoicing.py b/erpnext/patches/v12_0/add_document_type_field_for_italy_einvoicing.py index 4d649dd0f0c..6fe578dbd95 100644 --- a/erpnext/patches/v12_0/add_document_type_field_for_italy_einvoicing.py +++ b/erpnext/patches/v12_0/add_document_type_field_for_italy_einvoicing.py @@ -15,4 +15,4 @@ def execute(): ] } - create_custom_fields(custom_fields, update=True) \ No newline at end of file + create_custom_fields(custom_fields, update=True) diff --git a/erpnext/patches/v12_0/add_einvoice_status_field.py b/erpnext/patches/v12_0/add_einvoice_status_field.py index 387e88588d9..2dfd30714c8 100644 --- a/erpnext/patches/v12_0/add_einvoice_status_field.py +++ b/erpnext/patches/v12_0/add_einvoice_status_field.py @@ -13,13 +13,13 @@ def execute(): 'Sales Invoice': [ dict(fieldname='einvoice_section', label='E-Invoice Fields', fieldtype='Section Break', insert_after='gst_vehicle_type', print_hide=1, hidden=1), - + dict(fieldname='ack_no', label='Ack. No.', fieldtype='Data', read_only=1, hidden=1, insert_after='einvoice_section', no_copy=1, print_hide=1), - + dict(fieldname='ack_date', label='Ack. Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_no', no_copy=1, print_hide=1), - dict(fieldname='irn_cancel_date', label='Cancel Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_date', + dict(fieldname='irn_cancel_date', label='Cancel Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_date', no_copy=1, print_hide=1), dict(fieldname='signed_einvoice', label='Signed E-Invoice', fieldtype='Code', options='JSON', hidden=1, insert_after='irn_cancel_date', @@ -66,4 +66,4 @@ def execute(): if signed_einvoice: signed_einvoice = json.loads(signed_einvoice) frappe.db.set_value('Sales Invoice', inv.get('name'), 'ack_no', signed_einvoice.get('AckNo'), update_modified=False) - frappe.db.set_value('Sales Invoice', inv.get('name'), 'ack_date', signed_einvoice.get('AckDt'), update_modified=False) \ No newline at end of file + frappe.db.set_value('Sales Invoice', inv.get('name'), 'ack_date', signed_einvoice.get('AckDt'), update_modified=False) diff --git a/erpnext/patches/v12_0/add_einvoice_summary_report_permissions.py b/erpnext/patches/v12_0/add_einvoice_summary_report_permissions.py index bf8f566d32a..c1c11e26006 100644 --- a/erpnext/patches/v12_0/add_einvoice_summary_report_permissions.py +++ b/erpnext/patches/v12_0/add_einvoice_summary_report_permissions.py @@ -15,4 +15,4 @@ def execute(): dict(role='Accounts User'), dict(role='Accounts Manager') ] - )).insert() \ No newline at end of file + )).insert() diff --git a/erpnext/patches/v12_0/add_eway_bill_in_delivery_note.py b/erpnext/patches/v12_0/add_eway_bill_in_delivery_note.py index bb4b0380f88..cf1ed3676bf 100644 --- a/erpnext/patches/v12_0/add_eway_bill_in_delivery_note.py +++ b/erpnext/patches/v12_0/add_eway_bill_in_delivery_note.py @@ -16,4 +16,4 @@ def execute(): 'insert_after': 'customer_name_in_arabic', 'translatable': 0, 'owner': 'Administrator' - }) \ No newline at end of file + }) diff --git a/erpnext/patches/v12_0/add_ewaybill_validity_field.py b/erpnext/patches/v12_0/add_ewaybill_validity_field.py index 87d98f1a563..f29b71437e8 100644 --- a/erpnext/patches/v12_0/add_ewaybill_validity_field.py +++ b/erpnext/patches/v12_0/add_ewaybill_validity_field.py @@ -13,4 +13,4 @@ def execute(): depends_on='ewaybill', read_only=1, allow_on_submit=1, insert_after='ewaybill') ] } - create_custom_fields(custom_fields, update=True) \ No newline at end of file + create_custom_fields(custom_fields, update=True) diff --git a/erpnext/patches/v12_0/add_export_type_field_in_party_master.py b/erpnext/patches/v12_0/add_export_type_field_in_party_master.py index 5bb6e3fb339..a0b1f87d61b 100644 --- a/erpnext/patches/v12_0/add_export_type_field_in_party_master.py +++ b/erpnext/patches/v12_0/add_export_type_field_in_party_master.py @@ -38,5 +38,3 @@ def execute(): WHERE fieldname = 'is_inter_state' AND dt IN ('Sales Taxes and Charges Template', 'Purchase Taxes and Charges Template') """) - - diff --git a/erpnext/patches/v12_0/add_gst_category_in_delivery_note.py b/erpnext/patches/v12_0/add_gst_category_in_delivery_note.py index 1208222504c..c90819238c8 100644 --- a/erpnext/patches/v12_0/add_gst_category_in_delivery_note.py +++ b/erpnext/patches/v12_0/add_gst_category_in_delivery_note.py @@ -16,4 +16,4 @@ def execute(): ] } - create_custom_fields(custom_fields, update=True) \ No newline at end of file + create_custom_fields(custom_fields, update=True) diff --git a/erpnext/patches/v12_0/add_item_name_in_work_orders.py b/erpnext/patches/v12_0/add_item_name_in_work_orders.py index 485dd314a16..d765b93d218 100644 --- a/erpnext/patches/v12_0/add_item_name_in_work_orders.py +++ b/erpnext/patches/v12_0/add_item_name_in_work_orders.py @@ -11,4 +11,4 @@ def execute(): SET wo.item_name = item.item_name """) - frappe.db.commit() \ No newline at end of file + frappe.db.commit() diff --git a/erpnext/patches/v12_0/add_permission_in_lower_deduction.py b/erpnext/patches/v12_0/add_permission_in_lower_deduction.py index af9bf74f30e..2e42368b152 100644 --- a/erpnext/patches/v12_0/add_permission_in_lower_deduction.py +++ b/erpnext/patches/v12_0/add_permission_in_lower_deduction.py @@ -10,4 +10,4 @@ def execute(): add_permission('Lower Deduction Certificate', 'Accounts Manager', 0) update_permission_property('Lower Deduction Certificate', 'Accounts Manager', 0, 'write', 1) - update_permission_property('Lower Deduction Certificate', 'Accounts Manager', 0, 'create', 1) \ No newline at end of file + update_permission_property('Lower Deduction Certificate', 'Accounts Manager', 0, 'create', 1) diff --git a/erpnext/patches/v12_0/create_accounting_dimensions_in_missing_doctypes.py b/erpnext/patches/v12_0/create_accounting_dimensions_in_missing_doctypes.py index 657decfed23..f171542df16 100644 --- a/erpnext/patches/v12_0/create_accounting_dimensions_in_missing_doctypes.py +++ b/erpnext/patches/v12_0/create_accounting_dimensions_in_missing_doctypes.py @@ -39,4 +39,4 @@ def execute(): create_custom_field(doctype, df) frappe.clear_cache(doctype=doctype) - count += 1 \ No newline at end of file + count += 1 diff --git a/erpnext/patches/v12_0/create_default_energy_point_rules.py b/erpnext/patches/v12_0/create_default_energy_point_rules.py index 88233b4cf7f..93d2576bb6d 100644 --- a/erpnext/patches/v12_0/create_default_energy_point_rules.py +++ b/erpnext/patches/v12_0/create_default_energy_point_rules.py @@ -3,4 +3,4 @@ from erpnext.setup.install import create_default_energy_point_rules def execute(): frappe.reload_doc('social', 'doctype', 'energy_point_rule') - create_default_energy_point_rules() \ No newline at end of file + create_default_energy_point_rules() diff --git a/erpnext/patches/v12_0/create_irs_1099_field_united_states.py b/erpnext/patches/v12_0/create_irs_1099_field_united_states.py index 7feaffdf408..23a8f24d780 100644 --- a/erpnext/patches/v12_0/create_irs_1099_field_united_states.py +++ b/erpnext/patches/v12_0/create_irs_1099_field_united_states.py @@ -13,4 +13,4 @@ def execute(): if not company: return - make_custom_fields() \ No newline at end of file + make_custom_fields() diff --git a/erpnext/patches/v12_0/create_itc_reversal_custom_fields.py b/erpnext/patches/v12_0/create_itc_reversal_custom_fields.py index 0078a53cd69..a6230f42771 100644 --- a/erpnext/patches/v12_0/create_itc_reversal_custom_fields.py +++ b/erpnext/patches/v12_0/create_itc_reversal_custom_fields.py @@ -112,4 +112,4 @@ def execute(): 'itc_central_tax': values.get('itc_central_tax'), 'itc_state_tax': values['itc_state_tax'], 'itc_cess_amount': values['itc_cess_amount'], - }) \ No newline at end of file + }) diff --git a/erpnext/patches/v12_0/create_taxable_value_field.py b/erpnext/patches/v12_0/create_taxable_value_field.py index a0c9fcf4cbe..b9ee81df50e 100644 --- a/erpnext/patches/v12_0/create_taxable_value_field.py +++ b/erpnext/patches/v12_0/create_taxable_value_field.py @@ -15,4 +15,4 @@ def execute(): ] } - create_custom_fields(custom_fields, update=True) \ No newline at end of file + create_custom_fields(custom_fields, update=True) diff --git a/erpnext/patches/v12_0/delete_priority_property_setter.py b/erpnext/patches/v12_0/delete_priority_property_setter.py index 59272675430..163855729df 100644 --- a/erpnext/patches/v12_0/delete_priority_property_setter.py +++ b/erpnext/patches/v12_0/delete_priority_property_setter.py @@ -6,4 +6,4 @@ def execute(): WHERE `tabProperty Setter`.doc_type='Issue' AND `tabProperty Setter`.field_name='priority' AND `tabProperty Setter`.property='options' - """) \ No newline at end of file + """) diff --git a/erpnext/patches/v12_0/fix_quotation_expired_status.py b/erpnext/patches/v12_0/fix_quotation_expired_status.py index c8708d80134..ac7e82d2d0d 100644 --- a/erpnext/patches/v12_0/fix_quotation_expired_status.py +++ b/erpnext/patches/v12_0/fix_quotation_expired_status.py @@ -6,23 +6,23 @@ def execute(): # filter out submitted expired quotations which has sales order created cond = "qo.docstatus = 1 and qo.status = 'Expired'" invalid_so_against_quo = """ - SELECT + SELECT so.name FROM `tabSales Order` so, `tabSales Order Item` so_item - WHERE + WHERE so_item.docstatus = 1 and so.docstatus = 1 and so_item.parent = so.name and so_item.prevdoc_docname = qo.name and qo.valid_till < so.transaction_date""" # check if SO was created after quotation expired - + frappe.db.sql( """UPDATE `tabQuotation` qo SET qo.status = 'Expired' WHERE {cond} and exists({invalid_so_against_quo})""" .format(cond=cond, invalid_so_against_quo=invalid_so_against_quo) ) - + valid_so_against_quo = """ - SELECT + SELECT so.name FROM `tabSales Order` so, `tabSales Order Item` so_item - WHERE + WHERE so_item.docstatus = 1 and so.docstatus = 1 and so_item.parent = so.name and so_item.prevdoc_docname = qo.name diff --git a/erpnext/patches/v12_0/move_item_tax_to_item_tax_template.py b/erpnext/patches/v12_0/move_item_tax_to_item_tax_template.py index a6471eb53cd..5c3fa5991c9 100644 --- a/erpnext/patches/v12_0/move_item_tax_to_item_tax_template.py +++ b/erpnext/patches/v12_0/move_item_tax_to_item_tax_template.py @@ -91,8 +91,9 @@ def get_item_tax_template(item_tax_templates, item_tax_map, item_code, parenttyp item_tax_template.title = make_autoname("Item Tax Template-.####") for tax_type, tax_rate in iteritems(item_tax_map): - account_details = frappe.db.get_value("Account", tax_type, ['name', 'account_type'], as_dict=1) + account_details = frappe.db.get_value("Account", tax_type, ['name', 'account_type', 'company'], as_dict=1) if account_details: + item_tax_template.company = account_details.company if account_details.account_type not in ('Tax', 'Chargeable', 'Income Account', 'Expense Account', 'Expenses Included In Valuation'): frappe.db.set_value('Account', account_details.name, 'account_type', 'Chargeable') else: diff --git a/erpnext/patches/v12_0/move_target_distribution_from_parent_to_child.py b/erpnext/patches/v12_0/move_target_distribution_from_parent_to_child.py index 548c1a47175..97badf355d9 100644 --- a/erpnext/patches/v12_0/move_target_distribution_from_parent_to_child.py +++ b/erpnext/patches/v12_0/move_target_distribution_from_parent_to_child.py @@ -19,4 +19,4 @@ def execute(): frappe.delete_doc("Report", "Sales Partner-wise Transaction Summary") frappe.delete_doc("Report", "Sales Person Target Variance Item Group-Wise") - frappe.delete_doc("Report", "Territory Target Variance Item Group-Wise") \ No newline at end of file + frappe.delete_doc("Report", "Territory Target Variance Item Group-Wise") diff --git a/erpnext/patches/v12_0/recalculate_requested_qty_in_bin.py b/erpnext/patches/v12_0/recalculate_requested_qty_in_bin.py index 8267df95e11..46794bebe70 100644 --- a/erpnext/patches/v12_0/recalculate_requested_qty_in_bin.py +++ b/erpnext/patches/v12_0/recalculate_requested_qty_in_bin.py @@ -10,4 +10,4 @@ def execute(): for entry in bin_details: update_bin_qty(entry.get("item_code"), entry.get("warehouse"), { "indented_qty": get_indented_qty(entry.get("item_code"), entry.get("warehouse")) - }) \ No newline at end of file + }) diff --git a/erpnext/patches/v12_0/remove_bank_remittance_custom_fields.py b/erpnext/patches/v12_0/remove_bank_remittance_custom_fields.py index d1446b3227d..be884f94d15 100644 --- a/erpnext/patches/v12_0/remove_bank_remittance_custom_fields.py +++ b/erpnext/patches/v12_0/remove_bank_remittance_custom_fields.py @@ -11,4 +11,4 @@ def execute(): if frappe.db.exists("Custom Field", "Company-bank_remittance_section"): deprecated_fields = ['bank_remittance_section', 'client_code', 'remittance_column_break', 'product_code'] for i in range(len(deprecated_fields)): - frappe.delete_doc("Custom Field", 'Company-'+deprecated_fields[i]) \ No newline at end of file + frappe.delete_doc("Custom Field", 'Company-'+deprecated_fields[i]) diff --git a/erpnext/patches/v12_0/remove_denied_leaves_from_leave_ledger.py b/erpnext/patches/v12_0/remove_denied_leaves_from_leave_ledger.py index 7859606e5cb..4fcffb702a4 100644 --- a/erpnext/patches/v12_0/remove_denied_leaves_from_leave_ledger.py +++ b/erpnext/patches/v12_0/remove_denied_leaves_from_leave_ledger.py @@ -25,4 +25,4 @@ def delete_denied_leaves_from_leave_ledger_entry(leave_application_list): WHERE transaction_type = 'Leave Application' AND transaction_name in (%s) ''' % (', '.join(['%s'] * len(leave_application_list))), #nosec - tuple(leave_application_list)) \ No newline at end of file + tuple(leave_application_list)) diff --git a/erpnext/patches/v12_0/remove_duplicate_leave_ledger_entries.py b/erpnext/patches/v12_0/remove_duplicate_leave_ledger_entries.py index 24286dcebf9..6b1b601db19 100644 --- a/erpnext/patches/v12_0/remove_duplicate_leave_ledger_entries.py +++ b/erpnext/patches/v12_0/remove_duplicate_leave_ledger_entries.py @@ -43,4 +43,4 @@ def delete_duplicate_ledger_entries(duplicate_records_list): AND is_carry_forward = %s AND from_date = %s AND to_date = %s - ''', tuple(d)) \ No newline at end of file + ''', tuple(d)) diff --git a/erpnext/patches/v12_0/rename_account_type_doctype.py b/erpnext/patches/v12_0/rename_account_type_doctype.py index ffb4e937b18..9a08ad45213 100644 --- a/erpnext/patches/v12_0/rename_account_type_doctype.py +++ b/erpnext/patches/v12_0/rename_account_type_doctype.py @@ -4,4 +4,4 @@ import frappe def execute(): frappe.rename_doc('DocType', 'Account Type', 'Bank Account Type', force=True) frappe.rename_doc('DocType', 'Account Subtype', 'Bank Account Subtype', force=True) - frappe.reload_doc('accounts', 'doctype', 'bank_account') \ No newline at end of file + frappe.reload_doc('accounts', 'doctype', 'bank_account') diff --git a/erpnext/patches/v12_0/rename_bank_account_field_in_journal_entry_account.py b/erpnext/patches/v12_0/rename_bank_account_field_in_journal_entry_account.py index 4230cb88f4d..7489ea30a09 100644 --- a/erpnext/patches/v12_0/rename_bank_account_field_in_journal_entry_account.py +++ b/erpnext/patches/v12_0/rename_bank_account_field_in_journal_entry_account.py @@ -14,4 +14,4 @@ def execute(): def update_journal_entry_account_fieldname(): ''' maps data from old field to the new field ''' if frappe.db.has_column('Journal Entry Account', 'bank_account_no'): - rename_field("Journal Entry Account", "bank_account_no", "bank_account") \ No newline at end of file + rename_field("Journal Entry Account", "bank_account_no", "bank_account") diff --git a/erpnext/patches/v12_0/rename_lost_reason_detail.py b/erpnext/patches/v12_0/rename_lost_reason_detail.py index d0dc356bd0e..c71b91c9256 100644 --- a/erpnext/patches/v12_0/rename_lost_reason_detail.py +++ b/erpnext/patches/v12_0/rename_lost_reason_detail.py @@ -15,4 +15,4 @@ def execute(): SELECT o.`name`, o.`creation`, o.`modified`, o.`modified_by`, o.`owner`, o.`docstatus`, o.`parent`, o.`parentfield`, o.`parenttype`, o.`idx`, o.`_comments`, o.`_assign`, o.`_user_tags`, o.`_liked_by`, o.`lost_reason` FROM `tabOpportunity Lost Reason` o LEFT JOIN `tabQuotation Lost Reason` q ON q.name = o.name WHERE q.name IS NULL""") - frappe.delete_doc("DocType", "Lost Reason Detail") \ No newline at end of file + frappe.delete_doc("DocType", "Lost Reason Detail") diff --git a/erpnext/patches/v12_0/rename_pos_closing_doctype.py b/erpnext/patches/v12_0/rename_pos_closing_doctype.py index 0577f81234c..9d8626b8527 100644 --- a/erpnext/patches/v12_0/rename_pos_closing_doctype.py +++ b/erpnext/patches/v12_0/rename_pos_closing_doctype.py @@ -7,10 +7,10 @@ def execute(): if frappe.db.table_exists("POS Closing Voucher"): if not frappe.db.exists("DocType", "POS Closing Entry"): frappe.rename_doc('DocType', 'POS Closing Voucher', 'POS Closing Entry', force=True) - + if not frappe.db.exists('DocType', 'POS Closing Entry Taxes'): frappe.rename_doc('DocType', 'POS Closing Voucher Taxes', 'POS Closing Entry Taxes', force=True) - + if not frappe.db.exists('DocType', 'POS Closing Voucher Details'): frappe.rename_doc('DocType', 'POS Closing Voucher Details', 'POS Closing Entry Detail', force=True) @@ -22,4 +22,4 @@ def execute(): frappe.delete_doc("DocType", "POS Closing Voucher") frappe.delete_doc("DocType", "POS Closing Voucher Taxes") frappe.delete_doc("DocType", "POS Closing Voucher Details") - frappe.delete_doc("DocType", "POS Closing Voucher Invoices") \ No newline at end of file + frappe.delete_doc("DocType", "POS Closing Voucher Invoices") diff --git a/erpnext/patches/v12_0/rename_tolerance_fields.py b/erpnext/patches/v12_0/rename_tolerance_fields.py index aa2fff4ca72..20b096331ed 100644 --- a/erpnext/patches/v12_0/rename_tolerance_fields.py +++ b/erpnext/patches/v12_0/rename_tolerance_fields.py @@ -12,4 +12,4 @@ def execute(): qty_allowance = frappe.db.get_single_value("Stock Settings", "over_delivery_receipt_allowance") frappe.db.set_value("Accounts Settings", None, "over_delivery_receipt_allowance", qty_allowance) - frappe.db.sql("update tabItem set over_billing_allowance=over_delivery_receipt_allowance") \ No newline at end of file + frappe.db.sql("update tabItem set over_billing_allowance=over_delivery_receipt_allowance") diff --git a/erpnext/patches/v12_0/replace_accounting_with_accounts_in_home_settings.py b/erpnext/patches/v12_0/replace_accounting_with_accounts_in_home_settings.py index 09fc4c1b04e..f88a22f6c9d 100644 --- a/erpnext/patches/v12_0/replace_accounting_with_accounts_in_home_settings.py +++ b/erpnext/patches/v12_0/replace_accounting_with_accounts_in_home_settings.py @@ -2,4 +2,4 @@ import frappe def execute(): frappe.db.sql("""UPDATE `tabUser` SET `home_settings` = REPLACE(`home_settings`, 'Accounting', 'Accounts')""") - frappe.cache().delete_key('home_settings') \ No newline at end of file + frappe.cache().delete_key('home_settings') diff --git a/erpnext/patches/v12_0/repost_stock_ledger_entries_for_target_warehouse.py b/erpnext/patches/v12_0/repost_stock_ledger_entries_for_target_warehouse.py index 13e935b2d39..c52f380d8c2 100644 --- a/erpnext/patches/v12_0/repost_stock_ledger_entries_for_target_warehouse.py +++ b/erpnext/patches/v12_0/repost_stock_ledger_entries_for_target_warehouse.py @@ -66,6 +66,3 @@ def execute(): frappe.db.sql(""" UPDATE `tabPacked Item` set target_warehouse = null WHERE creation > '2020-04-16' and docstatus < 2 and parenttype = 'Sales Order' """) - - - diff --git a/erpnext/patches/v12_0/set_automatically_process_deferred_accounting_in_accounts_settings.py b/erpnext/patches/v12_0/set_automatically_process_deferred_accounting_in_accounts_settings.py index 5ee75be4990..b5d7e3dcb9e 100644 --- a/erpnext/patches/v12_0/set_automatically_process_deferred_accounting_in_accounts_settings.py +++ b/erpnext/patches/v12_0/set_automatically_process_deferred_accounting_in_accounts_settings.py @@ -4,4 +4,4 @@ import frappe def execute(): frappe.reload_doc("accounts", "doctype", "accounts_settings") - frappe.db.set_value("Accounts Settings", None, "automatically_process_deferred_accounting_entry", 1) \ No newline at end of file + frappe.db.set_value("Accounts Settings", None, "automatically_process_deferred_accounting_entry", 1) diff --git a/erpnext/patches/v12_0/set_cost_center_in_child_table_of_expense_claim.py b/erpnext/patches/v12_0/set_cost_center_in_child_table_of_expense_claim.py index 8ba0d79a831..4415cfeaba9 100644 --- a/erpnext/patches/v12_0/set_cost_center_in_child_table_of_expense_claim.py +++ b/erpnext/patches/v12_0/set_cost_center_in_child_table_of_expense_claim.py @@ -5,4 +5,4 @@ def execute(): UPDATE `tabExpense Claim Detail` child, `tabExpense Claim` par SET child.cost_center = par.cost_center WHERE child.parent = par.name - """) \ No newline at end of file + """) diff --git a/erpnext/patches/v12_0/set_cwip_and_delete_asset_settings.py b/erpnext/patches/v12_0/set_cwip_and_delete_asset_settings.py index 4d4fc7c4629..13110dfe03f 100644 --- a/erpnext/patches/v12_0/set_cwip_and_delete_asset_settings.py +++ b/erpnext/patches/v12_0/set_cwip_and_delete_asset_settings.py @@ -10,8 +10,8 @@ def execute(): if frappe.db.exists("DocType", "Asset Settings"): frappe.reload_doctype("Asset Category") cwip_value = frappe.db.get_single_value("Asset Settings", "disable_cwip_accounting") - + frappe.db.sql("""UPDATE `tabAsset Category` SET enable_cwip_accounting = %s""", cint(cwip_value)) frappe.db.sql("""DELETE FROM `tabSingles` where doctype = 'Asset Settings'""") - frappe.delete_doc_if_exists("DocType", "Asset Settings") \ No newline at end of file + frappe.delete_doc_if_exists("DocType", "Asset Settings") diff --git a/erpnext/patches/v12_0/set_default_homepage_type.py b/erpnext/patches/v12_0/set_default_homepage_type.py index 241e4b9b5e1..a290e31cf24 100644 --- a/erpnext/patches/v12_0/set_default_homepage_type.py +++ b/erpnext/patches/v12_0/set_default_homepage_type.py @@ -1,4 +1,4 @@ import frappe def execute(): - frappe.db.set_value('Homepage', 'Homepage', 'hero_section_based_on', 'Default') \ No newline at end of file + frappe.db.set_value('Homepage', 'Homepage', 'hero_section_based_on', 'Default') diff --git a/erpnext/patches/v12_0/set_default_payroll_based_on.py b/erpnext/patches/v12_0/set_default_payroll_based_on.py index 04b54a6cf61..038bd6d21ae 100644 --- a/erpnext/patches/v12_0/set_default_payroll_based_on.py +++ b/erpnext/patches/v12_0/set_default_payroll_based_on.py @@ -3,4 +3,4 @@ import frappe def execute(): frappe.reload_doc("hr", "doctype", "hr_settings") - frappe.db.set_value("HR Settings", None, "payroll_based_on", "Leave") \ No newline at end of file + frappe.db.set_value("HR Settings", None, "payroll_based_on", "Leave") diff --git a/erpnext/patches/v12_0/set_default_shopify_app_type.py b/erpnext/patches/v12_0/set_default_shopify_app_type.py index d040ea7f71c..65958a25afd 100644 --- a/erpnext/patches/v12_0/set_default_shopify_app_type.py +++ b/erpnext/patches/v12_0/set_default_shopify_app_type.py @@ -3,4 +3,4 @@ import frappe def execute(): frappe.reload_doc('erpnext_integrations', 'doctype', 'shopify_settings') - frappe.db.set_value('Shopify Settings', None, 'app_type', 'Private') \ No newline at end of file + frappe.db.set_value('Shopify Settings', None, 'app_type', 'Private') diff --git a/erpnext/patches/v12_0/set_expense_account_in_landed_cost_voucher_taxes.py b/erpnext/patches/v12_0/set_expense_account_in_landed_cost_voucher_taxes.py index a996a69b3d9..a27c7b24a8c 100644 --- a/erpnext/patches/v12_0/set_expense_account_in_landed_cost_voucher_taxes.py +++ b/erpnext/patches/v12_0/set_expense_account_in_landed_cost_voucher_taxes.py @@ -30,4 +30,4 @@ def execute(): s.docstatus = 1 AND s.company = %s AND t.parent = s.name - """, (account, company)) \ No newline at end of file + """, (account, company)) diff --git a/erpnext/patches/v12_0/set_gst_category.py b/erpnext/patches/v12_0/set_gst_category.py index 55bbdee7edf..cc093953bf4 100644 --- a/erpnext/patches/v12_0/set_gst_category.py +++ b/erpnext/patches/v12_0/set_gst_category.py @@ -48,5 +48,3 @@ def execute(): frappe.db.sql(""" UPDATE `tab{doctype}` t1, `tabAddress` t2, `tabDynamic Link` t3 SET t1.gst_category = "Overseas" where t3.link_name = t1.name and t3.parent = t2.name and t2.country != 'India' """.format(doctype=doctype)) #nosec - - diff --git a/erpnext/patches/v12_0/set_italian_import_supplier_invoice_permissions.py b/erpnext/patches/v12_0/set_italian_import_supplier_invoice_permissions.py index a6011c4dace..8fdc73b8ff1 100644 --- a/erpnext/patches/v12_0/set_italian_import_supplier_invoice_permissions.py +++ b/erpnext/patches/v12_0/set_italian_import_supplier_invoice_permissions.py @@ -9,4 +9,4 @@ def execute(): countries = frappe.get_all("Company", fields="country") countries = [country["country"] for country in countries] if "Italy" in countries: - add_permissions() \ No newline at end of file + add_permissions() diff --git a/erpnext/patches/v12_0/set_multi_uom_in_rfq.py b/erpnext/patches/v12_0/set_multi_uom_in_rfq.py index 70ca6b222e9..a5c8f7524a7 100644 --- a/erpnext/patches/v12_0/set_multi_uom_in_rfq.py +++ b/erpnext/patches/v12_0/set_multi_uom_in_rfq.py @@ -13,4 +13,4 @@ def execute(): SET stock_uom = uom, conversion_factor = 1, - stock_qty = qty""") \ No newline at end of file + stock_qty = qty""") diff --git a/erpnext/patches/v12_0/set_payment_entry_status.py b/erpnext/patches/v12_0/set_payment_entry_status.py index fafbec6a9a7..84645a38639 100644 --- a/erpnext/patches/v12_0/set_payment_entry_status.py +++ b/erpnext/patches/v12_0/set_payment_entry_status.py @@ -6,4 +6,4 @@ def execute(): WHEN docstatus = 1 THEN 'Submitted' WHEN docstatus = 2 THEN 'Cancelled' ELSE 'Draft' - END;""") \ No newline at end of file + END;""") diff --git a/erpnext/patches/v12_0/set_priority_for_support.py b/erpnext/patches/v12_0/set_priority_for_support.py index a5490ef20d5..66696bee541 100644 --- a/erpnext/patches/v12_0/set_priority_for_support.py +++ b/erpnext/patches/v12_0/set_priority_for_support.py @@ -81,4 +81,4 @@ def set_priorities_service_level_agreement(): doc.flags.ignore_validate = True doc.save(ignore_permissions=True) except frappe.db.TableMissingError: - frappe.reload_doc("support", "doctype", "service_level_agreement") \ No newline at end of file + frappe.reload_doc("support", "doctype", "service_level_agreement") diff --git a/erpnext/patches/v12_0/set_produced_qty_field_in_sales_order_for_work_order.py b/erpnext/patches/v12_0/set_produced_qty_field_in_sales_order_for_work_order.py index 07026732fd4..6c11cb415f9 100644 --- a/erpnext/patches/v12_0/set_produced_qty_field_in_sales_order_for_work_order.py +++ b/erpnext/patches/v12_0/set_produced_qty_field_in_sales_order_for_work_order.py @@ -11,4 +11,4 @@ def execute(): filters={'sales_order': ('!=', ''), 'sales_order_item': ('!=', '')}): # update produced qty in sales order - update_produced_qty_in_so_item(d.sales_order, d.sales_order_item) \ No newline at end of file + update_produced_qty_in_so_item(d.sales_order, d.sales_order_item) diff --git a/erpnext/patches/v12_0/set_production_capacity_in_workstation.py b/erpnext/patches/v12_0/set_production_capacity_in_workstation.py index bae1e28deb9..babaebeaefc 100644 --- a/erpnext/patches/v12_0/set_production_capacity_in_workstation.py +++ b/erpnext/patches/v12_0/set_production_capacity_in_workstation.py @@ -5,4 +5,4 @@ def execute(): frappe.reload_doc("manufacturing", "doctype", "workstation") frappe.db.sql(""" UPDATE `tabWorkstation` - SET production_capacity = 1 """) \ No newline at end of file + SET production_capacity = 1 """) diff --git a/erpnext/patches/v12_0/set_quotation_status.py b/erpnext/patches/v12_0/set_quotation_status.py index 64a9080a8fe..87643a23545 100644 --- a/erpnext/patches/v12_0/set_quotation_status.py +++ b/erpnext/patches/v12_0/set_quotation_status.py @@ -4,4 +4,4 @@ import frappe def execute(): frappe.db.sql(""" UPDATE `tabQuotation` set status = 'Open' - where docstatus = 1 and status = 'Submitted' """) \ No newline at end of file + where docstatus = 1 and status = 'Submitted' """) diff --git a/erpnext/patches/v12_0/set_updated_purpose_in_pick_list.py b/erpnext/patches/v12_0/set_updated_purpose_in_pick_list.py index 63ca540a8e2..1cc37caba42 100644 --- a/erpnext/patches/v12_0/set_updated_purpose_in_pick_list.py +++ b/erpnext/patches/v12_0/set_updated_purpose_in_pick_list.py @@ -8,4 +8,4 @@ import frappe def execute(): frappe.reload_doc("stock", "doctype", "pick_list") frappe.db.sql("""UPDATE `tabPick List` set purpose = 'Delivery' - WHERE docstatus = 1 and purpose = 'Delivery against Sales Order' """) \ No newline at end of file + WHERE docstatus = 1 and purpose = 'Delivery against Sales Order' """) diff --git a/erpnext/patches/v12_0/setup_einvoice_fields.py b/erpnext/patches/v12_0/setup_einvoice_fields.py index 2474bc3b82c..82b14fc9d60 100644 --- a/erpnext/patches/v12_0/setup_einvoice_fields.py +++ b/erpnext/patches/v12_0/setup_einvoice_fields.py @@ -14,9 +14,9 @@ def execute(): 'Sales Invoice': [ dict(fieldname='irn', label='IRN', fieldtype='Data', read_only=1, insert_after='customer', no_copy=1, print_hide=1, depends_on='eval:in_list(["Registered Regular", "SEZ", "Overseas", "Deemed Export"], doc.gst_category) && doc.irn_cancelled === 0'), - + dict(fieldname='ack_no', label='Ack. No.', fieldtype='Data', read_only=1, hidden=1, insert_after='irn', no_copy=1, print_hide=1), - + dict(fieldname='ack_date', label='Ack. Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_no', no_copy=1, print_hide=1), dict(fieldname='irn_cancelled', label='IRN Cancelled', fieldtype='Check', no_copy=1, print_hide=1, diff --git a/erpnext/patches/v12_0/show_einvoice_irn_cancelled_field.py b/erpnext/patches/v12_0/show_einvoice_irn_cancelled_field.py new file mode 100644 index 00000000000..2319c17b34c --- /dev/null +++ b/erpnext/patches/v12_0/show_einvoice_irn_cancelled_field.py @@ -0,0 +1,12 @@ +from __future__ import unicode_literals +import frappe + +def execute(): + company = frappe.get_all('Company', filters = {'country': 'India'}) + if not company: + return + + irn_cancelled_field = frappe.db.exists('Custom Field', {'dt': 'Sales Invoice', 'fieldname': 'irn_cancelled'}) + if irn_cancelled_field: + frappe.db.set_value('Custom Field', irn_cancelled_field, 'depends_on', 'eval: doc.irn') + frappe.db.set_value('Custom Field', irn_cancelled_field, 'read_only', 0) diff --git a/erpnext/patches/v12_0/stock_entry_enhancements.py b/erpnext/patches/v12_0/stock_entry_enhancements.py index 847d92894be..17fdcd9395a 100644 --- a/erpnext/patches/v12_0/stock_entry_enhancements.py +++ b/erpnext/patches/v12_0/stock_entry_enhancements.py @@ -49,4 +49,4 @@ def add_gst_hsn_code_field(): `tabStock Entry Detail`.gst_hsn_code = `tabItem`.gst_hsn_code Where `tabItem`.name = `tabStock Entry Detail`.item_code and `tabItem`.gst_hsn_code is not null - """) \ No newline at end of file + """) diff --git a/erpnext/patches/v12_0/unhide_cost_center_field.py b/erpnext/patches/v12_0/unhide_cost_center_field.py index 6005ab70726..3474a34af4b 100644 --- a/erpnext/patches/v12_0/unhide_cost_center_field.py +++ b/erpnext/patches/v12_0/unhide_cost_center_field.py @@ -10,4 +10,4 @@ def execute(): WHERE doc_type in ('Sales Invoice', 'Purchase Invoice', 'Payment Entry') AND field_name = 'cost_center' AND property = 'hidden' - """) \ No newline at end of file + """) diff --git a/erpnext/patches/v12_0/update_appointment_reminder_scheduler_entry.py b/erpnext/patches/v12_0/update_appointment_reminder_scheduler_entry.py index 91931eeb3bc..f4516649610 100644 --- a/erpnext/patches/v12_0/update_appointment_reminder_scheduler_entry.py +++ b/erpnext/patches/v12_0/update_appointment_reminder_scheduler_entry.py @@ -4,4 +4,4 @@ def execute(): job = frappe.db.exists('Scheduled Job Type', 'patient_appointment.send_appointment_reminder') if job: method = 'erpnext.healthcare.doctype.patient_appointment.patient_appointment.send_appointment_reminder' - frappe.db.set_value('Scheduled Job Type', job, 'method', method) \ No newline at end of file + frappe.db.set_value('Scheduled Job Type', job, 'method', method) diff --git a/erpnext/patches/v12_0/update_bom_in_so_mr.py b/erpnext/patches/v12_0/update_bom_in_so_mr.py index 309ae4c2ab7..8a871718133 100644 --- a/erpnext/patches/v12_0/update_bom_in_so_mr.py +++ b/erpnext/patches/v12_0/update_bom_in_so_mr.py @@ -16,4 +16,4 @@ def execute(): WHERE child_doc.item_code = item.name and child_doc.docstatus < 2 and item.default_bom is not null and item.default_bom != '' {cond} - """.format(doc = doctype, cond = condition)) \ No newline at end of file + """.format(doc = doctype, cond = condition)) diff --git a/erpnext/patches/v12_0/update_end_date_and_status_in_email_campaign.py b/erpnext/patches/v12_0/update_end_date_and_status_in_email_campaign.py index db71a735def..c45f6221f93 100644 --- a/erpnext/patches/v12_0/update_end_date_and_status_in_email_campaign.py +++ b/erpnext/patches/v12_0/update_end_date_and_status_in_email_campaign.py @@ -21,4 +21,4 @@ def execute(): elif end_date >= today_date: doc.db_set("status", "In Progress") elif end_date < today_date: - doc.db_set("status", "Completed") \ No newline at end of file + doc.db_set("status", "Completed") diff --git a/erpnext/patches/v12_0/update_ewaybill_field_position.py b/erpnext/patches/v12_0/update_ewaybill_field_position.py index c0230c43954..9e5f599d2c8 100644 --- a/erpnext/patches/v12_0/update_ewaybill_field_position.py +++ b/erpnext/patches/v12_0/update_ewaybill_field_position.py @@ -25,4 +25,4 @@ def execute(): 'translatable': 0 }) - ewaybill_field.save() \ No newline at end of file + ewaybill_field.save() diff --git a/erpnext/patches/v12_0/update_gst_category.py b/erpnext/patches/v12_0/update_gst_category.py index 963edad150e..1a54216b885 100644 --- a/erpnext/patches/v12_0/update_gst_category.py +++ b/erpnext/patches/v12_0/update_gst_category.py @@ -16,4 +16,4 @@ def execute(): frappe.db.sql(""" UPDATE `tabPurchase Invoice` set gst_category = 'Unregistered' where gst_category = 'Registered Regular' and ifnull(supplier_gstin, '')='' - """) \ No newline at end of file + """) diff --git a/erpnext/patches/v12_0/update_healthcare_refactored_changes.py b/erpnext/patches/v12_0/update_healthcare_refactored_changes.py index d06c5713d23..d0b04433979 100644 --- a/erpnext/patches/v12_0/update_healthcare_refactored_changes.py +++ b/erpnext/patches/v12_0/update_healthcare_refactored_changes.py @@ -134,4 +134,4 @@ def execute(): status = (CASE WHEN visited >= max_visits THEN 'Completed' ELSE 'Pending' END) - """) \ No newline at end of file + """) diff --git a/erpnext/patches/v12_0/update_is_cancelled_field.py b/erpnext/patches/v12_0/update_is_cancelled_field.py index 0b2e82750b2..4bbec44aa42 100644 --- a/erpnext/patches/v12_0/update_is_cancelled_field.py +++ b/erpnext/patches/v12_0/update_is_cancelled_field.py @@ -12,4 +12,4 @@ def execute(): frappe.reload_doc("stock", "doctype", "stock_ledger_entry") frappe.reload_doc("stock", "doctype", "serial_no") except: - pass \ No newline at end of file + pass diff --git a/erpnext/patches/v12_0/update_item_tax_template_company.py b/erpnext/patches/v12_0/update_item_tax_template_company.py index f7496999b33..e15894df890 100644 --- a/erpnext/patches/v12_0/update_item_tax_template_company.py +++ b/erpnext/patches/v12_0/update_item_tax_template_company.py @@ -10,4 +10,4 @@ def execute(): for tax in doc.taxes: doc.company = frappe.get_value('Account', tax.tax_type, 'company') break - doc.save() \ No newline at end of file + doc.save() diff --git a/erpnext/patches/v12_0/update_owner_fields_in_acc_dimension_custom_fields.py b/erpnext/patches/v12_0/update_owner_fields_in_acc_dimension_custom_fields.py index e4dcecd9bdb..6ebaf48e0e8 100644 --- a/erpnext/patches/v12_0/update_owner_fields_in_acc_dimension_custom_fields.py +++ b/erpnext/patches/v12_0/update_owner_fields_in_acc_dimension_custom_fields.py @@ -14,4 +14,4 @@ def execute(): SET owner = 'Administrator' WHERE fieldname = %s AND dt IN (%s)""" % #nosec - ('%s', ', '.join(['%s']* len(doclist))), tuple([dimension.fieldname] + doclist)) \ No newline at end of file + ('%s', ', '.join(['%s']* len(doclist))), tuple([dimension.fieldname] + doclist)) diff --git a/erpnext/patches/v12_0/update_price_list_currency_in_bom.py b/erpnext/patches/v12_0/update_price_list_currency_in_bom.py index f5e7b947c23..09f07074299 100644 --- a/erpnext/patches/v12_0/update_price_list_currency_in_bom.py +++ b/erpnext/patches/v12_0/update_price_list_currency_in_bom.py @@ -28,4 +28,4 @@ def execute(): plc_conversion_rate = get_exchange_rate(d.currency, d.company_currency, getdate(d.creation), "for_buying") - frappe.db.set_value("BOM", d.name, "plc_conversion_rate", plc_conversion_rate) \ No newline at end of file + frappe.db.set_value("BOM", d.name, "plc_conversion_rate", plc_conversion_rate) diff --git a/erpnext/patches/v12_0/update_state_code_for_daman_and_diu.py b/erpnext/patches/v12_0/update_state_code_for_daman_and_diu.py index 7450e9cd8c0..8dbfa1866d3 100644 --- a/erpnext/patches/v12_0/update_state_code_for_daman_and_diu.py +++ b/erpnext/patches/v12_0/update_state_code_for_daman_and_diu.py @@ -19,4 +19,4 @@ def execute(): gst_state = 'Dadra and Nagar Haveli and Daman and Diu', gst_state_number = 26 WHERE gst_state = 'Daman and Diu' - """) \ No newline at end of file + """) diff --git a/erpnext/patches/v12_0/update_uom_conversion_factor.py b/erpnext/patches/v12_0/update_uom_conversion_factor.py index b5a20aa6fd9..24914fd13bc 100644 --- a/erpnext/patches/v12_0/update_uom_conversion_factor.py +++ b/erpnext/patches/v12_0/update_uom_conversion_factor.py @@ -8,4 +8,4 @@ def execute(): frappe.reload_doc("setup", "doctype", "UOM") frappe.reload_doc("stock", "doctype", "UOM Category") - add_uom_data() \ No newline at end of file + add_uom_data() diff --git a/erpnext/patches/v13_0/add_custom_field_for_south_africa.py b/erpnext/patches/v13_0/add_custom_field_for_south_africa.py new file mode 100644 index 00000000000..73ff1cad5b6 --- /dev/null +++ b/erpnext/patches/v13_0/add_custom_field_for_south_africa.py @@ -0,0 +1,14 @@ +# Copyright (c) 2020, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals +import frappe +from erpnext.regional.south_africa.setup import make_custom_fields, add_permissions + +def execute(): + company = frappe.get_all('Company', filters = {'country': 'South Africa'}) + if not company: + return + + make_custom_fields() + add_permissions() diff --git a/erpnext/patches/v13_0/add_missing_fg_item_for_stock_entry.py b/erpnext/patches/v13_0/add_missing_fg_item_for_stock_entry.py new file mode 100644 index 00000000000..0d8109c41ad --- /dev/null +++ b/erpnext/patches/v13_0/add_missing_fg_item_for_stock_entry.py @@ -0,0 +1,112 @@ +# Copyright (c) 2020, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +import frappe +from frappe.utils import cstr, flt, cint +from erpnext.stock.stock_ledger import make_sl_entries +from erpnext.controllers.stock_controller import create_repost_item_valuation_entry + +def execute(): + if not frappe.db.has_column('Work Order', 'has_batch_no'): + return + + frappe.reload_doc('manufacturing', 'doctype', 'manufacturing_settings') + if cint(frappe.db.get_single_value('Manufacturing Settings', 'make_serial_no_batch_from_work_order')): + return + + frappe.reload_doc('manufacturing', 'doctype', 'work_order') + filters = { + 'docstatus': 1, + 'produced_qty': ('>', 0), + 'creation': ('>=', '2021-06-29 00:00:00'), + 'has_batch_no': 1 + } + + fields = ['name', 'production_item'] + + work_orders = [d.name for d in frappe.get_all('Work Order', filters = filters, fields=fields)] + + if not work_orders: + return + + repost_stock_entries = [] + + stock_entries = frappe.db.sql_list(''' + SELECT + se.name + FROM + `tabStock Entry` se + WHERE + se.purpose = 'Manufacture' and se.docstatus < 2 and se.work_order in %s + and not exists( + select name from `tabStock Entry Detail` sed where sed.parent = se.name and sed.is_finished_item = 1 + ) + ORDER BY + se.posting_date, se.posting_time + ''', (work_orders,)) + + if stock_entries: + print('Length of stock entries', len(stock_entries)) + + for stock_entry in stock_entries: + doc = frappe.get_doc('Stock Entry', stock_entry) + doc.set_work_order_details() + doc.load_items_from_bom() + doc.calculate_rate_and_amount() + set_expense_account(doc) + doc.make_batches('t_warehouse') + + if doc.docstatus == 0: + doc.save() + else: + repost_stock_entry(doc) + repost_stock_entries.append(doc) + + for repost_doc in repost_stock_entries: + repost_future_sle_and_gle(repost_doc) + +def set_expense_account(doc): + for row in doc.items: + if row.is_finished_item and not row.expense_account: + row.expense_account = frappe.get_cached_value('Company', doc.company, 'stock_adjustment_account') + +def repost_stock_entry(doc): + doc.db_update() + for child_row in doc.items: + if child_row.is_finished_item: + child_row.db_update() + + sl_entries = [] + finished_item_row = doc.get_finished_item_row() + get_sle_for_target_warehouse(doc, sl_entries, finished_item_row) + + if sl_entries: + try: + make_sl_entries(sl_entries, True) + except Exception: + print(f'SLE entries not posted for the stock entry {doc.name}') + traceback = frappe.get_traceback() + frappe.log_error(traceback) + +def get_sle_for_target_warehouse(doc, sl_entries, finished_item_row): + for d in doc.get('items'): + if cstr(d.t_warehouse) and finished_item_row and d.name == finished_item_row.name: + sle = doc.get_sl_entries(d, { + "warehouse": cstr(d.t_warehouse), + "actual_qty": flt(d.transfer_qty), + "incoming_rate": flt(d.valuation_rate) + }) + + sle.recalculate_rate = 1 + sl_entries.append(sle) + +def repost_future_sle_and_gle(doc): + args = frappe._dict({ + "posting_date": doc.posting_date, + "posting_time": doc.posting_time, + "voucher_type": doc.doctype, + "voucher_no": doc.name, + "company": doc.company + }) + + create_repost_item_valuation_entry(args) diff --git a/erpnext/patches/v13_0/add_naming_series_to_old_projects.py b/erpnext/patches/v13_0/add_naming_series_to_old_projects.py index 5ed9040f1ed..a7b66f0d2bb 100644 --- a/erpnext/patches/v13_0/add_naming_series_to_old_projects.py +++ b/erpnext/patches/v13_0/add_naming_series_to_old_projects.py @@ -10,4 +10,3 @@ def execute(): naming_series = 'PROJ-.####' WHERE naming_series is NULL""") - diff --git a/erpnext/patches/v13_0/bill_for_rejected_quantity_in_purchase_invoice.py b/erpnext/patches/v13_0/bill_for_rejected_quantity_in_purchase_invoice.py index be85cfdeeff..7de9fa1e23e 100644 --- a/erpnext/patches/v13_0/bill_for_rejected_quantity_in_purchase_invoice.py +++ b/erpnext/patches/v13_0/bill_for_rejected_quantity_in_purchase_invoice.py @@ -5,4 +5,4 @@ def execute(): frappe.reload_doctype("Buying Settings") buying_settings = frappe.get_single("Buying Settings") buying_settings.bill_for_rejected_quantity_in_purchase_invoice = 0 - buying_settings.save() \ No newline at end of file + buying_settings.save() diff --git a/erpnext/patches/v13_0/change_default_pos_print_format.py b/erpnext/patches/v13_0/change_default_pos_print_format.py index 605a29e4778..1e4f383dda6 100644 --- a/erpnext/patches/v13_0/change_default_pos_print_format.py +++ b/erpnext/patches/v13_0/change_default_pos_print_format.py @@ -5,4 +5,4 @@ def execute(): frappe.db.sql( """UPDATE `tabPOS Profile` profile SET profile.`print_format` = 'POS Invoice' - WHERE profile.`print_format` = 'Point of Sale'""") \ No newline at end of file + WHERE profile.`print_format` = 'Point of Sale'""") diff --git a/erpnext/patches/v13_0/check_is_income_tax_component.py b/erpnext/patches/v13_0/check_is_income_tax_component.py index c92d52dcec0..7a52dc88d21 100644 --- a/erpnext/patches/v13_0/check_is_income_tax_component.py +++ b/erpnext/patches/v13_0/check_is_income_tax_component.py @@ -19,10 +19,10 @@ def execute(): ] for doctype in doctypes: - frappe.reload_doc('Payroll', 'doctype', doctype) + frappe.reload_doc('Payroll', 'doctype', doctype, force=True) - reports = ['Professional Tax Deductions', 'Provident Fund Deductions'] + reports = ['Professional Tax Deductions', 'Provident Fund Deductions', 'E-Invoice Summary'] for report in reports: frappe.reload_doc('Regional', 'Report', report) frappe.reload_doc('Regional', 'Report', report) @@ -43,4 +43,4 @@ def execute(): if frappe.db.exists("Salary Component", "Provident Fund"): frappe.db.set_value("Salary Component", "Provident Fund", "component_type", "Provident Fund") if frappe.db.exists("Salary Component", "Professional Tax"): - frappe.db.set_value("Salary Component", "Professional Tax", "component_type", "Professional Tax") \ No newline at end of file + frappe.db.set_value("Salary Component", "Professional Tax", "component_type", "Professional Tax") diff --git a/erpnext/patches/v13_0/convert_qi_parameter_to_link_field.py b/erpnext/patches/v13_0/convert_qi_parameter_to_link_field.py index 289b6a761e3..341955aa35f 100644 --- a/erpnext/patches/v13_0/convert_qi_parameter_to_link_field.py +++ b/erpnext/patches/v13_0/convert_qi_parameter_to_link_field.py @@ -20,4 +20,4 @@ def execute(): "doctype": "Quality Inspection Parameter", "parameter": parameter, "description": parameter - }).insert(ignore_permissions=True) \ No newline at end of file + }).insert(ignore_permissions=True) diff --git a/erpnext/patches/v13_0/create_healthcare_custom_fields_in_stock_entry_detail.py b/erpnext/patches/v13_0/create_healthcare_custom_fields_in_stock_entry_detail.py index 585e5406265..08d4876c0d1 100644 --- a/erpnext/patches/v13_0/create_healthcare_custom_fields_in_stock_entry_detail.py +++ b/erpnext/patches/v13_0/create_healthcare_custom_fields_in_stock_entry_detail.py @@ -7,4 +7,4 @@ def execute(): return if data['custom_fields']: - create_custom_fields(data['custom_fields']) \ No newline at end of file + create_custom_fields(data['custom_fields']) diff --git a/erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py b/erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py index 90dc0e2e18b..9a354537f7d 100644 --- a/erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py +++ b/erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py @@ -74,6 +74,3 @@ def create_assignment(employee, leave_policy, leave_period=None, allocation_exis def get_employee_with_grade(grade): return frappe.get_list("Employee", filters = {"grade": grade}) - - - diff --git a/erpnext/patches/v13_0/create_uae_pos_invoice_fields.py b/erpnext/patches/v13_0/create_uae_pos_invoice_fields.py index 59b2e49b26e..6ad3402ba02 100644 --- a/erpnext/patches/v13_0/create_uae_pos_invoice_fields.py +++ b/erpnext/patches/v13_0/create_uae_pos_invoice_fields.py @@ -15,4 +15,4 @@ def execute(): frappe.reload_doc('accounts', 'doctype', 'pos_invoice') frappe.reload_doc('accounts', 'doctype', 'pos_invoice_item') - make_custom_fields() \ No newline at end of file + make_custom_fields() diff --git a/erpnext/patches/v13_0/delete_old_purchase_reports.py b/erpnext/patches/v13_0/delete_old_purchase_reports.py index 8bdc07ee5b8..57620d3e986 100644 --- a/erpnext/patches/v13_0/delete_old_purchase_reports.py +++ b/erpnext/patches/v13_0/delete_old_purchase_reports.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals import frappe +from erpnext.accounts.utils import check_and_delete_linked_reports def execute(): reports_to_delete = ["Requested Items To Be Ordered", @@ -13,6 +14,7 @@ def execute(): for report in reports_to_delete: if frappe.db.exists("Report", report): delete_auto_email_reports(report) + check_and_delete_linked_reports(report) frappe.delete_doc("Report", report) @@ -20,4 +22,4 @@ def delete_auto_email_reports(report): """ Check for one or multiple Auto Email Reports and delete """ auto_email_reports = frappe.db.get_values("Auto Email Report", {"report": report}, ["name"]) for auto_email_report in auto_email_reports: - frappe.delete_doc("Auto Email Report", auto_email_report[0]) \ No newline at end of file + frappe.delete_doc("Auto Email Report", auto_email_report[0]) diff --git a/erpnext/patches/v13_0/delete_old_sales_reports.py b/erpnext/patches/v13_0/delete_old_sales_reports.py index 0f44865808a..905a42c0c4c 100644 --- a/erpnext/patches/v13_0/delete_old_sales_reports.py +++ b/erpnext/patches/v13_0/delete_old_sales_reports.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals import frappe +from erpnext.accounts.utils import check_and_delete_linked_reports def execute(): reports_to_delete = ["Ordered Items To Be Delivered", "Ordered Items To Be Billed"] @@ -11,6 +12,7 @@ def execute(): for report in reports_to_delete: if frappe.db.exists("Report", report): delete_auto_email_reports(report) + check_and_delete_linked_reports(report) frappe.delete_doc("Report", report) @@ -18,4 +20,4 @@ def delete_auto_email_reports(report): """ Check for one or multiple Auto Email Reports and delete """ auto_email_reports = frappe.db.get_values("Auto Email Report", {"report": report}, ["name"]) for auto_email_report in auto_email_reports: - frappe.delete_doc("Auto Email Report", auto_email_report[0]) \ No newline at end of file + frappe.delete_doc("Auto Email Report", auto_email_report[0]) diff --git a/erpnext/patches/v13_0/delete_orphaned_tables.py b/erpnext/patches/v13_0/delete_orphaned_tables.py new file mode 100644 index 00000000000..50a4a0efcbe --- /dev/null +++ b/erpnext/patches/v13_0/delete_orphaned_tables.py @@ -0,0 +1,69 @@ +# Copyright (c) 2019, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals + +import frappe +from frappe.utils import getdate + +def execute(): + frappe.reload_doc('setup', 'doctype', 'transaction_deletion_record') + + if has_deleted_company_transactions(): + child_doctypes = get_child_doctypes_whose_parent_doctypes_were_affected() + + for doctype in child_doctypes: + docs = frappe.get_all(doctype, fields=['name', 'parent', 'parenttype', 'creation']) + + for doc in docs: + if not frappe.db.exists(doc['parenttype'], doc['parent']): + frappe.db.delete(doctype, {'name': doc['name']}) + + elif check_for_new_doc_with_same_name_as_deleted_parent(doc): + frappe.db.delete(doctype, {'name': doc['name']}) + +def has_deleted_company_transactions(): + return frappe.get_all('Transaction Deletion Record') + +def get_child_doctypes_whose_parent_doctypes_were_affected(): + parent_doctypes = get_affected_doctypes() + child_doctypes = frappe.get_all( + 'DocField', + filters={ + 'fieldtype': 'Table', + 'parent':['in', parent_doctypes] + }, pluck='options') + + return child_doctypes + +def get_affected_doctypes(): + affected_doctypes = [] + tdr_docs = frappe.get_all('Transaction Deletion Record', pluck="name") + + for tdr in tdr_docs: + tdr_doc = frappe.get_doc("Transaction Deletion Record", tdr) + + for doctype in tdr_doc.doctypes: + if is_not_child_table(doctype.doctype_name): + affected_doctypes.append(doctype.doctype_name) + + affected_doctypes = remove_duplicate_items(affected_doctypes) + return affected_doctypes + +def is_not_child_table(doctype): + return not bool(frappe.get_value('DocType', doctype, 'istable')) + +def remove_duplicate_items(affected_doctypes): + return list(set(affected_doctypes)) + +def check_for_new_doc_with_same_name_as_deleted_parent(doc): + """ + Compares creation times of parent and child docs. + Since Transaction Deletion Record resets the naming series after deletion, + it allows the creation of new docs with the same names as the deleted ones. + """ + + parent_creation_time = frappe.db.get_value(doc['parenttype'], doc['parent'], 'creation') + child_creation_time = doc['creation'] + + return getdate(parent_creation_time) > getdate(child_creation_time) diff --git a/erpnext/patches/v13_0/delete_report_requested_items_to_order.py b/erpnext/patches/v13_0/delete_report_requested_items_to_order.py index 94a9fa85a8e..8d6340d44ef 100644 --- a/erpnext/patches/v13_0/delete_report_requested_items_to_order.py +++ b/erpnext/patches/v13_0/delete_report_requested_items_to_order.py @@ -9,4 +9,4 @@ def execute(): frappe.db.sql(""" DELETE FROM `tabReport` WHERE name = 'Requested Items to Order' - """) \ No newline at end of file + """) diff --git a/erpnext/patches/v13_0/drop_razorpay_payload_column.py b/erpnext/patches/v13_0/drop_razorpay_payload_column.py index 8980fd00392..76b8041cd94 100644 --- a/erpnext/patches/v13_0/drop_razorpay_payload_column.py +++ b/erpnext/patches/v13_0/drop_razorpay_payload_column.py @@ -4,4 +4,4 @@ import frappe def execute(): if frappe.db.exists("DocType", "Membership"): if 'webhook_payload' in frappe.db.get_table_columns("Membership"): - frappe.db.sql("alter table `tabMembership` drop column webhook_payload") \ No newline at end of file + frappe.db.sql("alter table `tabMembership` drop column webhook_payload") diff --git a/erpnext/patches/v13_0/fix_non_unique_represents_company.py b/erpnext/patches/v13_0/fix_non_unique_represents_company.py index 61dc824dd4c..f20c73ae102 100644 --- a/erpnext/patches/v13_0/fix_non_unique_represents_company.py +++ b/erpnext/patches/v13_0/fix_non_unique_represents_company.py @@ -5,4 +5,4 @@ def execute(): update tabCustomer set represents_company = NULL where represents_company = '' - """) \ No newline at end of file + """) diff --git a/erpnext/patches/v13_0/germany_fill_debtor_creditor_number.py b/erpnext/patches/v13_0/germany_fill_debtor_creditor_number.py index 11e1e9b3b94..dca43b4193d 100644 --- a/erpnext/patches/v13_0/germany_fill_debtor_creditor_number.py +++ b/erpnext/patches/v13_0/germany_fill_debtor_creditor_number.py @@ -12,7 +12,7 @@ def execute(): German companies used to use a dedicated payable/receivable account for every party to mimick party accounts in the external accounting software "DATEV". This is no longer necessary. The reference ID for DATEV will be - stored in a new custom field "debtor_creditor_number". + stored in a new custom field "debtor_creditor_number". """ company_list = frappe.get_all('Company', filters={'country': 'Germany'}) diff --git a/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py b/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py index 021bb72cae6..c4ad1b7ff4f 100644 --- a/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py +++ b/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py @@ -67,4 +67,4 @@ def execute(): def get_creation_time(): return frappe.db.sql(''' SELECT create_time FROM - INFORMATION_SCHEMA.TABLES where TABLE_NAME = "tabRepost Item Valuation" ''', as_list=1)[0][0] \ No newline at end of file + INFORMATION_SCHEMA.TABLES where TABLE_NAME = "tabRepost Item Valuation" ''', as_list=1)[0][0] diff --git a/erpnext/patches/v13_0/loyalty_points_entry_for_pos_invoice.py b/erpnext/patches/v13_0/loyalty_points_entry_for_pos_invoice.py index ee7734053c1..d2228c3bf31 100644 --- a/erpnext/patches/v13_0/loyalty_points_entry_for_pos_invoice.py +++ b/erpnext/patches/v13_0/loyalty_points_entry_for_pos_invoice.py @@ -9,7 +9,7 @@ def execute(): '''`sales_invoice` field from loyalty point entry is splitted into `invoice_type` & `invoice` fields''' frappe.reload_doc("Accounts", "doctype", "loyalty_point_entry") - + if not frappe.db.has_column('Loyalty Point Entry', 'sales_invoice'): return @@ -17,4 +17,4 @@ def execute(): """UPDATE `tabLoyalty Point Entry` lpe SET lpe.`invoice_type` = 'Sales Invoice', lpe.`invoice` = lpe.`sales_invoice` WHERE lpe.`sales_invoice` IS NOT NULL - AND (lpe.`invoice` IS NULL OR lpe.`invoice` = '')""") \ No newline at end of file + AND (lpe.`invoice` IS NULL OR lpe.`invoice` = '')""") diff --git a/erpnext/patches/v13_0/make_non_standard_user_type.py b/erpnext/patches/v13_0/make_non_standard_user_type.py index a9d7883d40a..73361f00262 100644 --- a/erpnext/patches/v13_0/make_non_standard_user_type.py +++ b/erpnext/patches/v13_0/make_non_standard_user_type.py @@ -21,4 +21,4 @@ def execute(): frappe.flags.ignore_select_perm = True frappe.flags.update_select_perm_after_migrate = True - add_non_standard_user_types() \ No newline at end of file + add_non_standard_user_types() diff --git a/erpnext/patches/v13_0/migrate_stripe_api.py b/erpnext/patches/v13_0/migrate_stripe_api.py new file mode 100644 index 00000000000..355421a1f42 --- /dev/null +++ b/erpnext/patches/v13_0/migrate_stripe_api.py @@ -0,0 +1,7 @@ +import frappe +from frappe.model.utils.rename_field import rename_field + + +def execute(): + frappe.reload_doc("accounts", "doctype", "subscription_plan") + rename_field("Subscription Plan", "payment_plan_id", "product_price_id") diff --git a/erpnext/patches/v13_0/move_branch_code_to_bank_account.py b/erpnext/patches/v13_0/move_branch_code_to_bank_account.py index 833ae2a48fb..24d9196d29f 100644 --- a/erpnext/patches/v13_0/move_branch_code_to_bank_account.py +++ b/erpnext/patches/v13_0/move_branch_code_to_bank_account.py @@ -14,4 +14,4 @@ def execute(): frappe.db.sql("""UPDATE `tabBank` b, `tabBank Account` ba SET ba.branch_code = b.branch_code WHERE ba.bank = b.name AND - ifnull(b.branch_code, '') != '' AND ifnull(ba.branch_code, '') = ''""") \ No newline at end of file + ifnull(b.branch_code, '') != '' AND ifnull(ba.branch_code, '') = ''""") diff --git a/erpnext/patches/v13_0/patch_to_fix_reverse_linking_in_additional_salary_encashment_and_incentive.py b/erpnext/patches/v13_0/patch_to_fix_reverse_linking_in_additional_salary_encashment_and_incentive.py index fde8f864703..15aeb76e53f 100644 --- a/erpnext/patches/v13_0/patch_to_fix_reverse_linking_in_additional_salary_encashment_and_incentive.py +++ b/erpnext/patches/v13_0/patch_to_fix_reverse_linking_in_additional_salary_encashment_and_incentive.py @@ -51,4 +51,3 @@ def execute(): and parent = %s and salary_component = %s """, (salary["name"], comp_type, salary["salary_slip"], salary["salary_component"])) - diff --git a/erpnext/patches/v13_0/rename_discharge_ordered_date_in_ip_record.py b/erpnext/patches/v13_0/rename_discharge_ordered_date_in_ip_record.py new file mode 100644 index 00000000000..52a5885fc2f --- /dev/null +++ b/erpnext/patches/v13_0/rename_discharge_ordered_date_in_ip_record.py @@ -0,0 +1,8 @@ +from __future__ import unicode_literals +import frappe +from frappe.model.utils.rename_field import rename_field + +def execute(): + frappe.reload_doc("Healthcare", "doctype", "Inpatient Record") + if frappe.db.has_column("Inpatient Record", "discharge_ordered_date"): + rename_field("Inpatient Record", "discharge_ordered_date", "discharge_ordered_datetime") diff --git a/erpnext/patches/v13_0/rename_issue_doctype_fields.py b/erpnext/patches/v13_0/rename_issue_doctype_fields.py index fa1dfed6435..4885c0b7afa 100644 --- a/erpnext/patches/v13_0/rename_issue_doctype_fields.py +++ b/erpnext/patches/v13_0/rename_issue_doctype_fields.py @@ -37,10 +37,11 @@ def execute(): if frappe.db.exists('DocType', 'Opportunity'): opportunities = frappe.db.get_all('Opportunity', fields=['name', 'mins_to_first_response'], order_by='creation desc') - frappe.reload_doc('crm', 'doctype', 'opportunity') + frappe.reload_doctype('Opportunity', force=True) rename_field('Opportunity', 'mins_to_first_response', 'first_response_time') # change fieldtype to duration + frappe.reload_doc('crm', 'doctype', 'opportunity', force=True) count = 0 for entry in opportunities: mins_to_first_response = convert_to_seconds(entry.mins_to_first_response, 'Minutes') @@ -58,6 +59,8 @@ def execute(): def convert_to_seconds(value, unit): seconds = 0 + if value == 0: + return seconds if unit == 'Hours': seconds = value * 3600 if unit == 'Minutes': diff --git a/erpnext/patches/v13_0/rename_issue_status_hold_to_on_hold.py b/erpnext/patches/v13_0/rename_issue_status_hold_to_on_hold.py index 48325fc2d43..4ef04ad9b1b 100644 --- a/erpnext/patches/v13_0/rename_issue_status_hold_to_on_hold.py +++ b/erpnext/patches/v13_0/rename_issue_status_hold_to_on_hold.py @@ -17,4 +17,4 @@ def rename_status(): status = 'On Hold' WHERE status = 'Hold' - """) \ No newline at end of file + """) diff --git a/erpnext/patches/v13_0/rename_membership_settings_to_non_profit_settings.py b/erpnext/patches/v13_0/rename_membership_settings_to_non_profit_settings.py index 3fa09a7baaa..f60567b6b21 100644 --- a/erpnext/patches/v13_0/rename_membership_settings_to_non_profit_settings.py +++ b/erpnext/patches/v13_0/rename_membership_settings_to_non_profit_settings.py @@ -19,4 +19,4 @@ def execute(): } for old_name, new_name in rename_fields_map.items(): - rename_field("Non Profit Settings", old_name, new_name) \ No newline at end of file + rename_field("Non Profit Settings", old_name, new_name) diff --git a/erpnext/patches/v13_0/rename_stop_to_send_birthday_reminders.py b/erpnext/patches/v13_0/rename_stop_to_send_birthday_reminders.py new file mode 100644 index 00000000000..1787a560254 --- /dev/null +++ b/erpnext/patches/v13_0/rename_stop_to_send_birthday_reminders.py @@ -0,0 +1,23 @@ +import frappe +from frappe.model.utils.rename_field import rename_field + +def execute(): + frappe.reload_doc('hr', 'doctype', 'hr_settings') + + try: + # Rename the field + rename_field('HR Settings', 'stop_birthday_reminders', 'send_birthday_reminders') + + # Reverse the value + old_value = frappe.db.get_single_value('HR Settings', 'send_birthday_reminders') + + frappe.db.set_value( + 'HR Settings', + 'HR Settings', + 'send_birthday_reminders', + 1 if old_value == 0 else 0 + ) + + except Exception as e: + if e.args[0] != 1054: + raise \ No newline at end of file diff --git a/erpnext/patches/v13_0/replace_pos_page_with_point_of_sale_page.py b/erpnext/patches/v13_0/replace_pos_page_with_point_of_sale_page.py index 390e217cada..d8bcd7f0775 100644 --- a/erpnext/patches/v13_0/replace_pos_page_with_point_of_sale_page.py +++ b/erpnext/patches/v13_0/replace_pos_page_with_point_of_sale_page.py @@ -3,4 +3,4 @@ import frappe def execute(): if frappe.db.exists("Page", "point-of-sale"): - frappe.rename_doc("Page", "pos", "point-of-sale", 1, 1) \ No newline at end of file + frappe.rename_doc("Page", "pos", "point-of-sale", 1, 1) diff --git a/erpnext/patches/v13_0/replace_pos_payment_mode_table.py b/erpnext/patches/v13_0/replace_pos_payment_mode_table.py index 7cb264830ab..bc1fc98e4da 100644 --- a/erpnext/patches/v13_0/replace_pos_payment_mode_table.py +++ b/erpnext/patches/v13_0/replace_pos_payment_mode_table.py @@ -23,5 +23,5 @@ def execute(): pos_payment_method.parentfield = payment_mode.parentfield pos_payment_method.parenttype = payment_mode.parenttype pos_payment_method.db_insert() - + frappe.db.sql("""delete from `tabSales Invoice Payment` where parent=%s""", pos_profile.name) diff --git a/erpnext/patches/v13_0/reset_clearance_date_for_intracompany_payment_entries.py b/erpnext/patches/v13_0/reset_clearance_date_for_intracompany_payment_entries.py new file mode 100644 index 00000000000..1da5275761b --- /dev/null +++ b/erpnext/patches/v13_0/reset_clearance_date_for_intracompany_payment_entries.py @@ -0,0 +1,45 @@ +# Copyright (c) 2019, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals + +import frappe + +def execute(): + """ + Reset Clearance Date for Payment Entries of type Internal Transfer that have only been reconciled with one Bank Transaction. + This will allow the Payment Entries to be reconciled with the second Bank Transaction using the Bank Reconciliation Tool. + """ + + intra_company_pe = get_intra_company_payment_entries_with_clearance_dates() + reconciled_bank_transactions = get_reconciled_bank_transactions(intra_company_pe) + + for payment_entry in reconciled_bank_transactions: + if len(reconciled_bank_transactions[payment_entry]) == 1: + frappe.db.set_value('Payment Entry', payment_entry, 'clearance_date', None) + +def get_intra_company_payment_entries_with_clearance_dates(): + return frappe.get_all( + 'Payment Entry', + filters = { + 'payment_type': 'Internal Transfer', + 'clearance_date': ["not in", None] + }, + pluck = 'name' + ) + +def get_reconciled_bank_transactions(intra_company_pe): + """Returns dictionary where each key:value pair is Payment Entry : List of Bank Transactions reconciled with Payment Entry""" + + reconciled_bank_transactions = {} + + for payment_entry in intra_company_pe: + reconciled_bank_transactions[payment_entry] = frappe.get_all( + 'Bank Transaction Payments', + filters = { + 'payment_entry': payment_entry + }, + pluck='parent' + ) + + return reconciled_bank_transactions \ No newline at end of file diff --git a/erpnext/patches/v13_0/set_company_in_leave_ledger_entry.py b/erpnext/patches/v13_0/set_company_in_leave_ledger_entry.py index 66857c4e659..13ec41ec55e 100644 --- a/erpnext/patches/v13_0/set_company_in_leave_ledger_entry.py +++ b/erpnext/patches/v13_0/set_company_in_leave_ledger_entry.py @@ -4,4 +4,4 @@ def execute(): frappe.reload_doc('HR', 'doctype', 'Leave Allocation') frappe.reload_doc('HR', 'doctype', 'Leave Ledger Entry') frappe.db.sql("""update `tabLeave Ledger Entry` as lle set company = (select company from `tabEmployee` where employee = lle.employee)""") - frappe.db.sql("""update `tabLeave Allocation` as la set company = (select company from `tabEmployee` where employee = la.employee)""") \ No newline at end of file + frappe.db.sql("""update `tabLeave Allocation` as la set company = (select company from `tabEmployee` where employee = la.employee)""") diff --git a/erpnext/patches/v13_0/set_operation_time_based_on_operating_cost.py b/erpnext/patches/v13_0/set_operation_time_based_on_operating_cost.py new file mode 100644 index 00000000000..4acbdd63a00 --- /dev/null +++ b/erpnext/patches/v13_0/set_operation_time_based_on_operating_cost.py @@ -0,0 +1,15 @@ +import frappe + +def execute(): + frappe.reload_doc('manufacturing', 'doctype', 'bom') + frappe.reload_doc('manufacturing', 'doctype', 'bom_operation') + + frappe.db.sql(''' + UPDATE + `tabBOM Operation` + SET + time_in_mins = (operating_cost * 60) / hour_rate + WHERE + time_in_mins = 0 AND operating_cost > 0 + AND hour_rate > 0 AND docstatus = 1 AND parenttype = "BOM" + ''') \ No newline at end of file diff --git a/erpnext/patches/v13_0/set_payment_channel_in_payment_gateway_account.py b/erpnext/patches/v13_0/set_payment_channel_in_payment_gateway_account.py index edca2383930..7f75946af9f 100644 --- a/erpnext/patches/v13_0/set_payment_channel_in_payment_gateway_account.py +++ b/erpnext/patches/v13_0/set_payment_channel_in_payment_gateway_account.py @@ -14,4 +14,4 @@ def set_payment_channel_as_email(): frappe.db.sql(""" UPDATE `tabPayment Gateway Account` SET `payment_channel` = "Email" - """) \ No newline at end of file + """) diff --git a/erpnext/patches/v13_0/set_pos_closing_as_failed.py b/erpnext/patches/v13_0/set_pos_closing_as_failed.py index 1c576db1c7e..7968e74f50f 100644 --- a/erpnext/patches/v13_0/set_pos_closing_as_failed.py +++ b/erpnext/patches/v13_0/set_pos_closing_as_failed.py @@ -4,4 +4,4 @@ import frappe def execute(): frappe.reload_doc('accounts', 'doctype', 'pos_closing_entry') - frappe.db.sql("update `tabPOS Closing Entry` set `status` = 'Failed' where `status` = 'Queued'") \ No newline at end of file + frappe.db.sql("update `tabPOS Closing Entry` set `status` = 'Failed' where `status` = 'Queued'") diff --git a/erpnext/patches/v13_0/set_training_event_attendance.py b/erpnext/patches/v13_0/set_training_event_attendance.py index 18cad8d86c0..3db183fb2ab 100644 --- a/erpnext/patches/v13_0/set_training_event_attendance.py +++ b/erpnext/patches/v13_0/set_training_event_attendance.py @@ -6,4 +6,4 @@ def execute(): frappe.reload_doc('hr', 'doctype', 'training_event_employee') frappe.db.sql("update `tabTraining Event Employee` set `attendance` = 'Present'") - frappe.db.sql("update `tabTraining Event Employee` set `is_mandatory` = 1 where `attendance` = 'Mandatory'") \ No newline at end of file + frappe.db.sql("update `tabTraining Event Employee` set `is_mandatory` = 1 where `attendance` = 'Mandatory'") diff --git a/erpnext/patches/v13_0/set_youtube_video_id.py b/erpnext/patches/v13_0/set_youtube_video_id.py index c3b49eb4fe5..f6104d1579f 100644 --- a/erpnext/patches/v13_0/set_youtube_video_id.py +++ b/erpnext/patches/v13_0/set_youtube_video_id.py @@ -7,4 +7,4 @@ def execute(): for video in frappe.get_all("Video", fields=["name", "url", "youtube_video_id"]): if video.url and not video.youtube_video_id: - frappe.db.set_value("Video", video.name, "youtube_video_id", get_id_from_url(video.url)) \ No newline at end of file + frappe.db.set_value("Video", video.name, "youtube_video_id", get_id_from_url(video.url)) diff --git a/erpnext/patches/v13_0/setting_custom_roles_for_some_regional_reports.py b/erpnext/patches/v13_0/setting_custom_roles_for_some_regional_reports.py index ecc7822e1d7..c8c160fae71 100644 --- a/erpnext/patches/v13_0/setting_custom_roles_for_some_regional_reports.py +++ b/erpnext/patches/v13_0/setting_custom_roles_for_some_regional_reports.py @@ -7,4 +7,4 @@ def execute(): if not company: return - add_custom_roles_for_reports() \ No newline at end of file + add_custom_roles_for_reports() diff --git a/erpnext/patches/v13_0/setup_patient_history_settings_for_standard_doctypes.py b/erpnext/patches/v13_0/setup_patient_history_settings_for_standard_doctypes.py index d927524a3c0..83581dd4144 100644 --- a/erpnext/patches/v13_0/setup_patient_history_settings_for_standard_doctypes.py +++ b/erpnext/patches/v13_0/setup_patient_history_settings_for_standard_doctypes.py @@ -13,4 +13,4 @@ def execute(): frappe.reload_doc("healthcare", "doctype", "Patient History Standard Document Type") frappe.reload_doc("healthcare", "doctype", "Patient History Custom Document Type") - setup_patient_history_settings() \ No newline at end of file + setup_patient_history_settings() diff --git a/erpnext/patches/v13_0/shopify_deprecation_warning.py b/erpnext/patches/v13_0/shopify_deprecation_warning.py new file mode 100644 index 00000000000..8b0f1935cfb --- /dev/null +++ b/erpnext/patches/v13_0/shopify_deprecation_warning.py @@ -0,0 +1,15 @@ +import click +import frappe + + +def execute(): + + frappe.reload_doc("erpnext_integrations", "doctype", "shopify_settings") + if not frappe.db.get_single_value("Shopify Settings", "enable_shopify"): + return + + click.secho( + "Shopify Integration is moved to a separate app and will be removed from ERPNext in version-14.\n" + "Please install the app to continue using the integration: https://github.com/frappe/ecommerce_integrations", + fg="yellow", + ) diff --git a/erpnext/patches/v13_0/stock_entry_enhancements.py b/erpnext/patches/v13_0/stock_entry_enhancements.py index 0bdcc9c0e88..7b93ce35768 100644 --- a/erpnext/patches/v13_0/stock_entry_enhancements.py +++ b/erpnext/patches/v13_0/stock_entry_enhancements.py @@ -8,18 +8,18 @@ def execute(): frappe.reload_doc("stock", "doctype", "stock_entry") if frappe.db.has_column("Stock Entry", "add_to_transit"): frappe.db.sql(""" - UPDATE `tabStock Entry` SET + UPDATE `tabStock Entry` SET stock_entry_type = 'Material Transfer', purpose = 'Material Transfer', add_to_transit = 1 WHERE stock_entry_type = 'Send to Warehouse' """) - frappe.db.sql("""UPDATE `tabStock Entry` SET + frappe.db.sql("""UPDATE `tabStock Entry` SET stock_entry_type = 'Material Transfer', purpose = 'Material Transfer' WHERE stock_entry_type = 'Receive at Warehouse' """) - + frappe.reload_doc("stock", "doctype", "warehouse_type") if not frappe.db.exists('Warehouse Type', 'Transit'): doc = frappe.new_doc('Warehouse Type') @@ -28,4 +28,4 @@ def execute(): frappe.reload_doc("stock", "doctype", "stock_entry_type") frappe.delete_doc_if_exists("Stock Entry Type", "Send to Warehouse") - frappe.delete_doc_if_exists("Stock Entry Type", "Receive at Warehouse") \ No newline at end of file + frappe.delete_doc_if_exists("Stock Entry Type", "Receive at Warehouse") diff --git a/erpnext/patches/v13_0/update_actual_start_and_end_date_in_wo.py b/erpnext/patches/v13_0/update_actual_start_and_end_date_in_wo.py index adfa20e368a..50f233deef4 100644 --- a/erpnext/patches/v13_0/update_actual_start_and_end_date_in_wo.py +++ b/erpnext/patches/v13_0/update_actual_start_and_end_date_in_wo.py @@ -38,4 +38,4 @@ def execute(): jc.production_item = wo.production_item, jc.item_name = wo.item_name WHERE jc.work_order = wo.name and IFNULL(jc.production_item, "") = "" - """) \ No newline at end of file + """) diff --git a/erpnext/patches/v13_0/update_amt_in_work_order_required_items.py b/erpnext/patches/v13_0/update_amt_in_work_order_required_items.py new file mode 100644 index 00000000000..dc9ed18eade --- /dev/null +++ b/erpnext/patches/v13_0/update_amt_in_work_order_required_items.py @@ -0,0 +1,9 @@ +import frappe + +def execute(): + """ Correct amount in child table of required items table.""" + + frappe.reload_doc("manufacturing", "doctype", "work_order") + frappe.reload_doc("manufacturing", "doctype", "work_order_item") + + frappe.db.sql("""UPDATE `tabWork Order Item` SET amount = rate * required_qty""") diff --git a/erpnext/patches/v13_0/update_deferred_settings.py b/erpnext/patches/v13_0/update_deferred_settings.py index a7d82077b76..bcc09527a29 100644 --- a/erpnext/patches/v13_0/update_deferred_settings.py +++ b/erpnext/patches/v13_0/update_deferred_settings.py @@ -8,4 +8,4 @@ def execute(): accounts_settings.book_deferred_entries_based_on = 'Days' accounts_settings.book_deferred_entries_via_journal_entry = 0 accounts_settings.submit_journal_entries = 0 - accounts_settings.save() \ No newline at end of file + accounts_settings.save() diff --git a/erpnext/patches/v13_0/update_export_type_for_gst.py b/erpnext/patches/v13_0/update_export_type_for_gst.py new file mode 100644 index 00000000000..ef70b55d94c --- /dev/null +++ b/erpnext/patches/v13_0/update_export_type_for_gst.py @@ -0,0 +1,32 @@ +import frappe + +def execute(): + company = frappe.get_all('Company', filters = {'country': 'India'}) + if not company: + return + + # Update custom fields + fieldname = frappe.db.get_value('Custom Field', {'dt': 'Customer', 'fieldname': 'export_type'}) + if fieldname: + frappe.db.set_value('Custom Field', fieldname, + { + 'default': '', + 'mandatory_depends_on': 'eval:in_list(["SEZ", "Overseas", "Deemed Export"], doc.gst_category)' + }) + + fieldname = frappe.db.get_value('Custom Field', {'dt': 'Supplier', 'fieldname': 'export_type'}) + if fieldname: + frappe.db.set_value('Custom Field', fieldname, + { + 'default': '', + 'mandatory_depends_on': 'eval:in_list(["SEZ", "Overseas"], doc.gst_category)' + }) + + # Update Customer/Supplier Masters + frappe.db.sql(""" + UPDATE `tabCustomer` set export_type = '' WHERE gst_category NOT IN ('SEZ', 'Overseas', 'Deemed Export') + """) + + frappe.db.sql(""" + UPDATE `tabSupplier` set export_type = '' WHERE gst_category NOT IN ('SEZ', 'Overseas') + """) diff --git a/erpnext/patches/v13_0/update_job_card_details.py b/erpnext/patches/v13_0/update_job_card_details.py index d4e65c6f2f2..733b3a960cf 100644 --- a/erpnext/patches/v13_0/update_job_card_details.py +++ b/erpnext/patches/v13_0/update_job_card_details.py @@ -13,4 +13,4 @@ def execute(): SET jc.hour_rate = wo.hour_rate WHERE jc.operation_id = wo.name and jc.docstatus < 2 and wo.hour_rate > 0 - """) \ No newline at end of file + """) diff --git a/erpnext/patches/v13_0/update_project_template_tasks.py b/erpnext/patches/v13_0/update_project_template_tasks.py index 8cc27d217fe..b41b74205c7 100644 --- a/erpnext/patches/v13_0/update_project_template_tasks.py +++ b/erpnext/patches/v13_0/update_project_template_tasks.py @@ -44,4 +44,4 @@ def execute(): "task": tsk.name, "subject": tsk.subject }) - template.save() \ No newline at end of file + template.save() diff --git a/erpnext/patches/v13_0/update_reason_for_resignation_in_employee.py b/erpnext/patches/v13_0/update_reason_for_resignation_in_employee.py index 792118fbee2..ccdc334f306 100644 --- a/erpnext/patches/v13_0/update_reason_for_resignation_in_employee.py +++ b/erpnext/patches/v13_0/update_reason_for_resignation_in_employee.py @@ -12,4 +12,3 @@ def execute(): SET reason_for_leaving = reason_for_resignation WHERE status = 'Left' and reason_for_leaving is null and reason_for_resignation is not null """) - diff --git a/erpnext/patches/v13_0/update_recipient_email_digest.py b/erpnext/patches/v13_0/update_recipient_email_digest.py new file mode 100644 index 00000000000..ed90e126670 --- /dev/null +++ b/erpnext/patches/v13_0/update_recipient_email_digest.py @@ -0,0 +1,22 @@ +# Copyright (c) 2020, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals +import frappe + +def execute(): + frappe.reload_doc("setup", "doctype", "Email Digest") + frappe.reload_doc("setup", "doctype", "Email Digest Recipient") + email_digests = frappe.db.get_list('Email Digest', fields=['name', 'recipient_list']) + for email_digest in email_digests: + if email_digest.recipient_list: + for recipient in email_digest.recipient_list.split("\n"): + if frappe.db.exists('User', recipient): + doc = frappe.get_doc({ + 'doctype': 'Email Digest Recipient', + 'parenttype': 'Email Digest', + 'parentfield': 'recipients', + 'parent': email_digest.name, + 'recipient': recipient + }) + doc.insert() diff --git a/erpnext/patches/v13_0/update_returned_qty_in_pr_dn.py b/erpnext/patches/v13_0/update_returned_qty_in_pr_dn.py index 7f42cd92e3c..a5769d2957c 100644 --- a/erpnext/patches/v13_0/update_returned_qty_in_pr_dn.py +++ b/erpnext/patches/v13_0/update_returned_qty_in_pr_dn.py @@ -1,22 +1,28 @@ -# Copyright (c) 2019, Frappe and Contributors +# Copyright (c) 2021, Frappe and Contributors # License: GNU General Public License v3. See license.txt - -from __future__ import unicode_literals import frappe +from erpnext.controllers.status_updater import OverAllowanceError def execute(): frappe.reload_doc('stock', 'doctype', 'purchase_receipt') frappe.reload_doc('stock', 'doctype', 'purchase_receipt_item') frappe.reload_doc('stock', 'doctype', 'delivery_note') frappe.reload_doc('stock', 'doctype', 'delivery_note_item') + frappe.reload_doc('stock', 'doctype', 'stock_settings') def update_from_return_docs(doctype): for return_doc in frappe.get_all(doctype, filters={'is_return' : 1, 'docstatus' : 1}): # Update original receipt/delivery document from return return_doc = frappe.get_cached_doc(doctype, return_doc.name) - return_doc.update_prevdoc_status() + try: + return_doc.update_prevdoc_status() + except OverAllowanceError: + frappe.db.rollback() + continue + return_against = frappe.get_doc(doctype, return_doc.return_against) return_against.update_billing_status() + frappe.db.commit() # Set received qty in stock uom in PR, as returned qty is checked against it frappe.db.sql(""" update `tabPurchase Receipt Item` @@ -24,4 +30,4 @@ def execute(): where docstatus = 1 """) for doctype in ('Purchase Receipt', 'Delivery Note'): - update_from_return_docs(doctype) \ No newline at end of file + update_from_return_docs(doctype) diff --git a/erpnext/patches/v13_0/update_subscription.py b/erpnext/patches/v13_0/update_subscription.py index 871ebf17c4e..d25e9c805b7 100644 --- a/erpnext/patches/v13_0/update_subscription.py +++ b/erpnext/patches/v13_0/update_subscription.py @@ -38,4 +38,4 @@ def execute(): UPDATE `tabSubscription Plan` SET price_determination = %s WHERE price_determination = %s - """, (value, key)) \ No newline at end of file + """, (value, key)) diff --git a/erpnext/patches/v13_0/update_subscription_status_in_memberships.py b/erpnext/patches/v13_0/update_subscription_status_in_memberships.py new file mode 100644 index 00000000000..d9c3e453d47 --- /dev/null +++ b/erpnext/patches/v13_0/update_subscription_status_in_memberships.py @@ -0,0 +1,9 @@ +import frappe + +def execute(): + if frappe.db.exists('DocType', 'Member'): + frappe.reload_doc('Non Profit', 'doctype', 'Member') + + if frappe.db.has_column('Member', 'subscription_activated'): + frappe.db.sql('UPDATE `tabMember` SET subscription_status = "Active" WHERE subscription_activated = 1') + frappe.db.sql_ddl('ALTER table `tabMember` DROP COLUMN subscription_activated') diff --git a/erpnext/patches/v13_0/update_tds_check_field.py b/erpnext/patches/v13_0/update_tds_check_field.py new file mode 100644 index 00000000000..341b0e8e2e2 --- /dev/null +++ b/erpnext/patches/v13_0/update_tds_check_field.py @@ -0,0 +1,9 @@ +import frappe + +def execute(): + if frappe.db.has_table("Tax Withholding Category") \ + and frappe.db.has_column("Tax Withholding Category", "round_off_tax_amount"): + frappe.db.sql(""" + UPDATE `tabTax Withholding Category` set round_off_tax_amount = 0 + WHERE round_off_tax_amount IS NULL + """) diff --git a/erpnext/patches/v13_0/update_timesheet_changes.py b/erpnext/patches/v13_0/update_timesheet_changes.py index 93b7f8e59a4..a36c84ea6e2 100644 --- a/erpnext/patches/v13_0/update_timesheet_changes.py +++ b/erpnext/patches/v13_0/update_timesheet_changes.py @@ -22,4 +22,4 @@ def execute(): exchange_rate = 1.0, base_total_billable_amount = total_billable_amount, base_total_billed_amount = total_billed_amount, - base_total_costing_amount = total_costing_amount""".format(base_currency)) \ No newline at end of file + base_total_costing_amount = total_costing_amount""".format(base_currency)) diff --git a/erpnext/patches/v13_0/updates_for_multi_currency_payroll.py b/erpnext/patches/v13_0/updates_for_multi_currency_payroll.py index 340bf4947b6..7d344f9cd7e 100644 --- a/erpnext/patches/v13_0/updates_for_multi_currency_payroll.py +++ b/erpnext/patches/v13_0/updates_for_multi_currency_payroll.py @@ -96,8 +96,8 @@ def execute(): # update currency in following doctypes based on company currency doctypes_for_currency = ['Employee Advance', 'Leave Encashment', 'Employee Benefit Application', - 'Employee Benefit Claim', 'Employee Incentive', 'Additional Salary', - 'Employee Tax Exemption Declaration', 'Employee Tax Exemption Proof Submission', + 'Employee Benefit Claim', 'Employee Incentive', 'Additional Salary', + 'Employee Tax Exemption Declaration', 'Employee Tax Exemption Proof Submission', 'Income Tax Slab', 'Retention Bonus', 'Salary Structure'] for dt in doctypes_for_currency: diff --git a/erpnext/patches/v13_0/validate_options_for_data_field.py b/erpnext/patches/v13_0/validate_options_for_data_field.py new file mode 100644 index 00000000000..568d1a4b0cb --- /dev/null +++ b/erpnext/patches/v13_0/validate_options_for_data_field.py @@ -0,0 +1,25 @@ +# Copyright (c) 2021, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals +import frappe +from frappe.model import data_field_options + +def execute(): + + for field in frappe.get_all('Custom Field', + fields = ['name'], + filters = { + 'fieldtype': 'Data', + 'options': ['!=', None] + }): + + if field not in data_field_options: + frappe.db.sql(""" + UPDATE + `tabCustom Field` + SET + options=NULL + WHERE + name=%s + """, (field)) diff --git a/erpnext/patches/v8_1/removed_roles_from_gst_report_non_indian_account.py b/erpnext/patches/v8_1/removed_roles_from_gst_report_non_indian_account.py index ccb2e0ec74a..55f5f8201fb 100644 --- a/erpnext/patches/v8_1/removed_roles_from_gst_report_non_indian_account.py +++ b/erpnext/patches/v8_1/removed_roles_from_gst_report_non_indian_account.py @@ -15,4 +15,4 @@ def execute(): where parenttype = 'Report' and parent in('GST Sales Register', 'GST Purchase Register', 'GST Itemised Sales Register', - 'GST Itemised Purchase Register', 'Eway Bill')""") \ No newline at end of file + 'GST Itemised Purchase Register', 'Eway Bill')""") diff --git a/erpnext/patches/v8_1/setup_gst_india.py b/erpnext/patches/v8_1/setup_gst_india.py index e8b017d8644..c214990693c 100644 --- a/erpnext/patches/v8_1/setup_gst_india.py +++ b/erpnext/patches/v8_1/setup_gst_india.py @@ -50,4 +50,4 @@ ERPNext Team. try: sendmail_to_system_managers("[Important] ERPNext GST updates", message) except Exception as e: - pass \ No newline at end of file + pass diff --git a/erpnext/payroll/doctype/additional_salary/additional_salary.py b/erpnext/payroll/doctype/additional_salary/additional_salary.py index ebeddf97f9e..381f399e9fa 100644 --- a/erpnext/payroll/doctype/additional_salary/additional_salary.py +++ b/erpnext/payroll/doctype/additional_salary/additional_salary.py @@ -7,6 +7,7 @@ import frappe from frappe.model.document import Document from frappe import _, bold from frappe.utils import getdate, date_diff, comma_and, formatdate +from erpnext.hr.utils import validate_active_employee class AdditionalSalary(Document): def on_submit(self): @@ -19,6 +20,7 @@ class AdditionalSalary(Document): self.update_employee_referral(cancel=True) def validate(self): + validate_active_employee(self.employee) self.validate_dates() self.validate_salary_structure() self.validate_recurring_additional_salary_overlap() diff --git a/erpnext/payroll/doctype/additional_salary/test_additional_salary.js b/erpnext/payroll/doctype/additional_salary/test_additional_salary.js deleted file mode 100644 index c18e1875854..00000000000 --- a/erpnext/payroll/doctype/additional_salary/test_additional_salary.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: Additional Salary", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Additional Salary - () => frappe.tests.make('Additional Salary', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/payroll/doctype/additional_salary/test_additional_salary.py b/erpnext/payroll/doctype/additional_salary/test_additional_salary.py index 4d47f25fcf3..2a9c56179e7 100644 --- a/erpnext/payroll/doctype/additional_salary/test_additional_salary.py +++ b/erpnext/payroll/doctype/additional_salary/test_additional_salary.py @@ -27,7 +27,7 @@ class TestAdditionalSalary(unittest.TestCase): frappe.db.set_value("Employee", emp_id, "relieving_date", add_days(nowdate(), 1800)) salary_structure = make_salary_structure("Test Salary Structure Additional Salary", "Monthly", employee=emp_id) add_sal = get_additional_salary(emp_id) - + ss = make_employee_salary_slip("test_additional@salary.com", "Monthly", salary_structure=salary_structure.name) for earning in ss.earnings: if earning.salary_component == "Recurring Salary Component": diff --git a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py index 27df30a459c..a1cde08a74c 100644 --- a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py +++ b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py @@ -9,10 +9,11 @@ from frappe.utils import date_diff, getdate, rounded, add_days, cstr, cint, flt from frappe.model.document import Document from erpnext.payroll.doctype.payroll_period.payroll_period import get_payroll_period_days, get_period_factor from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment import get_assigned_salary_structure -from erpnext.hr.utils import get_sal_slip_total_benefit_given, get_holidays_for_employee, get_previous_claimed_amount +from erpnext.hr.utils import get_sal_slip_total_benefit_given, get_holiday_dates_for_employee, get_previous_claimed_amount, validate_active_employee class EmployeeBenefitApplication(Document): def validate(self): + validate_active_employee(self.employee) self.validate_duplicate_on_payroll_period() if not self.max_benefits: self.max_benefits = get_max_benefits_remaining(self.employee, self.date, self.payroll_period) @@ -138,7 +139,7 @@ def get_max_benefits_remaining(employee, on_date, payroll_period): # Then the sum multiply with the no of lwp in that period # Include that amount to the prev_sal_slip_flexi_total to get the actual if have_depends_on_payment_days and per_day_amount_total > 0: - holidays = get_holidays_for_employee(employee, payroll_period_obj.start_date, on_date) + holidays = get_holiday_dates_for_employee(employee, payroll_period_obj.start_date, on_date) working_days = date_diff(on_date, payroll_period_obj.start_date) + 1 leave_days = calculate_lwp(employee, payroll_period_obj.start_date, holidays, working_days) leave_days_amount = leave_days * per_day_amount_total @@ -252,4 +253,4 @@ def get_earning_components_max_benefits(employee, date, earning_component): order by name """, salary_structure, earning_component) - return amount if amount else 0 \ No newline at end of file + return amount if amount else 0 diff --git a/erpnext/payroll/doctype/employee_benefit_application/test_employee_benefit_application.js b/erpnext/payroll/doctype/employee_benefit_application/test_employee_benefit_application.js deleted file mode 100644 index b355e1c4366..00000000000 --- a/erpnext/payroll/doctype/employee_benefit_application/test_employee_benefit_application.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: Employee Benefit Application", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Employee Benefit Application - () => frappe.tests.make('Employee Benefit Application', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.py b/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.py index d9937a7bb97..c6713f3aa46 100644 --- a/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.py +++ b/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.py @@ -8,12 +8,13 @@ from frappe import _ from frappe.utils import flt from frappe.model.document import Document from erpnext.payroll.doctype.employee_benefit_application.employee_benefit_application import get_max_benefits -from erpnext.hr.utils import get_previous_claimed_amount +from erpnext.hr.utils import get_previous_claimed_amount, validate_active_employee from erpnext.payroll.doctype.payroll_period.payroll_period import get_payroll_period from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment import get_assigned_salary_structure class EmployeeBenefitClaim(Document): def validate(self): + validate_active_employee(self.employee) max_benefits = get_max_benefits(self.employee, self.claim_date) if not max_benefits or max_benefits <= 0: frappe.throw(_("Employee {0} has no maximum benefit amount").format(self.employee)) diff --git a/erpnext/payroll/doctype/employee_benefit_claim/test_employee_benefit_claim.js b/erpnext/payroll/doctype/employee_benefit_claim/test_employee_benefit_claim.js deleted file mode 100644 index 3c808c0a560..00000000000 --- a/erpnext/payroll/doctype/employee_benefit_claim/test_employee_benefit_claim.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: Employee Benefit Claim", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Employee Benefit Claim - () => frappe.tests.make('Employee Benefit Claim', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/payroll/doctype/employee_incentive/employee_incentive.py b/erpnext/payroll/doctype/employee_incentive/employee_incentive.py index ead3db126f7..6b918ba76d1 100644 --- a/erpnext/payroll/doctype/employee_incentive/employee_incentive.py +++ b/erpnext/payroll/doctype/employee_incentive/employee_incentive.py @@ -6,9 +6,11 @@ from __future__ import unicode_literals import frappe from frappe import _ from frappe.model.document import Document +from erpnext.hr.utils import validate_active_employee class EmployeeIncentive(Document): def validate(self): + validate_active_employee(self.employee) self.validate_salary_structure() def validate_salary_structure(self): diff --git a/erpnext/payroll/doctype/employee_incentive/test_employee_incentive.js b/erpnext/payroll/doctype/employee_incentive/test_employee_incentive.js deleted file mode 100644 index 10bc03701fd..00000000000 --- a/erpnext/payroll/doctype/employee_incentive/test_employee_incentive.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: Employee Incentive", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Employee Incentive - () => frappe.tests.make('Employee Incentive', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/payroll/doctype/employee_tax_exemption_category/test_employee_tax_exemption_category.js b/erpnext/payroll/doctype/employee_tax_exemption_category/test_employee_tax_exemption_category.js deleted file mode 100644 index e0e43c32e3b..00000000000 --- a/erpnext/payroll/doctype/employee_tax_exemption_category/test_employee_tax_exemption_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: Employee Tax Exemption Category", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Employee Tax Exemption Category - () => frappe.tests.make('Employee Tax Exemption Category', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.py b/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.py index fb71a2877a1..e11d60a4649 100644 --- a/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.py +++ b/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.py @@ -8,11 +8,12 @@ from frappe.model.document import Document from frappe import _ from frappe.utils import flt from frappe.model.mapper import get_mapped_doc -from erpnext.hr.utils import validate_tax_declaration, get_total_exemption_amount, \ +from erpnext.hr.utils import validate_tax_declaration, get_total_exemption_amount, validate_active_employee, \ calculate_annual_eligible_hra_exemption, validate_duplicate_exemption_for_payroll_period class EmployeeTaxExemptionDeclaration(Document): def validate(self): + validate_active_employee(self.employee) validate_tax_declaration(self.declarations) validate_duplicate_exemption_for_payroll_period(self.doctype, self.name, self.payroll_period, self.employee) self.set_total_declared_amount() diff --git a/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.js b/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.js deleted file mode 100644 index 274a3a38603..00000000000 --- a/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.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: Employee Tax Exemption Declaration", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Employee Tax Exemption Declaration - () => frappe.tests.make('Employee Tax Exemption Declaration', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.py b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.py index 5bc33a65f2c..8131ae0fa85 100644 --- a/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.py +++ b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.py @@ -7,11 +7,12 @@ import frappe from frappe.model.document import Document from frappe import _ from frappe.utils import flt -from erpnext.hr.utils import validate_tax_declaration, get_total_exemption_amount, \ +from erpnext.hr.utils import validate_tax_declaration, get_total_exemption_amount, validate_active_employee, \ calculate_hra_exemption_for_period, validate_duplicate_exemption_for_payroll_period class EmployeeTaxExemptionProofSubmission(Document): def validate(self): + validate_active_employee(self.employee) validate_tax_declaration(self.tax_exemption_proofs) self.set_total_actual_amount() self.set_total_exemption_amount() diff --git a/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/test_employee_tax_exemption_proof_submission.js b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/test_employee_tax_exemption_proof_submission.js deleted file mode 100644 index cec75087280..00000000000 --- a/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/test_employee_tax_exemption_proof_submission.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: Employee Tax Exemption Proof Submission", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Employee Tax Exemption Proof Submission - () => frappe.tests.make('Employee Tax Exemption Proof Submission', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/payroll/doctype/employee_tax_exemption_sub_category/employee_tax_exemption_sub_category.py b/erpnext/payroll/doctype/employee_tax_exemption_sub_category/employee_tax_exemption_sub_category.py index a8dd7e4d6dd..d3f24c93780 100644 --- a/erpnext/payroll/doctype/employee_tax_exemption_sub_category/employee_tax_exemption_sub_category.py +++ b/erpnext/payroll/doctype/employee_tax_exemption_sub_category/employee_tax_exemption_sub_category.py @@ -13,4 +13,4 @@ class EmployeeTaxExemptionSubCategory(Document): category_max_amount = frappe.db.get_value("Employee Tax Exemption Category", self.exemption_category, "max_amount") if flt(self.max_amount) > flt(category_max_amount): frappe.throw(_("Max Exemption Amount cannot be greater than maximum exemption amount {0} of Tax Exemption Category {1}") - .format(category_max_amount, self.exemption_category)) \ No newline at end of file + .format(category_max_amount, self.exemption_category)) diff --git a/erpnext/payroll/doctype/employee_tax_exemption_sub_category/test_employee_tax_exemption_sub_category.js b/erpnext/payroll/doctype/employee_tax_exemption_sub_category/test_employee_tax_exemption_sub_category.js deleted file mode 100644 index 8a1a6d151dd..00000000000 --- a/erpnext/payroll/doctype/employee_tax_exemption_sub_category/test_employee_tax_exemption_sub_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: Employee Tax Exemption Sub Category", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Employee Tax Exemption Sub Category - () => frappe.tests.make('Employee Tax Exemption Sub Category', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/payroll/doctype/gratuity/gratuity.js b/erpnext/payroll/doctype/gratuity/gratuity.js index 565d2c49f94..377f3c64916 100644 --- a/erpnext/payroll/doctype/gratuity/gratuity.js +++ b/erpnext/payroll/doctype/gratuity/gratuity.js @@ -69,4 +69,4 @@ frappe.ui.form.on('Gratuity', { } } -}); \ No newline at end of file +}); diff --git a/erpnext/payroll/doctype/gratuity/gratuity.py b/erpnext/payroll/doctype/gratuity/gratuity.py index 1acd6e342fd..31a6af3a077 100644 --- a/erpnext/payroll/doctype/gratuity/gratuity.py +++ b/erpnext/payroll/doctype/gratuity/gratuity.py @@ -242,8 +242,11 @@ def get_salary_structure(employee): order_by = "from_date desc")[0].salary_structure def get_last_salary_slip(employee): - return frappe.get_list("Salary Slip", filters = { + salary_slips = frappe.get_list("Salary Slip", filters = { "employee": employee, 'docstatus': 1 }, - order_by = "start_date desc")[0].name - + order_by = "start_date desc" + ) + if not salary_slips: + return + return salary_slips[0].name diff --git a/erpnext/payroll/doctype/gratuity/gratuity_dashboard.py b/erpnext/payroll/doctype/gratuity/gratuity_dashboard.py index 5b2489f22cd..483e346a32d 100644 --- a/erpnext/payroll/doctype/gratuity/gratuity_dashboard.py +++ b/erpnext/payroll/doctype/gratuity/gratuity_dashboard.py @@ -17,4 +17,4 @@ def get_data(): 'items': ['Additional Salary'] } ] - } \ No newline at end of file + } diff --git a/erpnext/payroll/doctype/gratuity/test_gratuity.py b/erpnext/payroll/doctype/gratuity/test_gratuity.py index 7daea2da474..6c3b6fbae58 100644 --- a/erpnext/payroll/doctype/gratuity/test_gratuity.py +++ b/erpnext/payroll/doctype/gratuity/test_gratuity.py @@ -24,6 +24,11 @@ class TestGratuity(unittest.TestCase): frappe.db.sql("DELETE FROM `tabGratuity`") frappe.db.sql("DELETE FROM `tabAdditional Salary` WHERE ref_doctype = 'Gratuity'") + def test_get_last_salary_slip_should_return_none_for_new_employee(self): + new_employee = make_employee("new_employee@salary.com", company='_Test Company') + salary_slip = get_last_salary_slip(new_employee) + assert salary_slip is None + def test_check_gratuity_amount_based_on_current_slab_and_additional_salary_creation(self): employee, sal_slip = create_employee_and_get_last_salary_slip() diff --git a/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.js b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.js index ee6c5df7371..014a121c96a 100644 --- a/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.js +++ b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.js @@ -37,4 +37,4 @@ frappe.ui.form.on('Gratuity Rule Slab', { frappe.throw(__("To(Year) year can not be less than From(year) ")); } } -}); \ No newline at end of file +}); diff --git a/erpnext/payroll/doctype/gratuity_rule/gratuity_rule_dashboard.py b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule_dashboard.py index 0d70163495a..0f27315cfbf 100644 --- a/erpnext/payroll/doctype/gratuity_rule/gratuity_rule_dashboard.py +++ b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule_dashboard.py @@ -10,4 +10,4 @@ def get_data(): 'items': ['Gratuity'] } ] - } \ No newline at end of file + } diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py index 36e728fc992..13cc423fc2c 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py @@ -117,7 +117,6 @@ class PayrollEntry(Document): Creates salary slip for selected employees if already not created """ self.check_permission('write') - self.created = 1 employees = [emp.employee for emp in self.employees] if employees: args = frappe._dict({ @@ -686,7 +685,7 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters): if filters.start_date and filters.end_date: employee_list = get_employee_list(filters) - emp = filters.get('employees') + emp = filters.get('employees') or [] include_employees = [employee.employee for employee in employee_list if employee.employee not in emp] filters.pop('start_date') filters.pop('end_date') diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry_dashboard.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry_dashboard.py index 7af507d119c..0346a7cc594 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry_dashboard.py +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry_dashboard.py @@ -13,4 +13,4 @@ def get_data(): 'items': ['Salary Slip', 'Journal Entry'] } ] - } \ No newline at end of file + } diff --git a/erpnext/payroll/doctype/payroll_period/payroll_period.py b/erpnext/payroll/doctype/payroll_period/payroll_period.py index ef3a6cc0061..66dec075d8f 100644 --- a/erpnext/payroll/doctype/payroll_period/payroll_period.py +++ b/erpnext/payroll/doctype/payroll_period/payroll_period.py @@ -7,7 +7,7 @@ import frappe from frappe import _ from frappe.utils import date_diff, getdate, formatdate, cint, month_diff, flt, add_months from frappe.model.document import Document -from erpnext.hr.utils import get_holidays_for_employee +from erpnext.hr.utils import get_holiday_dates_for_employee class PayrollPeriod(Document): def validate(self): @@ -65,7 +65,7 @@ def get_payroll_period_days(start_date, end_date, employee, company=None): actual_no_of_days = date_diff(getdate(payroll_period[0][2]), getdate(payroll_period[0][1])) + 1 working_days = actual_no_of_days if not cint(frappe.db.get_value("Payroll Settings", None, "include_holidays_in_total_working_days")): - holidays = get_holidays_for_employee(employee, getdate(payroll_period[0][1]), getdate(payroll_period[0][2])) + holidays = get_holiday_dates_for_employee(employee, getdate(payroll_period[0][1]), getdate(payroll_period[0][2])) working_days -= len(holidays) return payroll_period[0][0], working_days, actual_no_of_days return False, False, False diff --git a/erpnext/payroll/doctype/payroll_period/payroll_period_dashboard.py b/erpnext/payroll/doctype/payroll_period/payroll_period_dashboard.py index 4e9c7c9e7cc..e33299559cc 100644 --- a/erpnext/payroll/doctype/payroll_period/payroll_period_dashboard.py +++ b/erpnext/payroll/doctype/payroll_period/payroll_period_dashboard.py @@ -9,4 +9,4 @@ def get_data(): 'items': ['Employee Tax Exemption Proof Submission', 'Employee Tax Exemption Declaration'] }, ], - } \ No newline at end of file + } diff --git a/erpnext/payroll/doctype/payroll_period/test_payroll_period.js b/erpnext/payroll/doctype/payroll_period/test_payroll_period.js deleted file mode 100644 index 8c4ded96f38..00000000000 --- a/erpnext/payroll/doctype/payroll_period/test_payroll_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: Payroll Period", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Payroll Period - () => frappe.tests.make('Payroll Period', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/payroll/doctype/retention_bonus/retention_bonus.py b/erpnext/payroll/doctype/retention_bonus/retention_bonus.py index 049ea265cce..055bea74108 100644 --- a/erpnext/payroll/doctype/retention_bonus/retention_bonus.py +++ b/erpnext/payroll/doctype/retention_bonus/retention_bonus.py @@ -7,11 +7,10 @@ import frappe from frappe.model.document import Document from frappe import _ from frappe.utils import getdate - +from erpnext.hr.utils import validate_active_employee class RetentionBonus(Document): def validate(self): - if frappe.get_value('Employee', self.employee, 'status') != 'Active': - frappe.throw(_('Cannot create Retention Bonus for Left or Inactive Employees')) + validate_active_employee(self.employee) if getdate(self.bonus_payment_date) < getdate(): frappe.throw(_('Bonus Payment Date cannot be a past date')) diff --git a/erpnext/payroll/doctype/retention_bonus/test_retention_bonus.js b/erpnext/payroll/doctype/retention_bonus/test_retention_bonus.js deleted file mode 100644 index a4b95d3cb10..00000000000 --- a/erpnext/payroll/doctype/retention_bonus/test_retention_bonus.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: Retention Bonus", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Retention Bonus - () => frappe.tests.make('Retention Bonus', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/payroll/doctype/salary_component/salary_component.js b/erpnext/payroll/doctype/salary_component/salary_component.js index dbf75140ac1..e9e6f81862c 100644 --- a/erpnext/payroll/doctype/salary_component/salary_component.js +++ b/erpnext/payroll/doctype/salary_component/salary_component.js @@ -4,11 +4,18 @@ frappe.ui.form.on('Salary Component', { setup: function(frm) { frm.set_query("account", "accounts", function(doc, cdt, cdn) { - var d = locals[cdt][cdn]; + let d = frappe.get_doc(cdt, cdn); + + let root_type = "Liability"; + if (frm.doc.type == "Deduction") { + root_type = "Expense"; + } + return { filters: { "is_group": 0, - "company": d.company + "company": d.company, + "root_type": root_type } }; }); diff --git a/erpnext/payroll/doctype/salary_component/test_salary_component.js b/erpnext/payroll/doctype/salary_component/test_salary_component.js deleted file mode 100644 index c47d32d9965..00000000000 --- a/erpnext/payroll/doctype/salary_component/test_salary_component.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: Salary Component", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Salary Component - () => frappe.tests.make('Salary Component', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index 877503b41cb..fbe4742e21c 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -11,6 +11,7 @@ from frappe.model.naming import make_autoname from frappe import msgprint, _ from erpnext.payroll.doctype.payroll_entry.payroll_entry import get_start_end_dates from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee +from erpnext.hr.utils import get_holiday_dates_for_employee from erpnext.utilities.transaction_base import TransactionBase from frappe.utils.background_jobs import enqueue from erpnext.payroll.doctype.additional_salary.additional_salary import get_additional_salaries @@ -19,6 +20,7 @@ from erpnext.payroll.doctype.employee_benefit_application.employee_benefit_appli from erpnext.payroll.doctype.employee_benefit_claim.employee_benefit_claim import get_benefit_claim_amount, get_last_payroll_period_benefits from erpnext.loan_management.doctype.loan_repayment.loan_repayment import calculate_amounts, create_repayment_entry from erpnext.accounts.utils import get_fiscal_year +from erpnext.hr.utils import validate_active_employee from six import iteritems class SalarySlip(TransactionBase): @@ -39,6 +41,7 @@ class SalarySlip(TransactionBase): def validate(self): self.status = self.get_status() + validate_active_employee(self.employee) self.validate_dates() self.check_existing() if not self.salary_slip_based_on_timesheet: @@ -335,20 +338,7 @@ class SalarySlip(TransactionBase): return payment_days def get_holidays_for_employee(self, start_date, end_date): - holiday_list = get_holiday_list_for_employee(self.employee) - holidays = frappe.db.sql_list('''select holiday_date from `tabHoliday` - where - parent=%(holiday_list)s - and holiday_date >= %(start_date)s - and holiday_date <= %(end_date)s''', { - "holiday_list": holiday_list, - "start_date": start_date, - "end_date": end_date - }) - - holidays = [cstr(i) for i in holidays] - - return holidays + return get_holiday_dates_for_employee(self.employee, start_date, end_date) def calculate_lwp_or_ppl_based_on_leave_application(self, holidays, working_days): lwp = 0 @@ -644,10 +634,13 @@ class SalarySlip(TransactionBase): continue if ( - (not d.additional_salary - and (not additional_salary or additional_salary.overwrite)) - or (additional_salary - and additional_salary.name == d.additional_salary) + ( + not d.additional_salary + and (not additional_salary or additional_salary.overwrite) + ) or ( + additional_salary + and additional_salary.name == d.additional_salary + ) ): component_row = d break @@ -678,8 +671,13 @@ class SalarySlip(TransactionBase): component_row.set('abbr', abbr) if additional_salary: - component_row.default_amount = 0 - component_row.additional_amount = amount + if additional_salary.overwrite: + component_row.additional_amount = flt(flt(amount) - flt(component_row.get("default_amount", 0)), + component_row.precision("additional_amount")) + else: + component_row.default_amount = 0 + component_row.additional_amount = amount + component_row.additional_salary = additional_salary.name component_row.deduct_full_tax_on_selected_payroll_date = \ additional_salary.deduct_full_tax_on_selected_payroll_date @@ -1091,6 +1089,7 @@ class SalarySlip(TransactionBase): "applicant": self.employee, "docstatus": 1, "repay_from_salary": 1, + "company": self.company }) def make_loan_repayment_entry(self): diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.js b/erpnext/payroll/doctype/salary_slip/test_salary_slip.js index 06a1c7d72df..a47eba1887d 100644 --- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.js +++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.js @@ -52,4 +52,4 @@ QUnit.test("test salary slip", function(assert) { () => frappe.click_button('Yes'), () => done() ]); -}); \ No newline at end of file +}); diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py index ce88cc3f1e1..6e8d3b3f306 100644 --- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py @@ -482,14 +482,19 @@ def make_employee_salary_slip(user, payroll_frequency, salary_structure=None): salary_structure = payroll_frequency + " Salary Structure Test for Salary Slip" - employee = frappe.db.get_value("Employee", {"user_id": user}) - salary_structure_doc = make_salary_structure(salary_structure, payroll_frequency, employee=employee) + employee = frappe.db.get_value("Employee", + { + "user_id": user + }, + ["name", "company", "employee_name"], + as_dict=True) + + salary_structure_doc = make_salary_structure(salary_structure, payroll_frequency, employee=employee.name, company=employee.company) salary_slip_name = frappe.db.get_value("Salary Slip", {"employee": frappe.db.get_value("Employee", {"user_id": user})}) if not salary_slip_name: - salary_slip = make_salary_slip(salary_structure_doc.name, employee = employee) - salary_slip.employee_name = frappe.get_value("Employee", - {"name":frappe.db.get_value("Employee", {"user_id": user})}, "employee_name") + salary_slip = make_salary_slip(salary_structure_doc.name, employee = employee.name) + salary_slip.employee_name = employee.employee_name salary_slip.payroll_frequency = payroll_frequency salary_slip.posting_date = nowdate() salary_slip.insert() diff --git a/erpnext/payroll/doctype/salary_structure/condition_and_formula_help.html b/erpnext/payroll/doctype/salary_structure/condition_and_formula_help.html index d07a1ab551a..0f6cc378513 100644 --- a/erpnext/payroll/doctype/salary_structure/condition_and_formula_help.html +++ b/erpnext/payroll/doctype/salary_structure/condition_and_formula_help.html @@ -44,4 +44,4 @@
Condition: annual_taxable_earning > 20000000
Formula: annual_taxable_earning * 0.10 
- \ No newline at end of file + diff --git a/erpnext/payroll/doctype/salary_structure/salary_structure.py b/erpnext/payroll/doctype/salary_structure/salary_structure.py index 58c445f8a96..6dfb3a57d5d 100644 --- a/erpnext/payroll/doctype/salary_structure/salary_structure.py +++ b/erpnext/payroll/doctype/salary_structure/salary_structure.py @@ -206,4 +206,3 @@ def get_employees(salary_structure): salary_structure, salary_structure)) return list(set([d.employee for d in employees])) - diff --git a/erpnext/payroll/doctype/salary_structure/salary_structure_dashboard.py b/erpnext/payroll/doctype/salary_structure/salary_structure_dashboard.py index 547f2b81be7..0159e3530fb 100644 --- a/erpnext/payroll/doctype/salary_structure/salary_structure_dashboard.py +++ b/erpnext/payroll/doctype/salary_structure/salary_structure_dashboard.py @@ -15,4 +15,4 @@ def get_data(): 'items': ['Employee Grade'] }, ] - } \ No newline at end of file + } diff --git a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py index e7d123c9960..3957d834d33 100644 --- a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py +++ b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py @@ -119,26 +119,25 @@ def make_salary_structure(salary_structure, payroll_frequency, employee=None, if test_tax: frappe.db.sql("""delete from `tabSalary Structure` where name=%s""",(salary_structure)) - if not frappe.db.exists('Salary Structure', salary_structure): - details = { - "doctype": "Salary Structure", - "name": salary_structure, - "company": company or erpnext.get_default_company(), - "earnings": make_earning_salary_component(setup=True, test_tax=test_tax, company_list=["_Test Company"]), - "deductions": make_deduction_salary_component(setup=True, test_tax=test_tax, company_list=["_Test Company"]), - "payroll_frequency": payroll_frequency, - "payment_account": get_random("Account", filters={'account_currency': currency}), - "currency": currency - } - if other_details and isinstance(other_details, dict): - details.update(other_details) - salary_structure_doc = frappe.get_doc(details) - salary_structure_doc.insert() - if not dont_submit: - salary_structure_doc.submit() + if frappe.db.exists("Salary Structure", salary_structure): + frappe.db.delete("Salary Structure", salary_structure) - else: - salary_structure_doc = frappe.get_doc("Salary Structure", salary_structure) + details = { + "doctype": "Salary Structure", + "name": salary_structure, + "company": company or erpnext.get_default_company(), + "earnings": make_earning_salary_component(setup=True, test_tax=test_tax, company_list=["_Test Company"]), + "deductions": make_deduction_salary_component(setup=True, test_tax=test_tax, company_list=["_Test Company"]), + "payroll_frequency": payroll_frequency, + "payment_account": get_random("Account", filters={'account_currency': currency}), + "currency": currency + } + if other_details and isinstance(other_details, dict): + details.update(other_details) + salary_structure_doc = frappe.get_doc(details) + salary_structure_doc.insert() + if not dont_submit: + salary_structure_doc.submit() filters = {'employee':employee, 'docstatus': 1} if not from_date and payroll_period: diff --git a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py index a0c3013061d..5fb3ce2a98e 100644 --- a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py +++ b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py @@ -36,7 +36,7 @@ class SalaryStructureAssignment(Document): def validate_income_tax_slab(self): if not self.income_tax_slab: return - + income_tax_slab_currency = frappe.db.get_value('Income Tax Slab', self.income_tax_slab, 'currency') if self.currency != income_tax_slab_currency: frappe.throw(_("Currency of selected Income Tax Slab should be {0} instead of {1}").format(self.currency, income_tax_slab_currency)) @@ -69,4 +69,4 @@ def get_employee_currency(employee): employee_currency = frappe.db.get_value('Salary Structure Assignment', {'employee': employee}, 'currency') if not employee_currency: frappe.throw(_("There is no Salary Structure assigned to {0}. First assign a Salary Stucture.").format(employee)) - return employee_currency \ No newline at end of file + return employee_currency diff --git a/erpnext/payroll/doctype/salary_structure_assignment/test_salary_structure_assignment.js b/erpnext/payroll/doctype/salary_structure_assignment/test_salary_structure_assignment.js deleted file mode 100644 index 2f52576c7a7..00000000000 --- a/erpnext/payroll/doctype/salary_structure_assignment/test_salary_structure_assignment.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: Salary Structure Assignment", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Salary Structure Assignment - () => frappe.tests.make('Salary Structure Assignment', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/payroll/notification/as b/erpnext/payroll/notification/as index 7a395572613..05c2c1bec27 100644 --- a/erpnext/payroll/notification/as +++ b/erpnext/payroll/notification/as @@ -1 +1 @@ -update from `tabNotification` set module='Payroll' where name = "Retention Bonus" \ No newline at end of file +update from `tabNotification` set module='Payroll' where name = "Retention Bonus" diff --git a/erpnext/payroll/print_format/salary_slip_based_on_timesheet/salary_slip_based_on_timesheet.json b/erpnext/payroll/print_format/salary_slip_based_on_timesheet/salary_slip_based_on_timesheet.json index ceaf4a6ac71..d5fee6b5b83 100644 --- a/erpnext/payroll/print_format/salary_slip_based_on_timesheet/salary_slip_based_on_timesheet.json +++ b/erpnext/payroll/print_format/salary_slip_based_on_timesheet/salary_slip_based_on_timesheet.json @@ -13,6 +13,6 @@ "name": "Salary Slip based on Timesheet", "owner": "Administrator", "print_format_builder": 1, - "print_format_type": "Server", + "print_format_type": "Jinja", "standard": "Yes" } \ No newline at end of file diff --git a/erpnext/payroll/print_format/salary_slip_standard/salary_slip_standard.json b/erpnext/payroll/print_format/salary_slip_standard/salary_slip_standard.json index b01239fe02d..98a4435a50e 100644 --- a/erpnext/payroll/print_format/salary_slip_standard/salary_slip_standard.json +++ b/erpnext/payroll/print_format/salary_slip_standard/salary_slip_standard.json @@ -16,7 +16,7 @@ "name": "Salary Slip Standard", "owner": "Administrator", "print_format_builder": 1, - "print_format_type": "Server", + "print_format_type": "Jinja", "show_section_headings": 0, "standard": "Yes" } \ No newline at end of file diff --git a/erpnext/payroll/report/bank_remittance/bank_remittance.js b/erpnext/payroll/report/bank_remittance/bank_remittance.js index 6482ed34516..8b75b4facea 100644 --- a/erpnext/payroll/report/bank_remittance/bank_remittance.js +++ b/erpnext/payroll/report/bank_remittance/bank_remittance.js @@ -25,4 +25,3 @@ frappe.query_reports["Bank Remittance"] = { ] } - diff --git a/erpnext/payroll/report/bank_remittance/bank_remittance.py b/erpnext/payroll/report/bank_remittance/bank_remittance.py index 500543ceb02..05a5366a5c1 100644 --- a/erpnext/payroll/report/bank_remittance/bank_remittance.py +++ b/erpnext/payroll/report/bank_remittance/bank_remittance.py @@ -95,6 +95,7 @@ def execute(filters=None): "amount": salary.net_pay, } data.append(row) + return columns, data def get_bank_accounts(): @@ -116,7 +117,7 @@ def get_payroll_entries(accounts, filters): entries = get_all("Payroll Entry", payroll_filter, ["name", "payment_account"]) payment_accounts = [d.payment_account for d in entries] - set_company_account(payment_accounts, entries) + entries = set_company_account(payment_accounts, entries) return entries def get_salary_slips(payroll_entries): diff --git a/erpnext/payroll/report/income_tax_deductions/income_tax_deductions.js b/erpnext/payroll/report/income_tax_deductions/income_tax_deductions.js index 4bbb7f6a1be..6ecf2b1960c 100644 --- a/erpnext/payroll/report/income_tax_deductions/income_tax_deductions.js +++ b/erpnext/payroll/report/income_tax_deductions/income_tax_deductions.js @@ -4,4 +4,4 @@ frappe.require("assets/erpnext/js/salary_slip_deductions_report_filters.js", function() { frappe.query_reports["Income Tax Deductions"] = erpnext.salary_slip_deductions_report_filters; -}); \ No newline at end of file +}); diff --git a/erpnext/payroll/report/salary_payments_based_on_payment_mode/salary_payments_based_on_payment_mode.js b/erpnext/payroll/report/salary_payments_based_on_payment_mode/salary_payments_based_on_payment_mode.js index 166d982c9c6..9b829541692 100644 --- a/erpnext/payroll/report/salary_payments_based_on_payment_mode/salary_payments_based_on_payment_mode.js +++ b/erpnext/payroll/report/salary_payments_based_on_payment_mode/salary_payments_based_on_payment_mode.js @@ -4,4 +4,4 @@ frappe.require("assets/erpnext/js/salary_slip_deductions_report_filters.js", function() { frappe.query_reports["Salary Payments Based On Payment Mode"] = erpnext.salary_slip_deductions_report_filters; -}); \ No newline at end of file +}); diff --git a/erpnext/portal/doctype/homepage/homepage.py b/erpnext/portal/doctype/homepage/homepage.py index 4e4d4774abf..54ea7c62df4 100644 --- a/erpnext/portal/doctype/homepage/homepage.py +++ b/erpnext/portal/doctype/homepage/homepage.py @@ -23,4 +23,3 @@ class Homepage(Document): doc.save() self.append('products', dict(item_code=d.name, item_name=d.item_name, description=d.description, image=d.image)) - diff --git a/erpnext/portal/doctype/products_settings/test_products_settings.js b/erpnext/portal/doctype/products_settings/test_products_settings.js deleted file mode 100644 index b7049b37e18..00000000000 --- a/erpnext/portal/doctype/products_settings/test_products_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: Products Settings", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Products Settings - () => frappe.tests.make('Products Settings', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/portal/product_configurator/test_product_configurator.py b/erpnext/portal/product_configurator/test_product_configurator.py index daaba671736..28e064218b1 100644 --- a/erpnext/portal/product_configurator/test_product_configurator.py +++ b/erpnext/portal/product_configurator/test_product_configurator.py @@ -141,4 +141,4 @@ class TestProductConfigurator(unittest.TestCase): # teardown doc.delete() - item_group_doc.delete() \ No newline at end of file + item_group_doc.delete() diff --git a/erpnext/portal/product_configurator/utils.py b/erpnext/portal/product_configurator/utils.py index d77eb2c3966..d60b1a2b05e 100644 --- a/erpnext/portal/product_configurator/utils.py +++ b/erpnext/portal/product_configurator/utils.py @@ -2,6 +2,7 @@ import frappe from frappe.utils import cint from erpnext.portal.product_configurator.item_variants_cache import ItemVariantsCacheManager from erpnext.shopping_cart.product_info import get_product_info_for_website +from erpnext.setup.doctype.item_group.item_group import get_child_groups def get_field_filter_data(): product_settings = get_product_settings() @@ -89,6 +90,7 @@ def get_products_for_website(field_filters=None, attribute_filters=None, search= def get_products_html_for_website(field_filters=None, attribute_filters=None): field_filters = frappe.parse_json(field_filters) attribute_filters = frappe.parse_json(attribute_filters) + set_item_group_filters(field_filters) items = get_products_for_website(field_filters, attribute_filters) html = ''.join(get_html_for_items(items)) @@ -98,6 +100,10 @@ def get_products_html_for_website(field_filters=None, attribute_filters=None): return html +def set_item_group_filters(field_filters): + if field_filters is not None and 'item_group' in field_filters: + field_filters['item_group'] = [ig[0] for ig in get_child_groups(field_filters['item_group'])] + def get_item_codes_by_attributes(attribute_filters, template_item_code=None): items = [] diff --git a/erpnext/projects/doctype/activity_cost/activity_cost.js b/erpnext/projects/doctype/activity_cost/activity_cost.js index ba10153e5cd..2d22caad8e2 100644 --- a/erpnext/projects/doctype/activity_cost/activity_cost.js +++ b/erpnext/projects/doctype/activity_cost/activity_cost.js @@ -1 +1 @@ -cur_frm.add_fetch('employee', 'employee_name', 'employee_name'); \ No newline at end of file +cur_frm.add_fetch('employee', 'employee_name', 'employee_name'); diff --git a/erpnext/projects/doctype/activity_cost/activity_cost.py b/erpnext/projects/doctype/activity_cost/activity_cost.py index 862a70717ab..99226ea581c 100644 --- a/erpnext/projects/doctype/activity_cost/activity_cost.py +++ b/erpnext/projects/doctype/activity_cost/activity_cost.py @@ -13,7 +13,7 @@ class ActivityCost(Document): def validate(self): self.set_title() self.check_unique() - + def set_title(self): if self.employee: if not self.employee_name: diff --git a/erpnext/projects/doctype/activity_cost/test_activity_cost.py b/erpnext/projects/doctype/activity_cost/test_activity_cost.py index 67d76eb1eee..5f35f299b36 100644 --- a/erpnext/projects/doctype/activity_cost/test_activity_cost.py +++ b/erpnext/projects/doctype/activity_cost/test_activity_cost.py @@ -22,4 +22,4 @@ class TestActivityCost(unittest.TestCase): activity_cost1.insert() activity_cost2 = frappe.copy_doc(activity_cost1) self.assertRaises(DuplicationError, activity_cost2.insert ) - frappe.db.sql("delete from `tabActivity Cost`") \ No newline at end of file + frappe.db.sql("delete from `tabActivity Cost`") diff --git a/erpnext/projects/doctype/activity_type/activity_type.py b/erpnext/projects/doctype/activity_type/activity_type.py index 8b610c29561..50e18ef4de9 100644 --- a/erpnext/projects/doctype/activity_type/activity_type.py +++ b/erpnext/projects/doctype/activity_type/activity_type.py @@ -5,4 +5,4 @@ from __future__ import unicode_literals from frappe.model.document import Document class ActivityType(Document): - pass \ No newline at end of file + pass diff --git a/erpnext/projects/doctype/activity_type/test_activity_type.py b/erpnext/projects/doctype/activity_type/test_activity_type.py index 3ea28dfbe2c..dcb01018de0 100644 --- a/erpnext/projects/doctype/activity_type/test_activity_type.py +++ b/erpnext/projects/doctype/activity_type/test_activity_type.py @@ -4,4 +4,4 @@ from __future__ import unicode_literals import frappe -test_records = frappe.get_test_records('Activity Type') \ No newline at end of file +test_records = frappe.get_test_records('Activity Type') diff --git a/erpnext/projects/doctype/project/project_dashboard.html b/erpnext/projects/doctype/project/project_dashboard.html index f5bfbb7ca1f..1f299e30833 100644 --- a/erpnext/projects/doctype/project/project_dashboard.html +++ b/erpnext/projects/doctype/project/project_dashboard.html @@ -23,4 +23,4 @@
-{% endfor %} \ No newline at end of file +{% endfor %} diff --git a/erpnext/projects/doctype/project/test_project.js b/erpnext/projects/doctype/project/test_project.js deleted file mode 100644 index 16494f62b60..00000000000 --- a/erpnext/projects/doctype/project/test_project.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: Project", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Project - () => frappe.tests.make('Project', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/projects/doctype/project_template/project_template.py b/erpnext/projects/doctype/project_template/project_template.py index aace40240c4..2426fd2af89 100644 --- a/erpnext/projects/doctype/project_template/project_template.py +++ b/erpnext/projects/doctype/project_template/project_template.py @@ -22,7 +22,7 @@ class ProjectTemplate(Document): task_details_format = get_link_to_form("Task",task_details.name) dependency_task_format = get_link_to_form("Task", dependency_task.task) frappe.throw(_("Task {0} depends on Task {1}. Please add Task {1} to the Tasks list.").format(frappe.bold(task_details_format), frappe.bold(dependency_task_format))) - + def check_dependent_task_presence(self, task): for task_details in self.tasks: if task_details.task == task: diff --git a/erpnext/projects/doctype/project_template/test_project_template.py b/erpnext/projects/doctype/project_template/test_project_template.py index 95663cdcbbb..d546fd09a30 100644 --- a/erpnext/projects/doctype/project_template/test_project_template.py +++ b/erpnext/projects/doctype/project_template/test_project_template.py @@ -26,4 +26,4 @@ def make_project_template(project_template_name, project_tasks=[]): }) doc.insert() - return frappe.get_doc('Project Template', project_template_name) \ No newline at end of file + return frappe.get_doc('Project Template', project_template_name) diff --git a/erpnext/projects/doctype/project_type/project_type.js b/erpnext/projects/doctype/project_type/project_type.js index a1f941fe148..e3dda5eccc5 100644 --- a/erpnext/projects/doctype/project_type/project_type.js +++ b/erpnext/projects/doctype/project_type/project_type.js @@ -3,4 +3,4 @@ frappe.ui.form.on('Project Type', { -}); \ No newline at end of file +}); diff --git a/erpnext/projects/doctype/project_type/project_type.py b/erpnext/projects/doctype/project_type/project_type.py index f46876eda23..36137ca0186 100644 --- a/erpnext/projects/doctype/project_type/project_type.py +++ b/erpnext/projects/doctype/project_type/project_type.py @@ -10,4 +10,4 @@ from frappe import _ class ProjectType(Document): def on_trash(self): if self.name == "External": - frappe.throw(_("You cannot delete Project Type 'External'")) \ No newline at end of file + frappe.throw(_("You cannot delete Project Type 'External'")) diff --git a/erpnext/projects/doctype/project_type/test_project_type.js b/erpnext/projects/doctype/project_type/test_project_type.js deleted file mode 100644 index c2198c452c0..00000000000 --- a/erpnext/projects/doctype/project_type/test_project_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: Project Type", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially('Project Type', [ - // insert a new Project Type - () => frappe.tests.make([ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/projects/doctype/project_update/project_update.py b/erpnext/projects/doctype/project_update/project_update.py index faa4bf1f9b6..2e1ec746ed6 100644 --- a/erpnext/projects/doctype/project_update/project_update.py +++ b/erpnext/projects/doctype/project_update/project_update.py @@ -39,4 +39,4 @@ def email_sending(project_name,frequency,date_start,date_end,progress,number_of_ for emails in email: frappe.sendmail(recipients=emails,subject=frappe._(project_name + ' ' + 'Summary'),message = msg) else: - pass \ No newline at end of file + pass diff --git a/erpnext/projects/doctype/project_update/test_project_update.js b/erpnext/projects/doctype/project_update/test_project_update.js deleted file mode 100644 index bda510b921a..00000000000 --- a/erpnext/projects/doctype/project_update/test_project_update.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: Project Update", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Project Update - () => frappe.tests.make('Project Update', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/projects/doctype/project_update/test_project_update.py b/erpnext/projects/doctype/project_update/test_project_update.py index d5d09194446..2edd2f85a31 100644 --- a/erpnext/projects/doctype/project_update/test_project_update.py +++ b/erpnext/projects/doctype/project_update/test_project_update.py @@ -10,4 +10,4 @@ class TestProjectUpdate(unittest.TestCase): pass test_records = frappe.get_test_records('Project Update') -test_ignore = ["Sales Order"] \ No newline at end of file +test_ignore = ["Sales Order"] diff --git a/erpnext/projects/doctype/projects_settings/test_projects_settings.js b/erpnext/projects/doctype/projects_settings/test_projects_settings.js deleted file mode 100644 index f6feaa48049..00000000000 --- a/erpnext/projects/doctype/projects_settings/test_projects_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: Projects Settings", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Projects Settings - () => frappe.tests.make('Projects Settings', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/projects/doctype/task/task_tree.js b/erpnext/projects/doctype/task/task_tree.js index d1d872f28a4..9ebfcdd180c 100644 --- a/erpnext/projects/doctype/task/task_tree.js +++ b/erpnext/projects/doctype/task/task_tree.js @@ -81,4 +81,4 @@ frappe.treeview_settings['Task'] = { } ], extend_toolbar: true -}; \ No newline at end of file +}; diff --git a/erpnext/projects/doctype/timesheet/test_timesheet.js b/erpnext/projects/doctype/timesheet/test_timesheet.js deleted file mode 100644 index c081d6f8ea4..00000000000 --- a/erpnext/projects/doctype/timesheet/test_timesheet.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: Timesheet", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Timesheet - () => frappe.tests.make('Timesheet', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/projects/doctype/timesheet/timesheet.css b/erpnext/projects/doctype/timesheet/timesheet.css index 3a38415e6c6..1e055629ba8 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.css +++ b/erpnext/projects/doctype/timesheet/timesheet.css @@ -20,4 +20,4 @@ .playpause { border-right: 1px dashed #fff; border-bottom: 1px dashed #fff; -} \ No newline at end of file +} diff --git a/erpnext/projects/doctype/timesheet/timesheet.js b/erpnext/projects/doctype/timesheet/timesheet.js index 84c7b8118b8..1655b76b988 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.js +++ b/erpnext/projects/doctype/timesheet/timesheet.js @@ -399,4 +399,4 @@ function set_project_in_timelog(frm) { frappe.model.set_value(item.doctype, item.name, "project", frm.doc.parent_project); }); } -} \ No newline at end of file +} diff --git a/erpnext/projects/doctype/timesheet/timesheet.json b/erpnext/projects/doctype/timesheet/timesheet.json index 75f7478ed18..be6771e56f9 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.json +++ b/erpnext/projects/doctype/timesheet/timesheet.json @@ -310,6 +310,7 @@ "read_only": 1 }, { + "default": "1", "fieldname": "exchange_rate", "fieldtype": "Float", "label": "Exchange Rate" @@ -319,7 +320,7 @@ "idx": 1, "is_submittable": 1, "links": [], - "modified": "2021-05-18 16:10:08.249619", + "modified": "2021-06-09 12:08:53.930200", "modified_by": "Administrator", "module": "Projects", "name": "Timesheet", diff --git a/erpnext/projects/doctype/timesheet/timesheet.py b/erpnext/projects/doctype/timesheet/timesheet.py index c8bd80fca0a..3a3d9e2c2a5 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.py +++ b/erpnext/projects/doctype/timesheet/timesheet.py @@ -15,12 +15,15 @@ from erpnext.manufacturing.doctype.workstation.workstation import (check_if_with WorkstationHolidayError) from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings import get_mins_between_operations from erpnext.setup.utils import get_exchange_rate +from erpnext.hr.utils import validate_active_employee class OverlapError(frappe.ValidationError): pass class OverWorkLoggedError(frappe.ValidationError): pass class Timesheet(Document): def validate(self): + if self.employee: + validate_active_employee(self.employee) self.set_employee_name() self.set_status() self.validate_dates() @@ -213,24 +216,66 @@ class Timesheet(Document): @frappe.whitelist() def get_projectwise_timesheet_data(project=None, parent=None, from_time=None, to_time=None): - condition = '' + condition = "" if project: - condition += "and tsd.project = %(project)s" + condition += "AND tsd.project = %(project)s " if parent: - condition += "AND tsd.parent = %(parent)s" + condition += "AND tsd.parent = %(parent)s " if from_time and to_time: condition += "AND CAST(tsd.from_time as DATE) BETWEEN %(from_time)s AND %(to_time)s" - return frappe.db.sql("""SELECT tsd.name as name, - tsd.parent as parent, tsd.billing_hours as billing_hours, - tsd.billing_amount as billing_amount, tsd.activity_type as activity_type, - tsd.description as description, ts.currency as currency - FROM `tabTimesheet Detail` tsd - INNER JOIN `tabTimesheet` ts ON ts.name = tsd.parent - WHERE tsd.parenttype = 'Timesheet' - and tsd.docstatus=1 {0} - and tsd.is_billable = 1 - and tsd.sales_invoice is null""".format(condition), {'project': project, 'parent': parent, 'from_time': from_time, 'to_time': to_time}, as_dict=1) + query = f""" + SELECT + + tsd.name as name, + tsd.parent as time_sheet, + tsd.from_time as from_time, + tsd.to_time as to_time, + tsd.billing_hours as billing_hours, + tsd.billing_amount as billing_amount, + tsd.activity_type as activity_type, + tsd.description as description, + ts.currency as currency, + tsd.project_name as project_name + + FROM `tabTimesheet Detail` tsd + + INNER JOIN `tabTimesheet` ts + ON ts.name = tsd.parent + + WHERE + + tsd.parenttype = 'Timesheet' + AND tsd.docstatus = 1 + AND tsd.is_billable = 1 + AND tsd.sales_invoice is NULL + {condition} + + ORDER BY tsd.from_time ASC + """ + + filters = { + "project": project, + "parent": parent, + "from_time": from_time, + "to_time": to_time + } + + return frappe.db.sql(query, filters, as_dict=1) + + +@frappe.whitelist() +def get_timesheet_detail_rate(timelog, currency): + timelog_detail = frappe.db.sql("""SELECT tsd.billing_amount as billing_amount, + ts.currency as currency FROM `tabTimesheet Detail` tsd + INNER JOIN `tabTimesheet` ts ON ts.name=tsd.parent + WHERE tsd.name = '{0}'""".format(timelog), as_dict = 1)[0] + + if timelog_detail.currency: + exchange_rate = get_exchange_rate(timelog_detail.currency, currency) + + return timelog_detail.billing_amount * exchange_rate + return timelog_detail.billing_amount @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs diff --git a/erpnext/projects/doctype/timesheet/timesheet_calendar.js b/erpnext/projects/doctype/timesheet/timesheet_calendar.js index 14f016a7653..80967ede1ce 100644 --- a/erpnext/projects/doctype/timesheet/timesheet_calendar.js +++ b/erpnext/projects/doctype/timesheet/timesheet_calendar.js @@ -9,8 +9,8 @@ frappe.views.calendar["Timesheet"] = { "title": "title" }, style_map: { - "0": "info", - "1": "standard", + "0": "info", + "1": "standard", "2": "danger" }, gantt: true, diff --git a/erpnext/projects/doctype/timesheet/timesheet_dashboard.py b/erpnext/projects/doctype/timesheet/timesheet_dashboard.py index acff97a2269..088d98c4d5f 100644 --- a/erpnext/projects/doctype/timesheet/timesheet_dashboard.py +++ b/erpnext/projects/doctype/timesheet/timesheet_dashboard.py @@ -10,4 +10,4 @@ def get_data(): 'items': ['Sales Invoice', 'Salary Slip'] } ] - } \ No newline at end of file + } diff --git a/erpnext/projects/doctype/timesheet/timesheet_list.js b/erpnext/projects/doctype/timesheet/timesheet_list.js index 1b200f855db..b59fdc96fe8 100644 --- a/erpnext/projects/doctype/timesheet/timesheet_list.js +++ b/erpnext/projects/doctype/timesheet/timesheet_list.js @@ -4,13 +4,13 @@ frappe.listview_settings['Timesheet'] = { if (doc.status== "Billed") { return [__("Billed"), "green", "status,=," + "Billed"] } - + if (doc.status== "Payslip") { return [__("Payslip"), "green", "status,=," + "Payslip"] } - + if (doc.status== "Completed") { return [__("Completed"), "green", "status,=," + "Completed"] } } -}; \ No newline at end of file +}; diff --git a/erpnext/projects/report/billing_summary.py b/erpnext/projects/report/billing_summary.py index 5efde41b5b3..a22ed7b8338 100644 --- a/erpnext/projects/report/billing_summary.py +++ b/erpnext/projects/report/billing_summary.py @@ -144,4 +144,4 @@ def get_billable_and_total_duration(activity, start_time, end_time): if activity_duration != activity.billing_hours: billing_duration = activity_duration * activity.billing_hours / activity.hours - return flt(activity_duration, precision), flt(billing_duration, precision) \ No newline at end of file + return flt(activity_duration, precision), flt(billing_duration, precision) diff --git a/erpnext/projects/report/daily_timesheet_summary/daily_timesheet_summary.py b/erpnext/projects/report/daily_timesheet_summary/daily_timesheet_summary.py index 682fb2e09dc..3dcae5b1b53 100644 --- a/erpnext/projects/report/daily_timesheet_summary/daily_timesheet_summary.py +++ b/erpnext/projects/report/daily_timesheet_summary/daily_timesheet_summary.py @@ -20,8 +20,8 @@ def execute(filters=None): return columns, data def get_column(): - return [_("Timesheet") + ":Link/Timesheet:120", _("Employee") + "::150", _("Employee Name") + "::150", - _("From Datetime") + "::140", _("To Datetime") + "::140", _("Hours") + "::70", + return [_("Timesheet") + ":Link/Timesheet:120", _("Employee") + "::150", _("Employee Name") + "::150", + _("From Datetime") + "::140", _("To Datetime") + "::140", _("Hours") + "::70", _("Activity Type") + "::120", _("Task") + ":Link/Task:150", _("Project") + ":Link/Project:120", _("Status") + "::70"] @@ -45,4 +45,4 @@ def get_conditions(filters): if match_conditions: conditions += " and %s" % match_conditions - return conditions \ No newline at end of file + return conditions diff --git a/erpnext/projects/report/delayed_tasks_summary/test_delayed_tasks_summary.py b/erpnext/projects/report/delayed_tasks_summary/test_delayed_tasks_summary.py index dbeedb4be92..78291b2d781 100644 --- a/erpnext/projects/report/delayed_tasks_summary/test_delayed_tasks_summary.py +++ b/erpnext/projects/report/delayed_tasks_summary/test_delayed_tasks_summary.py @@ -10,7 +10,7 @@ class TestDelayedTasksSummary(unittest.TestCase): def setUp(self): task1 = create_task("_Test Task 98", add_days(nowdate(), -10), nowdate()) create_task("_Test Task 99", add_days(nowdate(), -10), add_days(nowdate(), -1)) - + task1.status = "Completed" task1.completed_on = add_days(nowdate(), -1) task1.save() @@ -38,7 +38,7 @@ class TestDelayedTasksSummary(unittest.TestCase): ] report = execute(filters) data = list(filter(lambda x: x.subject == "_Test Task 99", report[1]))[0] - + for key in ["subject", "status", "priority", "delay"]: self.assertEqual(expected_data[0].get(key), data.get(key)) @@ -51,4 +51,4 @@ class TestDelayedTasksSummary(unittest.TestCase): def tearDown(self): for task in ["_Test Task 98", "_Test Task 99"]: - frappe.get_doc("Task", {"subject": task}).delete() \ No newline at end of file + frappe.get_doc("Task", {"subject": task}).delete() diff --git a/erpnext/projects/report/employee_billing_summary/employee_billing_summary.py b/erpnext/projects/report/employee_billing_summary/employee_billing_summary.py index cd5ad7803a5..17c92c234d5 100644 --- a/erpnext/projects/report/employee_billing_summary/employee_billing_summary.py +++ b/erpnext/projects/report/employee_billing_summary/employee_billing_summary.py @@ -11,4 +11,4 @@ def execute(filters=None): columns = get_columns() data = get_data(filters) - return columns, data \ No newline at end of file + return columns, data diff --git a/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/test_employee_util.py b/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/test_employee_util.py index 0e5a59756e3..969fc556e8d 100644 --- a/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/test_employee_util.py +++ b/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/test_employee_util.py @@ -195,4 +195,4 @@ class TestEmployeeUtilization(unittest.TestCase): 'per_util': 27.78, 'per_util_billed_only': 27.78 } - ] \ No newline at end of file + ] diff --git a/erpnext/projects/report/project_billing_summary/project_billing_summary.py b/erpnext/projects/report/project_billing_summary/project_billing_summary.py index cd5ad7803a5..17c92c234d5 100644 --- a/erpnext/projects/report/project_billing_summary/project_billing_summary.py +++ b/erpnext/projects/report/project_billing_summary/project_billing_summary.py @@ -11,4 +11,4 @@ def execute(filters=None): columns = get_columns() data = get_data(filters) - return columns, data \ No newline at end of file + return columns, data diff --git a/erpnext/projects/report/project_profitability/project_profitability.py b/erpnext/projects/report/project_profitability/project_profitability.py index 9139d84facc..0a52f7bf904 100644 --- a/erpnext/projects/report/project_profitability/project_profitability.py +++ b/erpnext/projects/report/project_profitability/project_profitability.py @@ -208,4 +208,4 @@ def get_columns(): "options": "Currency", "width": 80 } - ] \ No newline at end of file + ] diff --git a/erpnext/projects/web_form/tasks/tasks.js b/erpnext/projects/web_form/tasks/tasks.js index 699703c5792..ffc5e984253 100644 --- a/erpnext/projects/web_form/tasks/tasks.js +++ b/erpnext/projects/web_form/tasks/tasks.js @@ -1,3 +1,3 @@ frappe.ready(function() { // bind events here -}) \ No newline at end of file +}) diff --git a/erpnext/projects/web_form/tasks/tasks.py b/erpnext/projects/web_form/tasks/tasks.py index e97f36d04b4..e5a94048be1 100644 --- a/erpnext/projects/web_form/tasks/tasks.py +++ b/erpnext/projects/web_form/tasks/tasks.py @@ -6,7 +6,7 @@ def get_context(context): if frappe.form_dict.project: context.parents = [{'title': frappe.form_dict.project, 'route': '/projects?project='+ frappe.form_dict.project}] context.success_url = "/projects?project=" + frappe.form_dict.project - + elif context.doc and context.doc.get('project'): context.parents = [{'title': context.doc.project, 'route': '/projects?project='+ context.doc.project}] context.success_url = "/projects?project=" + context.doc.project diff --git a/erpnext/public/build.json b/erpnext/public/build.json index 7a3cb838a99..6b70dab8037 100644 --- a/erpnext/public/build.json +++ b/erpnext/public/build.json @@ -3,7 +3,8 @@ "public/less/erpnext.less", "public/less/hub.less", "public/scss/call_popup.scss", - "public/scss/point-of-sale.scss" + "public/scss/point-of-sale.scss", + "public/scss/hierarchy_chart.scss" ], "css/marketplace.css": [ "public/less/hub.less" @@ -37,13 +38,15 @@ "public/js/templates/item_quick_entry.html", "public/js/utils/item_quick_entry.js", "public/js/utils/customer_quick_entry.js", + "public/js/utils/supplier_quick_entry.js", "public/js/education/student_button.html", "public/js/education/assessment_result_tool.html", "public/js/hub/hub_factory.js", "public/js/call_popup/call_popup.js", "public/js/utils/dimension_tree_filter.js", "public/js/telephony.js", - "public/js/templates/call_link.html" + "public/js/templates/call_link.html", + "public/js/templates/node_card.html" ], "js/item-dashboard.min.js": [ "stock/dashboard/item_dashboard.html", @@ -66,5 +69,9 @@ "public/js/bank_reconciliation_tool/data_table_manager.js", "public/js/bank_reconciliation_tool/number_card.js", "public/js/bank_reconciliation_tool/dialog_manager.js" + ], + "js/hierarchy-chart.min.js": [ + "public/js/hierarchy_chart/hierarchy_chart_desktop.js", + "public/js/hierarchy_chart/hierarchy_chart_mobile.js" ] } diff --git a/erpnext/public/css/leaflet/leaflet.css b/erpnext/public/css/leaflet/leaflet.css index 979a8bd712b..d89fef3a534 100755 --- a/erpnext/public/css/leaflet/leaflet.css +++ b/erpnext/public/css/leaflet/leaflet.css @@ -608,4 +608,4 @@ .leaflet-div-icon { background: #fff; border: 1px solid #666; -} \ No newline at end of file +} diff --git a/erpnext/public/css/leaflet/leaflet.draw.css b/erpnext/public/css/leaflet/leaflet.draw.css index 6fb7db0e64a..56f4a3542ba 100755 --- a/erpnext/public/css/leaflet/leaflet.draw.css +++ b/erpnext/public/css/leaflet/leaflet.draw.css @@ -313,4 +313,4 @@ .leaflet-oldie .leaflet-draw-actions-top.leaflet-draw-actions-bottom a { height: 27px; line-height: 27px; -} \ No newline at end of file +} diff --git a/erpnext/public/images/erpnext-favicon.svg b/erpnext/public/images/erpnext-favicon.svg index a3ac3bb2ce2..6bc6b2c2db1 100644 --- a/erpnext/public/images/erpnext-favicon.svg +++ b/erpnext/public/images/erpnext-favicon.svg @@ -2,4 +2,4 @@ - \ No newline at end of file + diff --git a/erpnext/public/images/erpnext-logo.svg b/erpnext/public/images/erpnext-logo.svg index a3ac3bb2ce2..6bc6b2c2db1 100644 --- a/erpnext/public/images/erpnext-logo.svg +++ b/erpnext/public/images/erpnext-logo.svg @@ -2,4 +2,4 @@ - \ No newline at end of file + diff --git a/erpnext/public/images/pos.svg b/erpnext/public/images/pos.svg index 3d12d9cb86e..90714e9491d 100644 --- a/erpnext/public/images/pos.svg +++ b/erpnext/public/images/pos.svg @@ -1,4 +1,4 @@ - \ No newline at end of file + diff --git a/erpnext/public/js/contact.js b/erpnext/public/js/contact.js new file mode 100644 index 00000000000..41a0e8a9f99 --- /dev/null +++ b/erpnext/public/js/contact.js @@ -0,0 +1,16 @@ + + +frappe.ui.form.on("Contact", { + refresh(frm) { + frm.set_query('link_doctype', "links", function() { + return { + query: "frappe.contacts.address_and_contact.filter_dynamic_link_doctypes", + filters: { + fieldtype: ["in", ["HTML", "Text Editor"]], + fieldname: ["in", ["contact_html", "company_description"]], + } + }; + }); + frm.refresh_field("links"); + } +}); diff --git a/erpnext/public/js/controllers/accounts.js b/erpnext/public/js/controllers/accounts.js index 7b997a11530..84c717676c7 100644 --- a/erpnext/public/js/controllers/accounts.js +++ b/erpnext/public/js/controllers/accounts.js @@ -31,6 +31,14 @@ frappe.ui.form.on(cur_frm.doctype, { } } }); + frm.set_query("cost_center", "taxes", function(doc) { + return { + filters: { + "company": doc.company, + "is_group": 0 + } + }; + }); } }, validate: function(frm) { diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index 5c9f5d7da43..9c4851ebb0e 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -508,4 +508,4 @@ erpnext.buying.get_items_from_product_bundle = function(frm) { }); dialog.show(); -} \ No newline at end of file +} diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 1de9ec1a7df..90cb5559394 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -47,7 +47,10 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ if (in_list(["Sales Invoice", "POS Invoice"], this.frm.doc.doctype) && this.frm.doc.is_pos && this.frm.doc.is_return) { - this.update_paid_amount_for_return(); + if (this.frm.doc.doctype == "Sales Invoice") { + this.set_total_amount_to_default_mop(); + } + this.calculate_paid_amount(); } // Sales person's commission @@ -65,7 +68,7 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ this.frm.refresh_fields(); }, - calculate_discount_amount: function(){ + calculate_discount_amount: function() { if (frappe.meta.get_docfield(this.frm.doc.doctype, "discount_amount")) { this.set_discount_amount(); this.apply_discount_amount(); @@ -73,18 +76,15 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ }, _calculate_taxes_and_totals: function() { - frappe.run_serially([ - () => this.validate_conversion_rate(), - () => this.calculate_item_values(), - () => this.update_item_tax_map(), - () => this.initialize_taxes(), - () => this.determine_exclusive_rate(), - () => this.calculate_net_total(), - () => this.calculate_taxes(), - () => this.manipulate_grand_total_for_inclusive_tax(), - () => this.calculate_totals(), - () => this._cleanup() - ]); + this.validate_conversion_rate(); + this.calculate_item_values(); + this.initialize_taxes(); + this.determine_exclusive_rate(); + this.calculate_net_total(); + this.calculate_taxes(); + this.manipulate_grand_total_for_inclusive_tax(); + this.calculate_totals(); + this._cleanup(); }, validate_conversion_rate: function() { @@ -105,7 +105,7 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ }, calculate_item_values: function() { - var me = this; + let me = this; if (!this.discount_amount_applied) { $.each(this.frm.doc["items"] || [], function(i, item) { frappe.model.round_floats_in(item); @@ -266,46 +266,6 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ frappe.model.round_floats_in(this.frm.doc, ["total", "base_total", "net_total", "base_net_total"]); }, - update_item_tax_map: function() { - let me = this; - let item_codes = []; - let item_rates = {}; - let item_tax_templates = {}; - - $.each(this.frm.doc.items || [], function(i, item) { - if (item.item_code) { - // Use combination of name and item code in case same item is added multiple times - item_codes.push([item.item_code, item.name]); - item_rates[item.name] = item.net_rate; - item_tax_templates[item.name] = item.item_tax_template; - } - }); - - if (item_codes.length) { - return this.frm.call({ - method: "erpnext.stock.get_item_details.get_item_tax_info", - args: { - company: me.frm.doc.company, - tax_category: cstr(me.frm.doc.tax_category), - item_codes: item_codes, - item_rates: item_rates, - item_tax_templates: item_tax_templates - }, - callback: function(r) { - if (!r.exc) { - $.each(me.frm.doc.items || [], function(i, item) { - if (item.name && r.message.hasOwnProperty(item.name) && r.message[item.name].item_tax_template) { - item.item_tax_template = r.message[item.name].item_tax_template; - item.item_tax_rate = r.message[item.name].item_tax_rate; - me.add_taxes_from_item_tax_template(item.item_tax_rate); - } - }); - } - } - }); - } - }, - add_taxes_from_item_tax_template: function(item_tax_map) { let me = this; @@ -630,8 +590,6 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ tax.item_wise_tax_detail = JSON.stringify(tax.item_wise_tax_detail); }); } - - this.frm.refresh_fields(); }, set_discount_amount: function() { @@ -775,7 +733,7 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ } }, - update_paid_amount_for_return: function() { + set_total_amount_to_default_mop: function() { var grand_total = this.frm.doc.rounded_total || this.frm.doc.grand_total; if(this.frm.doc.party_account_currency == this.frm.doc.currency) { @@ -788,17 +746,12 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ precision("base_grand_total") ); } - this.frm.doc.payments.find(pay => { if (pay.default) { pay.amount = total_amount_to_pay; - } else { - pay.amount = 0.0 } }); this.frm.refresh_fields(); - - this.calculate_paid_amount(); }, set_default_payment: function(total_amount_to_pay, update_paid_amount) { diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index b3af3d67eaa..0e99b43befa 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -342,30 +342,6 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ this.set_dynamic_labels(); this.setup_sms(); this.setup_quality_inspection(); - let scan_barcode_field = this.frm.get_field('scan_barcode'); - if (scan_barcode_field && scan_barcode_field.get_value()) { - scan_barcode_field.set_value(""); - scan_barcode_field.set_new_description(""); - - if (frappe.is_mobile()) { - if (scan_barcode_field.$input_wrapper.find('.input-group').length) return; - - let $input_group = $('
'); - scan_barcode_field.$input_wrapper.find('.control-input').append($input_group); - $input_group.append(scan_barcode_field.$input); - $(` - - `) - .on('click', '.btn', () => { - frappe.barcode.scan_barcode().then(barcode => { - scan_barcode_field.set_value(barcode); - }); - }) - .appendTo($input_group); - } - } }, scan_barcode: function() { @@ -732,7 +708,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ this.frm.trigger("item_code", cdt, cdn); } else { - // Replacing all occurences of comma with carriage return + // Replace all occurences of comma with line feed item.serial_no = item.serial_no.replace(/,/g, '\n'); item.conversion_factor = item.conversion_factor || 1; refresh_field("serial_no", item.name, item.parentfield); @@ -826,9 +802,9 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ frappe.run_serially([ () => me.frm.script_manager.trigger("currency"), + () => me.update_item_tax_map(), () => me.apply_default_taxes(), - () => me.apply_pricing_rule(), - () => me.calculate_taxes_and_totals() + () => me.apply_pricing_rule() ]); } } @@ -870,21 +846,25 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ if (frappe.meta.get_docfield(this.frm.doctype, "shipping_address") && in_list(['Purchase Order', 'Purchase Receipt', 'Purchase Invoice'], this.frm.doctype)) { - erpnext.utils.get_shipping_address(this.frm, function(){ + erpnext.utils.get_shipping_address(this.frm, function() { set_party_account(set_pricing); }); // Get default company billing address in Purchase Invoice, Order and Receipt - frappe.call({ - 'method': 'frappe.contacts.doctype.address.address.get_default_address', - 'args': { - 'doctype': 'Company', - 'name': this.frm.doc.company - }, - 'callback': function(r) { - me.frm.set_value('billing_address', r.message); - } - }); + if (this.frm.doc.company && frappe.meta.get_docfield(this.frm.doctype, "billing_address")) { + frappe.call({ + method: "erpnext.setup.doctype.company.company.get_default_company_address", + args: {name: this.frm.doc.company, existing_address: this.frm.doc.billing_address || ""}, + debounce: 2000, + callback: function(r) { + if (r.message) { + me.frm.set_value("billing_address", r.message); + } else { + me.frm.set_value("company_address", ""); + } + } + }); + } } else { set_party_account(set_pricing); @@ -1787,6 +1767,46 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ ]); }, + update_item_tax_map: function() { + let me = this; + let item_codes = []; + let item_rates = {}; + let item_tax_templates = {}; + + $.each(this.frm.doc.items || [], function(i, item) { + if (item.item_code) { + // Use combination of name and item code in case same item is added multiple times + item_codes.push([item.item_code, item.name]); + item_rates[item.name] = item.net_rate; + item_tax_templates[item.name] = item.item_tax_template; + } + }); + + if (item_codes.length) { + return this.frm.call({ + method: "erpnext.stock.get_item_details.get_item_tax_info", + args: { + company: me.frm.doc.company, + tax_category: cstr(me.frm.doc.tax_category), + item_codes: item_codes, + item_rates: item_rates, + item_tax_templates: item_tax_templates + }, + callback: function(r) { + if (!r.exc) { + $.each(me.frm.doc.items || [], function(i, item) { + if (item.name && r.message.hasOwnProperty(item.name) && r.message[item.name].item_tax_template) { + item.item_tax_template = r.message[item.name].item_tax_template; + item.item_tax_rate = r.message[item.name].item_tax_rate; + me.add_taxes_from_item_tax_template(item.item_tax_rate); + } + }); + } + } + }); + } + }, + item_tax_template: function(doc, cdt, cdn) { var me = this; if(me.frm.updating_party_details) return; @@ -2207,12 +2227,19 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ coupon_code: function() { var me = this; - frappe.run_serially([ - () => this.frm.doc.ignore_pricing_rule=1, - () => me.ignore_pricing_rule(), - () => this.frm.doc.ignore_pricing_rule=0, - () => me.apply_pricing_rule() - ]); + if (this.frm.doc.coupon_code) { + frappe.run_serially([ + () => this.frm.doc.ignore_pricing_rule=1, + () => me.ignore_pricing_rule(), + () => this.frm.doc.ignore_pricing_rule=0, + () => me.apply_pricing_rule() + ]); + } else { + frappe.run_serially([ + () => this.frm.doc.ignore_pricing_rule=1, + () => me.ignore_pricing_rule() + ]); + } } }); diff --git a/erpnext/public/js/education/assessment_result_tool.html b/erpnext/public/js/education/assessment_result_tool.html index b591010ec86..f7d1ab39fcf 100644 --- a/erpnext/public/js/education/assessment_result_tool.html +++ b/erpnext/public/js/education/assessment_result_tool.html @@ -69,4 +69,4 @@ {% endfor %} - \ No newline at end of file + diff --git a/erpnext/public/js/education/student_button.html b/erpnext/public/js/education/student_button.html index 3cf259216a7..b64c73a43c7 100644 --- a/erpnext/public/js/education/student_button.html +++ b/erpnext/public/js/education/student_button.html @@ -1,12 +1,12 @@
-
\ No newline at end of file +
diff --git a/erpnext/public/js/financial_statements.js b/erpnext/public/js/financial_statements.js index b2f7afe53f3..0d79b10c041 100644 --- a/erpnext/public/js/financial_statements.js +++ b/erpnext/public/js/financial_statements.js @@ -176,5 +176,3 @@ function get_filters() { return filters; } - - diff --git a/erpnext/public/js/help_links.js b/erpnext/public/js/help_links.js index aa9bba17c77..d0c935f4887 100644 --- a/erpnext/public/js/help_links.js +++ b/erpnext/public/js/help_links.js @@ -54,7 +54,7 @@ frappe.help.help_links["permission-manager"] = [ frappe.help.help_links["Form/System Settings"] = [ { - label: "Naming Series", + label: "System Settings", url: docsUrl + "user/manual/en/setting-up/settings/system-settings", }, ]; @@ -206,7 +206,7 @@ frappe.help.help_links["Form/PayPal Settings"] = [ label: "PayPal Settings", url: docsUrl + - "user/manual/en/setting-up/integrations/paypal-integration", + "user/manual/en/erpnext_integration/paypal-integration", }, ]; @@ -215,14 +215,14 @@ frappe.help.help_links["Form/Razorpay Settings"] = [ label: "Razorpay Settings", url: docsUrl + - "user/manual/en/setting-up/integrations/razorpay-integration", + "user/manual/en/erpnext_integration/razorpay-integration", }, ]; frappe.help.help_links["Form/Dropbox Settings"] = [ { label: "Dropbox Settings", - url: docsUrl + "user/manual/en/setting-up/integrations/dropbox-backup", + url: docsUrl + "user/manual/en/erpnext_integration/dropbox-backup", }, ]; @@ -230,7 +230,7 @@ frappe.help.help_links["Form/LDAP Settings"] = [ { label: "LDAP Settings", url: - docsUrl + "user/manual/en/setting-up/integrations/ldap-integration", + docsUrl + "user/manual/en/erpnext_integration/ldap-integration", }, ]; @@ -239,7 +239,7 @@ frappe.help.help_links["Form/Stripe Settings"] = [ label: "Stripe Settings", url: docsUrl + - "user/manual/en/setting-up/integrations/stripe-integration", + "user/manual/en/erpnext_integration/stripe-integration", }, ]; @@ -991,7 +991,7 @@ frappe.help.help_links["Form/BOM"] = [ label: "Nested BOM Structure", url: docsUrl + - "user/manual/en/manufacturing/articles/nested-bom-structure", + "user/manual/en/manufacturing/articles/managing-multi-level-bom", }, ]; diff --git a/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js b/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js new file mode 100644 index 00000000000..23ec2fdb849 --- /dev/null +++ b/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js @@ -0,0 +1,600 @@ +import html2canvas from 'html2canvas'; +erpnext.HierarchyChart = class { + /* Options: + - doctype + - wrapper: wrapper for the hierarchy view + - method: + - to get the data for each node + - this method should return id, name, title, image, and connections for each node + */ + constructor(doctype, wrapper, method) { + this.page = wrapper.page; + this.method = method; + this.doctype = doctype; + + this.setup_page_style(); + this.page.main.addClass('frappe-card'); + + this.nodes = {}; + this.setup_node_class(); + } + + setup_page_style() { + this.page.main.css({ + 'min-height': '300px', + 'max-height': '600px', + 'overflow': 'auto', + 'position': 'relative' + }); + } + + setup_node_class() { + let me = this; + this.Node = class { + constructor({ + id, parent, parent_id, image, name, title, expandable, connections, is_root // eslint-disable-line + }) { + // to setup values passed via constructor + $.extend(this, arguments[0]); + + this.expanded = 0; + + me.nodes[this.id] = this; + me.make_node_element(this); + + if (!me.all_nodes_expanded) { + me.setup_node_click_action(this); + } + + me.setup_edit_node_action(this); + } + }; + } + + make_node_element(node) { + let node_card = frappe.render_template('node_card', { + id: node.id, + name: node.name, + title: node.title, + image: node.image, + parent: node.parent_id, + connections: node.connections, + is_mobile: false + }); + + node.parent.append(node_card); + node.$link = $(`#${node.id}`); + } + + show() { + frappe.breadcrumbs.add('HR'); + + this.setup_actions(); + if ($(`[data-fieldname="company"]`).length) return; + let me = this; + + let company = this.page.add_field({ + fieldtype: 'Link', + options: 'Company', + fieldname: 'company', + placeholder: __('Select Company'), + default: frappe.defaults.get_default('company'), + only_select: true, + reqd: 1, + change: () => { + me.company = undefined; + + if (company.get_value() && me.company != company.get_value()) { + me.company = company.get_value(); + + // svg for connectors + me.make_svg_markers(); + me.setup_hierarchy(); + me.render_root_nodes(); + me.all_nodes_expanded = false; + } + } + }); + + company.refresh(); + $(`[data-fieldname="company"]`).trigger('change'); + $(`[data-fieldname="company"] .link-field`).css('z-index', 2); + } + + setup_actions() { + let me = this; + this.page.clear_inner_toolbar(); + this.page.add_inner_button(__('Export'), function() { + me.export_chart(); + }); + + this.page.add_inner_button(__('Expand All'), function() { + me.load_children(me.root_node, true); + me.all_nodes_expanded = true; + + me.page.remove_inner_button(__('Expand All')); + me.page.add_inner_button(__('Collapse All'), function() { + me.setup_hierarchy(); + me.render_root_nodes(); + me.all_nodes_expanded = false; + + me.page.remove_inner_button(__('Collapse All')); + me.setup_actions(); + }); + }); + } + + export_chart() { + frappe.dom.freeze(__('Exporting...')); + this.page.main.css({ + 'min-height': '', + 'max-height': '', + 'overflow': 'visible', + 'position': 'fixed', + 'left': '0', + 'top': '0' + }); + + $('.node-card').addClass('exported'); + + html2canvas(document.querySelector('#hierarchy-chart-wrapper'), { + scrollY: -window.scrollY, + scrollX: 0 + }).then(function(canvas) { + // Export the canvas to its data URI representation + let dataURL = canvas.toDataURL('image/png'); + + // download the image + let a = document.createElement('a'); + a.href = dataURL; + a.download = 'hierarchy_chart'; + a.click(); + }).finally(() => { + frappe.dom.unfreeze(); + }); + + this.setup_page_style(); + $('.node-card').removeClass('exported'); + } + + setup_hierarchy() { + if (this.$hierarchy) + this.$hierarchy.remove(); + + $(`#connectors`).empty(); + + // setup hierarchy + this.$hierarchy = $( + `
    +
  • +
      +
    • +
    `); + + this.page.main + .find('#hierarchy-chart-wrapper') + .append(this.$hierarchy); + + this.nodes = {}; + this.all_nodes_expanded = false; + } + + make_svg_markers() { + $('#hierarchy-chart-wrapper').remove(); + + this.page.main.append(` +
    + + + + + + + + + + + + + + + + + + + +
    `); + } + + render_root_nodes(expanded_view=false) { + let me = this; + + return frappe.call({ + method: me.method, + args: { + company: me.company + } + }).then(r => { + if (r.message.length) { + let expand_node = undefined; + let node = undefined; + + $.each(r.message, (i, data) => { + node = new me.Node({ + id: data.id, + parent: $('
  • ').appendTo(me.$hierarchy.find('.node-children')), + parent_id: undefined, + image: data.image, + name: data.name, + title: data.title, + expandable: true, + connections: data.connections, + is_root: true + }); + + if (!expand_node && data.connections) + expand_node = node; + }); + + me.root_node = expand_node; + if (!expanded_view) { + me.expand_node(expand_node); + } + } + }); + } + + expand_node(node) { + const is_sibling = this.selected_node && this.selected_node.parent_id === node.parent_id; + this.set_selected_node(node); + this.show_active_path(node); + this.collapse_previous_level_nodes(node); + + // since the previous node collapses, all connections to that node need to be rebuilt + // if a sibling node is clicked, connections don't need to be rebuilt + if (!is_sibling) { + // rebuild outgoing connections + this.refresh_connectors(node.parent_id); + + // rebuild incoming connections + let grandparent = $(`#${node.parent_id}`).attr('data-parent'); + this.refresh_connectors(grandparent); + } + + if (node.expandable && !node.expanded) { + return this.load_children(node); + } + } + + collapse_node() { + if (this.selected_node.expandable) { + this.selected_node.$children.hide(); + $(`path[data-parent="${this.selected_node.id}"]`).hide(); + this.selected_node.expanded = false; + } + } + + show_active_path(node) { + // mark node parent on active path + $(`#${node.parent_id}`).addClass('active-path'); + } + + load_children(node, deep=false) { + if (!deep) { + frappe.run_serially([ + () => this.get_child_nodes(node.id), + (child_nodes) => this.render_child_nodes(node, child_nodes) + ]); + } else { + frappe.run_serially([ + () => frappe.dom.freeze(), + () => this.setup_hierarchy(), + () => this.render_root_nodes(true), + () => this.get_all_nodes(node.id, node.name), + (data_list) => this.render_children_of_all_nodes(data_list), + () => frappe.dom.unfreeze() + ]); + } + } + + get_child_nodes(node_id) { + return new Promise(resolve => { + frappe.call({ + method: this.method, + args: { + parent: node_id, + company: this.company + } + }).then(r => resolve(r.message)); + }); + } + + render_child_nodes(node, child_nodes) { + const last_level = this.$hierarchy.find('.level:last').index(); + const current_level = $(`#${node.id}`).parent().parent().parent().index(); + + if (last_level === current_level) { + this.$hierarchy.append(` +
  • + `); + } + + if (!node.$children) { + node.$children = $('
      ') + .hide() + .appendTo(this.$hierarchy.find('.level:last')); + + node.$children.empty(); + + if (child_nodes) { + $.each(child_nodes, (_i, data) => { + this.add_node(node, data); + setTimeout(() => { + this.add_connector(node.id, data.id); + }, 250); + }); + } + } + + node.$children.show(); + $(`path[data-parent="${node.id}"]`).show(); + node.expanded = true; + } + + get_all_nodes(node_id, node_name) { + return new Promise(resolve => { + frappe.call({ + method: 'erpnext.utilities.hierarchy_chart.get_all_nodes', + args: { + method: this.method, + company: this.company, + parent: node_id, + parent_name: node_name + }, + callback: (r) => { + resolve(r.message); + } + }); + }); + } + + render_children_of_all_nodes(data_list) { + let entry = undefined; + let node = undefined; + + while (data_list.length) { + // to avoid overlapping connectors + entry = data_list.shift(); + node = this.nodes[entry.parent]; + if (node) { + this.render_child_nodes_for_expanded_view(node, entry.data); + } else if (data_list.length) { + data_list.push(entry); + } + } + } + + render_child_nodes_for_expanded_view(node, child_nodes) { + node.$children = $('
        '); + + const last_level = this.$hierarchy.find('.level:last').index(); + const node_level = $(`#${node.id}`).parent().parent().parent().index(); + + if (last_level === node_level) { + this.$hierarchy.append(` +
      • + `); + node.$children.appendTo(this.$hierarchy.find('.level:last')); + } else { + node.$children.appendTo(this.$hierarchy.find('.level:eq(' + (node_level + 1) + ')')); + } + + node.$children.hide().empty(); + + if (child_nodes) { + $.each(child_nodes, (_i, data) => { + this.add_node(node, data); + setTimeout(() => { + this.add_connector(node.id, data.id); + }, 250); + }); + } + + node.$children.show(); + $(`path[data-parent="${node.id}"]`).show(); + node.expanded = true; + } + + add_node(node, data) { + return new this.Node({ + id: data.id, + parent: $('
      • ').appendTo(node.$children), + parent_id: node.id, + image: data.image, + name: data.name, + title: data.title, + expandable: data.expandable, + connections: data.connections, + children: undefined + }); + } + + add_connector(parent_id, child_id) { + // using pure javascript for better performance + const parent_node = document.querySelector(`#${parent_id}`); + const child_node = document.querySelector(`#${child_id}`); + + let path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); + + // we need to connect right side of the parent to the left side of the child node + const pos_parent_right = { + x: parent_node.offsetLeft + parent_node.offsetWidth, + y: parent_node.offsetTop + parent_node.offsetHeight / 2 + }; + const pos_child_left = { + x: child_node.offsetLeft - 5, + y: child_node.offsetTop + child_node.offsetHeight / 2 + }; + + const connector = this.get_connector(pos_parent_right, pos_child_left); + + path.setAttribute('d', connector); + this.set_path_attributes(path, parent_id, child_id); + + document.getElementById('connectors').appendChild(path); + } + + get_connector(pos_parent_right, pos_child_left) { + if (pos_parent_right.y === pos_child_left.y) { + // don't add arcs if it's a straight line + return "M" + + (pos_parent_right.x) + "," + (pos_parent_right.y) + " " + + "L"+ + (pos_child_left.x) + "," + (pos_child_left.y); + } else { + let arc_1 = ""; + let arc_2 = ""; + let offset = 0; + + if (pos_parent_right.y > pos_child_left.y) { + // if child is above parent on Y axis 1st arc is anticlocwise + // second arc is clockwise + arc_1 = "a10,10 1 0 0 10,-10 "; + arc_2 = "a10,10 0 0 1 10,-10 "; + offset = 10; + } else { + // if child is below parent on Y axis 1st arc is clockwise + // second arc is anticlockwise + arc_1 = "a10,10 0 0 1 10,10 "; + arc_2 = "a10,10 1 0 0 10,10 "; + offset = -10; + } + + return "M" + (pos_parent_right.x) + "," + (pos_parent_right.y) + " " + + "L" + + (pos_parent_right.x + 40) + "," + (pos_parent_right.y) + " " + + arc_1 + + "L" + + (pos_parent_right.x + 50) + "," + (pos_child_left.y + offset) + " " + + arc_2 + + "L"+ + (pos_child_left.x) + "," + (pos_child_left.y); + } + } + + set_path_attributes(path, parent_id, child_id) { + path.setAttribute("data-parent", parent_id); + path.setAttribute("data-child", child_id); + const parent = $(`#${parent_id}`); + + if (parent.hasClass('active')) { + path.setAttribute("class", "active-connector"); + path.setAttribute("marker-start", "url(#arrowstart-active)"); + path.setAttribute("marker-end", "url(#arrowhead-active)"); + } else { + path.setAttribute("class", "collapsed-connector"); + path.setAttribute("marker-start", "url(#arrowstart-collapsed)"); + path.setAttribute("marker-end", "url(#arrowhead-collapsed)"); + } + } + + set_selected_node(node) { + // remove active class from the current node + if (this.selected_node) + this.selected_node.$link.removeClass('active'); + + // add active class to the newly selected node + this.selected_node = node; + node.$link.addClass('active'); + } + + collapse_previous_level_nodes(node) { + let node_parent = $(`#${node.parent_id}`); + let previous_level_nodes = node_parent.parent().parent().children('li'); + let node_card = undefined; + + previous_level_nodes.each(function() { + node_card = $(this).find('.node-card'); + + if (!node_card.hasClass('active-path')) { + node_card.addClass('collapsed'); + } + }); + } + + refresh_connectors(node_parent) { + if (!node_parent) return; + + $(`path[data-parent="${node_parent}"]`).remove(); + + frappe.run_serially([ + () => this.get_child_nodes(node_parent), + (child_nodes) => { + if (child_nodes) { + $.each(child_nodes, (_i, data) => { + this.add_connector(node_parent, data.id); + }); + } + } + ]); + } + + setup_node_click_action(node) { + let me = this; + let node_element = $(`#${node.id}`); + + node_element.click(function() { + const is_sibling = me.selected_node.parent_id === node.parent_id; + + if (is_sibling) { + me.collapse_node(); + } else if (node_element.is(':visible') + && (node_element.hasClass('collapsed') || node_element.hasClass('active-path'))) { + me.remove_levels_after_node(node); + me.remove_orphaned_connectors(); + } + + me.expand_node(node); + }); + } + + setup_edit_node_action(node) { + let node_element = $(`#${node.id}`); + let me = this; + + node_element.find('.btn-edit-node').click(function() { + frappe.set_route('Form', me.doctype, node.id); + }); + } + + remove_levels_after_node(node) { + let level = $(`#${node.id}`).parent().parent().parent().index(); + + level = $('.hierarchy > li:eq('+ level + ')'); + level.nextAll('li').remove(); + + let nodes = level.find('.node-card'); + let node_object = undefined; + + $.each(nodes, (_i, element) => { + node_object = this.nodes[element.id]; + node_object.expanded = 0; + node_object.$children = undefined; + }); + + nodes.removeClass('collapsed active-path'); + } + + remove_orphaned_connectors() { + let paths = $('#connectors > path'); + $.each(paths, (_i, path) => { + const parent = $(path).data('parent'); + const child = $(path).data('child'); + + if ($(`#${parent}`).length && $(`#${child}`).length) + return; + + $(path).remove(); + }); + } +}; diff --git a/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js b/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js new file mode 100644 index 00000000000..b1b78c05174 --- /dev/null +++ b/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js @@ -0,0 +1,551 @@ +erpnext.HierarchyChartMobile = class { + /* Options: + - doctype + - wrapper: wrapper for the hierarchy view + - method: + - to get the data for each node + - this method should return id, name, title, image, and connections for each node + */ + constructor(doctype, wrapper, method) { + this.page = wrapper.page; + this.method = method; + this.doctype = doctype; + + this.page.main.css({ + 'min-height': '300px', + 'max-height': '600px', + 'overflow': 'auto', + 'position': 'relative' + }); + this.page.main.addClass('frappe-card'); + + this.nodes = {}; + this.setup_node_class(); + } + + setup_node_class() { + let me = this; + this.Node = class { + constructor({ + id, parent, parent_id, image, name, title, expandable, connections, is_root // eslint-disable-line + }) { + // to setup values passed via constructor + $.extend(this, arguments[0]); + + this.expanded = 0; + + me.nodes[this.id] = this; + me.make_node_element(this); + me.setup_node_click_action(this); + me.setup_edit_node_action(this); + } + }; + } + + make_node_element(node) { + let node_card = frappe.render_template('node_card', { + id: node.id, + name: node.name, + title: node.title, + image: node.image, + parent: node.parent_id, + connections: node.connections, + is_mobile: true + }); + + node.parent.append(node_card); + node.$link = $(`#${node.id}`); + node.$link.addClass('mobile-node'); + } + + show() { + frappe.breadcrumbs.add('HR'); + + let me = this; + if ($(`[data-fieldname="company"]`).length) return; + + let company = this.page.add_field({ + fieldtype: 'Link', + options: 'Company', + fieldname: 'company', + placeholder: __('Select Company'), + default: frappe.defaults.get_default('company'), + only_select: true, + reqd: 1, + change: () => { + me.company = undefined; + + if (company.get_value() && me.company != company.get_value()) { + me.company = company.get_value(); + + // svg for connectors + me.make_svg_markers(); + + if (me.$sibling_group) + me.$sibling_group.remove(); + + // setup sibling group wrapper + me.$sibling_group = $(`
        `); + me.page.main.append(me.$sibling_group); + + me.setup_hierarchy(); + me.render_root_nodes(); + } + } + }); + + company.refresh(); + $(`[data-fieldname="company"]`).trigger('change'); + } + + make_svg_markers() { + $('#arrows').remove(); + + this.page.main.prepend(` + + + + + + + + + + + + + + + + + + + `); + } + + setup_hierarchy() { + $(`#connectors`).empty(); + if (this.$hierarchy) + this.$hierarchy.remove(); + + if (this.$sibling_group) + this.$sibling_group.empty(); + + this.$hierarchy = $( + `
          +
        • +
        `); + + this.page.main.append(this.$hierarchy); + } + + render_root_nodes() { + let me = this; + + frappe.call({ + method: me.method, + args: { + company: me.company + }, + }).then(r => { + if (r.message.length) { + let root_level = me.$hierarchy.find('.root-level'); + root_level.empty(); + + $.each(r.message, (_i, data) => { + return new me.Node({ + id: data.id, + parent: root_level, + parent_id: undefined, + image: data.image, + name: data.name, + title: data.title, + expandable: true, + connections: data.connections, + is_root: true + }); + }); + } + }); + } + + expand_node(node) { + const is_same_node = (this.selected_node && this.selected_node.id === node.id); + this.set_selected_node(node); + this.show_active_path(node); + + if (this.$sibling_group) { + const sibling_parent = this.$sibling_group.find('.node-group').attr('data-parent'); + if (node.parent_id !== undefined && node.parent_id != sibling_parent) + this.$sibling_group.empty(); + } + + if (!is_same_node) { + // since the previous/parent node collapses, all connections to that node need to be rebuilt + // rebuild outgoing connections of parent + this.refresh_connectors(node.parent_id, node.id); + + // rebuild incoming connections of parent + let grandparent = $(`#${node.parent_id}`).attr('data-parent'); + this.refresh_connectors(grandparent, node.parent_id); + } + + if (node.expandable && !node.expanded) { + return this.load_children(node); + } + } + + collapse_node() { + let node = this.selected_node; + if (node.expandable && node.$children) { + node.$children.hide(); + node.expanded = 0; + + // add a collapsed level to show the collapsed parent + // and a button beside it to move to that level + let node_parent = node.$link.parent(); + node_parent.prepend( + `
        ` + ); + + node_parent + .find('.collapsed-level') + .append(node.$link); + + frappe.run_serially([ + () => this.get_child_nodes(node.parent_id, node.id), + (child_nodes) => this.get_node_group(child_nodes, node.parent_id), + (node_group) => node_parent.find('.collapsed-level').append(node_group), + () => this.setup_node_group_action() + ]); + } + } + + show_active_path(node) { + // mark node parent on active path + $(`#${node.parent_id}`).addClass('active-path'); + } + + load_children(node) { + frappe.run_serially([ + () => this.get_child_nodes(node.id), + (child_nodes) => this.render_child_nodes(node, child_nodes) + ]); + } + + get_child_nodes(node_id, exclude_node=null) { + let me = this; + return new Promise(resolve => { + frappe.call({ + method: this.method, + args: { + parent: node_id, + company: me.company, + exclude_node: exclude_node + } + }).then(r => resolve(r.message)); + }); + } + + render_child_nodes(node, child_nodes) { + if (!node.$children) { + node.$children = $('
          ') + .hide() + .appendTo(node.$link.parent()); + + node.$children.empty(); + + if (child_nodes) { + $.each(child_nodes, (_i, data) => { + this.add_node(node, data); + $(`#${data.id}`).addClass('active-child'); + + setTimeout(() => { + this.add_connector(node.id, data.id); + }, 250); + }); + } + } + + node.$children.show(); + node.expanded = 1; + } + + add_node(node, data) { + var $li = $('
        • '); + + return new this.Node({ + id: data.id, + parent: $li.appendTo(node.$children), + parent_id: node.id, + image: data.image, + name: data.name, + title: data.title, + expandable: data.expandable, + connections: data.connections, + children: undefined + }); + } + + add_connector(parent_id, child_id) { + const parent_node = document.querySelector(`#${parent_id}`); + const child_node = document.querySelector(`#${child_id}`); + + const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); + + let connector = undefined; + + if ($(`#${parent_id}`).hasClass('active')) { + connector = this.get_connector_for_active_node(parent_node, child_node); + } else if ($(`#${parent_id}`).hasClass('active-path')) { + connector = this.get_connector_for_collapsed_node(parent_node, child_node); + } + + path.setAttribute('d', connector); + this.set_path_attributes(path, parent_id, child_id); + + document.getElementById('connectors').appendChild(path); + } + + get_connector_for_active_node(parent_node, child_node) { + // we need to connect the bottom left of the parent to the left side of the child node + let pos_parent_bottom = { + x: parent_node.offsetLeft + 20, + y: parent_node.offsetTop + parent_node.offsetHeight + }; + let pos_child_left = { + x: child_node.offsetLeft - 5, + y: child_node.offsetTop + child_node.offsetHeight / 2 + }; + + let connector = + "M" + + (pos_parent_bottom.x) + "," + (pos_parent_bottom.y) + " " + + "L" + + (pos_parent_bottom.x) + "," + (pos_child_left.y - 10) + " " + + "a10,10 1 0 0 10,10 " + + "L" + + (pos_child_left.x) + "," + (pos_child_left.y); + + return connector; + } + + get_connector_for_collapsed_node(parent_node, child_node) { + // we need to connect the bottom left of the parent to the top left of the child node + let pos_parent_bottom = { + x: parent_node.offsetLeft + 20, + y: parent_node.offsetTop + parent_node.offsetHeight + }; + let pos_child_top = { + x: child_node.offsetLeft + 20, + y: child_node.offsetTop + }; + + let connector = + "M" + + (pos_parent_bottom.x) + "," + (pos_parent_bottom.y) + " " + + "L" + + (pos_child_top.x) + "," + (pos_child_top.y); + + return connector; + } + + set_path_attributes(path, parent_id, child_id) { + path.setAttribute("data-parent", parent_id); + path.setAttribute("data-child", child_id); + const parent = $(`#${parent_id}`); + + if (parent.hasClass('active')) { + path.setAttribute("class", "active-connector"); + path.setAttribute("marker-start", "url(#arrowstart-active)"); + path.setAttribute("marker-end", "url(#arrowhead-active)"); + } else if (parent.hasClass('active-path')) { + path.setAttribute("class", "collapsed-connector"); + } + } + + set_selected_node(node) { + // remove .active class from the current node + if (this.selected_node) + this.selected_node.$link.removeClass('active'); + + // add active class to the newly selected node + this.selected_node = node; + node.$link.addClass('active'); + } + + setup_node_click_action(node) { + let me = this; + let node_element = $(`#${node.id}`); + + node_element.click(function() { + let el = undefined; + + if (node.is_root) { + el = $(this).detach(); + me.$hierarchy.empty(); + $(`#connectors`).empty(); + me.add_node_to_hierarchy(el, node); + } else if (node_element.is(':visible') && node_element.hasClass('active-path')) { + me.remove_levels_after_node(node); + me.remove_orphaned_connectors(); + } else { + el = $(this).detach(); + me.add_node_to_hierarchy(el, node); + me.collapse_node(); + } + + me.expand_node(node); + }); + } + + setup_edit_node_action(node) { + let node_element = $(`#${node.id}`); + let me = this; + + node_element.find('.btn-edit-node').click(function() { + frappe.set_route('Form', me.doctype, node.id); + }); + } + + setup_node_group_action() { + let me = this; + + $('.node-group').on('click', function() { + let parent = $(this).attr('data-parent'); + if (parent === 'undefined') { + me.setup_hierarchy(); + me.render_root_nodes(); + } else { + me.expand_sibling_group_node(parent); + } + }); + } + + add_node_to_hierarchy(node_element, node) { + this.$hierarchy.append(`
        • `); + node_element.removeClass('active-child active-path'); + this.$hierarchy.find('.level:last').append(node_element); + + let node_object = this.nodes[node.id]; + node_object.expanded = 0; + node_object.$children = undefined; + this.nodes[node.id] = node_object; + } + + get_node_group(nodes, parent, collapsed=true) { + let limit = 2; + const display_nodes = nodes.slice(0, limit); + const extra_nodes = nodes.slice(limit); + + let html = display_nodes.map(node => + this.get_avatar(node) + ).join(''); + + if (extra_nodes.length === 1) { + let node = extra_nodes[0]; + html += this.get_avatar(node); + } else if (extra_nodes.length > 1) { + html = ` + ${html} + +
          + +${extra_nodes.length} +
          +
          + `; + } + + if (html) { + const $node_group = + $(`
          +
          + ${html} +
          +
          `); + + if (collapsed) + $node_group.addClass('collapsed'); + + return $node_group; + } + + return null; + } + + get_avatar(node) { + return ` + + `; + } + + expand_sibling_group_node(parent) { + let node_object = this.nodes[parent]; + let node = node_object.$link; + + node.removeClass('active-child active-path'); + node_object.expanded = 0; + node_object.$children = undefined; + this.nodes[node.id] = node_object; + + // show parent's siblings and expand parent node + frappe.run_serially([ + () => this.get_child_nodes(node_object.parent_id, node_object.id), + (child_nodes) => this.get_node_group(child_nodes, node_object.parent_id, false), + (node_group) => { + if (node_group) + this.$sibling_group.empty().append(node_group); + }, + () => this.setup_node_group_action(), + () => this.reattach_and_expand_node(node, node_object) + ]); + } + + reattach_and_expand_node(node, node_object) { + var el = node.detach(); + + this.$hierarchy.empty().append(` +
        • + `); + this.$hierarchy.find('.level').append(el); + $(`#connectors`).empty(); + this.expand_node(node_object); + } + + remove_levels_after_node(node) { + let level = $(`#${node.id}`).parent().parent().index(); + + level = $('.hierarchy-mobile > li:eq('+ level + ')'); + level.nextAll('li').remove(); + + let node_object = this.nodes[node.id]; + let current_node = level.find(`#${node.id}`).detach(); + current_node.removeClass('active-child active-path'); + + node_object.expanded = 0; + node_object.$children = undefined; + + level.empty().append(current_node); + } + + remove_orphaned_connectors() { + let paths = $('#connectors > path'); + $.each(paths, (_i, path) => { + const parent = $(path).data('parent'); + const child = $(path).data('child'); + + if ($(`#${parent}`).length && $(`#${child}`).length) + return; + + $(path).remove(); + }); + } + + refresh_connectors(node_parent, node_id) { + if (!node_parent) return; + + $(`path[data-parent="${node_parent}"]`).remove(); + this.add_connector(node_parent, node_id); + } +}; diff --git a/erpnext/public/js/hub/components/ReviewArea.vue b/erpnext/public/js/hub/components/ReviewArea.vue index 5e4e439f3d0..aa83bb0e465 100644 --- a/erpnext/public/js/hub/components/ReviewArea.vue +++ b/erpnext/public/js/hub/components/ReviewArea.vue @@ -137,4 +137,4 @@ export default { } } } - \ No newline at end of file + diff --git a/erpnext/public/js/hub/components/ReviewTimelineItem.vue b/erpnext/public/js/hub/components/ReviewTimelineItem.vue index f0fe001973c..d0e83f3b1cd 100644 --- a/erpnext/public/js/hub/components/ReviewTimelineItem.vue +++ b/erpnext/public/js/hub/components/ReviewTimelineItem.vue @@ -51,4 +51,3 @@ export default { } } - diff --git a/erpnext/public/js/hub/pages/FeaturedItems.vue b/erpnext/public/js/hub/pages/FeaturedItems.vue index 63ae7e99bbd..8380b2b2c0b 100644 --- a/erpnext/public/js/hub/pages/FeaturedItems.vue +++ b/erpnext/public/js/hub/pages/FeaturedItems.vue @@ -69,7 +69,7 @@ export default { const item_name = this.items.filter(item => item.hub_item_name === hub_item_name); - alert_message = __('{0} removed. {1}', [item_name, + alert_message = __('{0} removed. {1}', [item_name, `${__('Undo')}`]); alert = frappe.show_alert(alert_message, grace_period / 1000, { diff --git a/erpnext/public/js/hub/pages/Publish.vue b/erpnext/public/js/hub/pages/Publish.vue index 96fa0aae4e5..ecba4b1e5a8 100644 --- a/erpnext/public/js/hub/pages/Publish.vue +++ b/erpnext/public/js/hub/pages/Publish.vue @@ -78,7 +78,7 @@ export default { empty_state_message: __('No Items selected yet. Browse and click on items below to publish.'), valid_items_instruction: __('Only items with an image and description can be published. Please update them if an item in your inventory does not appear.'), last_sync_message: (hub.settings.last_sync_datetime) - ? __('Last sync was {0}.', [`${comment_when(hub.settings.last_sync_datetime)}`]) + + ? __('Last sync was {0}.', [`${comment_when(hub.settings.last_sync_datetime)}`]) + ` ${__('See your Published Items.')}` : '' }; diff --git a/erpnext/public/js/hub/pages/Seller.vue b/erpnext/public/js/hub/pages/Seller.vue index c0903c64c37..3c9b800f4a0 100644 --- a/erpnext/public/js/hub/pages/Seller.vue +++ b/erpnext/public/js/hub/pages/Seller.vue @@ -24,7 +24,7 @@
          - {{ item_container_heading }} + {{ item_container_heading }} Customize your Featured Items @@ -160,7 +160,7 @@ export default { ]; setTimeout(() => this.init_seller_traffic_chart(), 1); - + }); }, diff --git a/erpnext/public/js/hub/vue-plugins.js b/erpnext/public/js/hub/vue-plugins.js index 6e6a7cb5989..4912d684991 100644 --- a/erpnext/public/js/hub/vue-plugins.js +++ b/erpnext/public/js/hub/vue-plugins.js @@ -55,4 +55,4 @@ const handleImage = (el, src) => { Vue.filter('striphtml', function (text) { return strip_html(text || ''); -}); \ No newline at end of file +}); diff --git a/erpnext/public/js/leaflet/leaflet.draw.js b/erpnext/public/js/leaflet/leaflet.draw.js index 4352f7025b7..26f1e19da5a 100755 --- a/erpnext/public/js/leaflet/leaflet.draw.js +++ b/erpnext/public/js/leaflet/leaflet.draw.js @@ -140,4 +140,4 @@ e.on("click", this._removeLayer, this) }, _disableLayerDelete: function(t) { var e = t.layer || t.target || t; e.off("click", this._removeLayer, this), this._deletedLayers.removeLayer(e) }, _removeLayer: function(t) { var e = t.layer || t.target || t; this._deletableLayers.removeLayer(e), this._deletedLayers.addLayer(e) }, _onMouseMove: function(t) { this._tooltip.updatePosition(t.latlng) }, _hasAvailableLayers: function() { return 0 !== this._deletableLayers.getLayers().length } }) -}(window, document); \ No newline at end of file +}(window, document); diff --git a/erpnext/public/js/leaflet/leaflet.js b/erpnext/public/js/leaflet/leaflet.js index 41d9bb9ed4c..91dd3d434c7 100755 --- a/erpnext/public/js/leaflet/leaflet.js +++ b/erpnext/public/js/leaflet/leaflet.js @@ -768,4 +768,4 @@ r = this._locateOptions; if (r.setView) { var a = this.getBoundsZoom(s); this.setView(n, r.maxZoom ? Math.min(a, r.maxZoom) : a) } var h = { latlng: n, bounds: s, timestamp: t.timestamp }; for (var l in t.coords) "number" == typeof t.coords[l] && (h[l] = t.coords[l]); this.fire("locationfound", h) } }) -}(window, document); \ No newline at end of file +}(window, document); diff --git a/erpnext/public/js/payment/payments.js b/erpnext/public/js/payment/payments.js index ddf87068097..57a2aee52e9 100644 --- a/erpnext/public/js/payment/payments.js +++ b/erpnext/public/js/payment/payments.js @@ -231,4 +231,4 @@ erpnext.payments = erpnext.stock.StockController.extend({ $(this.$body).find('.outstanding_amount').text(format_currency(this.frm.doc.outstanding_amount, frappe.get_doc(":Company", this.frm.doc.company).default_currency)); this.update_invoice(); } -}) \ No newline at end of file +}) diff --git a/erpnext/public/js/projects/timer.js b/erpnext/public/js/projects/timer.js index 26be997d482..0e5c0d3720e 100644 --- a/erpnext/public/js/projects/timer.js +++ b/erpnext/public/js/projects/timer.js @@ -159,4 +159,4 @@ erpnext.timesheet.control_timer = function(frm, dialog, row, timestamp=0) { $btn_complete.hide(); $btn_start.show(); } -}; \ No newline at end of file +}; diff --git a/erpnext/public/js/setup_wizard.js b/erpnext/public/js/setup_wizard.js index 6f5d67c7462..38e1eb5156c 100644 --- a/erpnext/public/js/setup_wizard.js +++ b/erpnext/public/js/setup_wizard.js @@ -147,7 +147,7 @@ erpnext.setup.slides_settings = [ } // Validate bank name - if(me.values.bank_account) { + if(me.values.bank_account) { frappe.call({ async: false, method: "erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts.validate_bank_account", diff --git a/erpnext/public/js/stock_analytics.js b/erpnext/public/js/stock_analytics.js index 140c9dc90b6..7085315485e 100644 --- a/erpnext/public/js/stock_analytics.js +++ b/erpnext/public/js/stock_analytics.js @@ -202,4 +202,3 @@ erpnext.StockAnalytics = erpnext.StockGridReport.extend({ frappe.set_route("query-report", "Stock Ledger"); } }); - diff --git a/erpnext/public/js/telephony.js b/erpnext/public/js/telephony.js index 9548d6c5f36..54f99e6e5db 100644 --- a/erpnext/public/js/telephony.js +++ b/erpnext/public/js/telephony.js @@ -6,7 +6,7 @@ frappe.ui.form.ControlData = frappe.ui.form.ControlData.extend( { } if (this.frm && this.frm.fields_dict) { Object.values(this.frm.fields_dict).forEach(function(field) { - if (field.df.read_only === 1 && field.df.options === 'Phone' + if (field.df.read_only === 1 && field.df.options === 'Phone' && field.disp_area.style[0] != 'display' && !field.has_icon) { field.setup_phone(); field.has_icon = true; diff --git a/erpnext/public/js/templates/item_quick_entry.html b/erpnext/public/js/templates/item_quick_entry.html index 6a5f36da77c..e5e78690622 100644 --- a/erpnext/public/js/templates/item_quick_entry.html +++ b/erpnext/public/js/templates/item_quick_entry.html @@ -1,3 +1,3 @@
          {{ __("Variant Attributes") }}
          -
          \ No newline at end of file +
          diff --git a/erpnext/public/js/templates/item_selector.html b/erpnext/public/js/templates/item_selector.html index 58fb26c0e4c..86a15f49072 100644 --- a/erpnext/public/js/templates/item_selector.html +++ b/erpnext/public/js/templates/item_selector.html @@ -34,4 +34,4 @@
          {% if ((i % 4 === 3) || (i===data.length - 1)) { %}
          {% } %} {% endfor %} -
          \ No newline at end of file +
          diff --git a/erpnext/public/js/templates/node_card.html b/erpnext/public/js/templates/node_card.html new file mode 100644 index 00000000000..4cb6ee03c0c --- /dev/null +++ b/erpnext/public/js/templates/node_card.html @@ -0,0 +1,33 @@ +
          +
          +
          + + + +
          +
          +
          + {{ name }} +
          + {{ frappe.utils.icon("edit", "xs") }} + {{ __("Edit") }} +
          +
          +
          +
          {{ title }}
          + + {% if is_mobile %} +
          + · {{ connections }} +
          + {% else %} + {% if connections == 1 %} +
          · {{ connections }} Connection
          + {% else %} +
          · {{ connections }} Connections
          + {% endif %} + {% endif %} +
          +
          +
          +
          diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index ce40ced11f2..85c1693657f 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -82,6 +82,17 @@ $.extend(erpnext, { }); frappe.set_route('Form','Journal Entry', journal_entry.name); }); + }, + + proceed_save_with_reminders_frequency_change: () => { + frappe.ui.hide_open_dialog(); + + frappe.call({ + method: 'erpnext.hr.doctype.hr_settings.hr_settings.set_proceed_with_frequency_change', + callback: () => { + cur_frm.save(); + } + }); } }); @@ -465,7 +476,23 @@ erpnext.utils.update_child_items = function(opts) { in_list_view: 1, read_only: 0, disabled: 0, - label: __('Item Code') + label: __('Item Code'), + get_query: function() { + let filters; + if (frm.doc.doctype == 'Sales Order') { + filters = {"is_sales_item": 1}; + } else if (frm.doc.doctype == 'Purchase Order') { + if (frm.doc.is_subcontracted == "Yes") { + filters = {"is_sub_contracted_item": 1}; + } else { + filters = {"is_purchase_item": 1}; + } + } + return { + query: "erpnext.controllers.queries.item_query", + filters: filters + }; + } }, { fieldtype:'Link', fieldname:'uom', @@ -547,7 +574,7 @@ erpnext.utils.update_child_items = function(opts) { }, ], primary_action: function() { - const trans_items = this.get_values()["trans_items"]; + const trans_items = this.get_values()["trans_items"].filter((item) => !!item.item_code); frappe.call({ method: 'erpnext.controllers.accounts_controller.update_child_qty_rate', freeze: true, diff --git a/erpnext/public/js/utils/customer_quick_entry.js b/erpnext/public/js/utils/customer_quick_entry.js index ebe6cd98f81..53aadd8166f 100644 --- a/erpnext/public/js/utils/customer_quick_entry.js +++ b/erpnext/public/js/utils/customer_quick_entry.js @@ -1,9 +1,9 @@ frappe.provide('frappe.ui.form'); frappe.ui.form.CustomerQuickEntryForm = frappe.ui.form.QuickEntryForm.extend({ - init: function(doctype, after_insert) { + init: function(doctype, after_insert, init_callback, doc, force) { + this._super(doctype, after_insert, init_callback, doc, force); this.skip_redirect_on_error = true; - this._super(doctype, after_insert); }, render_dialog: function() { @@ -78,4 +78,4 @@ frappe.ui.form.CustomerQuickEntryForm = frappe.ui.form.QuickEntryForm.extend({ return variant_fields; }, -}) \ No newline at end of file +}) diff --git a/erpnext/public/js/utils/dimension_tree_filter.js b/erpnext/public/js/utils/dimension_tree_filter.js index 96e181788e3..bb23f1512b9 100644 --- a/erpnext/public/js/utils/dimension_tree_filter.js +++ b/erpnext/public/js/utils/dimension_tree_filter.js @@ -100,4 +100,4 @@ erpnext.accounts.dimensions = { }); } } -}; \ No newline at end of file +}; diff --git a/erpnext/public/js/utils/item_quick_entry.js b/erpnext/public/js/utils/item_quick_entry.js index 27ef107acef..2930d8d9e34 100644 --- a/erpnext/public/js/utils/item_quick_entry.js +++ b/erpnext/public/js/utils/item_quick_entry.js @@ -404,4 +404,4 @@ frappe.ui.form.ItemQuickEntryForm = frappe.ui.form.QuickEntryForm.extend({ } return attribute; } -}); \ No newline at end of file +}); diff --git a/erpnext/public/js/utils/item_selector.js b/erpnext/public/js/utils/item_selector.js index d04c488a59d..a4775c7ec39 100644 --- a/erpnext/public/js/utils/item_selector.js +++ b/erpnext/public/js/utils/item_selector.js @@ -107,4 +107,4 @@ erpnext.ItemSelector = Class.extend({ me.dialog.results.html(frappe.render_template('item_selector', {'data':r.values})); }); } -}); \ No newline at end of file +}); diff --git a/erpnext/public/js/utils/party.js b/erpnext/public/js/utils/party.js index a79eadc7619..a492b32a9f6 100644 --- a/erpnext/public/js/utils/party.js +++ b/erpnext/public/js/utils/party.js @@ -76,6 +76,7 @@ erpnext.utils.get_party_details = function(frm, method, args, callback) { if (args) { args.posting_date = frm.doc.posting_date || frm.doc.transaction_date; + args.fetch_payment_terms_template = cint(!frm.doc.ignore_default_payment_terms_template); } } if (!args || !args.party) return; @@ -288,8 +289,8 @@ erpnext.utils.get_shipping_address = function(frm, callback) { company: frm.doc.company, address: frm.doc.shipping_address }, - callback: function(r){ - if (r.message){ + callback: function(r) { + if (r.message) { frm.set_value("shipping_address", r.message[0]) //Address title or name frm.set_value("shipping_address_display", r.message[1]) //Address to be displayed on the page } diff --git a/erpnext/public/js/utils/supplier_quick_entry.js b/erpnext/public/js/utils/supplier_quick_entry.js new file mode 100644 index 00000000000..8d591a96510 --- /dev/null +++ b/erpnext/public/js/utils/supplier_quick_entry.js @@ -0,0 +1,77 @@ +frappe.provide('frappe.ui.form'); + +frappe.ui.form.SupplierQuickEntryForm = class SupplierQuickEntryForm extends frappe.ui.form.QuickEntryForm { + constructor(doctype, after_insert, init_callback, doc, force) { + super(doctype, after_insert, init_callback, doc, force); + this.skip_redirect_on_error = true; + } + + render_dialog() { + this.mandatory = this.mandatory.concat(this.get_variant_fields()); + super.render_dialog(); + } + + get_variant_fields() { + var variant_fields = [ + { + fieldtype: "Section Break", + label: __("Primary Contact Details"), + collapsible: 1 + }, + { + label: __("Email Id"), + fieldname: "email_id", + fieldtype: "Data" + }, + { + fieldtype: "Column Break" + }, + { + label: __("Mobile Number"), + fieldname: "mobile_no", + fieldtype: "Data" + }, + { + fieldtype: "Section Break", + label: __("Primary Address Details"), + collapsible: 1 + }, + { + label: __("Address Line 1"), + fieldname: "address_line1", + fieldtype: "Data" + }, + { + label: __("Address Line 2"), + fieldname: "address_line2", + fieldtype: "Data" + }, + { + label: __("ZIP Code"), + fieldname: "pincode", + fieldtype: "Data" + }, + { + fieldtype: "Column Break" + }, + { + label: __("City"), + fieldname: "city", + fieldtype: "Data" + }, + { + label: __("State"), + fieldname: "state", + fieldtype: "Data" + }, + { + label: __("Country"), + fieldname: "country", + fieldtype: "Link", + options: "Country" + } + ]; + + return variant_fields; + } +}; diff --git a/erpnext/public/less/email.less b/erpnext/public/less/email.less index 4077c4940d1..738290fd85f 100644 --- a/erpnext/public/less/email.less +++ b/erpnext/public/less/email.less @@ -29,4 +29,4 @@ color: @border-color; border: 1px solid @border-color; background-color: #fff; -} \ No newline at end of file +} diff --git a/erpnext/public/less/erpnext.less b/erpnext/public/less/erpnext.less index 4076ebec1fd..4dea747f8b8 100644 --- a/erpnext/public/less/erpnext.less +++ b/erpnext/public/less/erpnext.less @@ -491,4 +491,4 @@ body[data-route="pos"] { .exercise-col { padding: 10px; -} \ No newline at end of file +} diff --git a/erpnext/public/less/pos.less b/erpnext/public/less/pos.less index b081ed4414b..fdb813ff41c 100644 --- a/erpnext/public/less/pos.less +++ b/erpnext/public/less/pos.less @@ -226,4 +226,4 @@ input[type=number]::-webkit-outer-spin-button { .quantity-total { font-size: 18px; -} \ No newline at end of file +} diff --git a/erpnext/public/less/products.less b/erpnext/public/less/products.less index 5e744ceac5b..d44c5173786 100644 --- a/erpnext/public/less/products.less +++ b/erpnext/public/less/products.less @@ -68,4 +68,4 @@ font-size: 12px; width: 24px; height: 24px; -} \ No newline at end of file +} diff --git a/erpnext/public/less/website.less b/erpnext/public/less/website.less index ac878de105b..171b4906443 100644 --- a/erpnext/public/less/website.less +++ b/erpnext/public/less/website.less @@ -385,4 +385,4 @@ max-height: 300px; object-fit: contain; } -} \ No newline at end of file +} diff --git a/erpnext/public/scss/hierarchy_chart.scss b/erpnext/public/scss/hierarchy_chart.scss new file mode 100644 index 00000000000..57d5e8414ae --- /dev/null +++ b/erpnext/public/scss/hierarchy_chart.scss @@ -0,0 +1,313 @@ +.node-card { + background: white; + stroke: 1px solid var(--gray-200); + box-shadow: var(--shadow-base); + border-radius: 0.5rem; + padding: 0.75rem; + margin-left: 3rem; + width: 18rem; + overflow: hidden; + + .btn-edit-node { + display: none; + } + + .edit-chart-node { + display: none; + } + + .node-edit-icon { + display: none; + } +} + +.node-card.exported { + box-shadow: none +} + +.node-image { + width: 3.0rem; + height: 3.0rem; +} + +.node-name { + font-size: 1rem; + line-height: 1.72; +} + +.node-title { + font-size: 0.75rem; + line-height: 1.35; +} + +.node-info { + width: 12.7rem; +} + +.node-connections { + font-size: 0.75rem; + line-height: 1.35; +} + +.node-card.active { + background: var(--blue-50); + border: 1px solid var(--blue-500); + box-shadow: var(--shadow-md); + border-radius: 0.5rem; + padding: 0.75rem; + width: 18rem; + + .btn-edit-node { + display: flex; + background: var(--blue-100); + color: var(--blue-500); + padding: .25rem .5rem; + font-size: .75rem; + justify-content: center; + box-shadow: var(--shadow-sm); + margin-left: auto; + } + + .edit-chart-node { + display: block; + margin-right: 0.25rem; + } + + .node-edit-icon { + display: block; + } + + .node-edit-icon > .icon{ + stroke: var(--blue-500); + } + + .node-name { + align-items: center; + justify-content: space-between; + margin-bottom: 2px; + width: 12.2rem; + } +} + +.node-card.active-path { + background: var(--blue-100); + border: 1px solid var(--blue-300); + box-shadow: var(--shadow-sm); + border-radius: 0.5rem; + padding: 0.75rem; + width: 15rem; + height: 3.0rem; + + .btn-edit-node { + display: none !important; + } + + .edit-chart-node { + display: none; + } + + .node-edit-icon { + display: none; + } + + .node-info { + display: none; + } + + .node-title { + display: none; + } + + .node-connections { + display: none; + } + + .node-name { + font-size: 0.85rem; + line-height: 1.35; + } + + .node-image { + width: 1.5rem; + height: 1.5rem; + } + + .node-meta { + align-items: baseline; + } +} + +.node-card.collapsed { + background: white; + stroke: 1px solid var(--gray-200); + box-shadow: var(--shadow-sm); + border-radius: 0.5rem; + padding: 0.75rem; + width: 15rem; + height: 3.0rem; + + .btn-edit-node { + display: none !important; + } + + .edit-chart-node { + display: none; + } + + .node-edit-icon { + display: none; + } + + .node-info { + display: none; + } + + .node-title { + display: none; + } + + .node-connections { + display: none; + } + + .node-name { + font-size: 0.85rem; + line-height: 1.35; + } + + .node-image { + width: 1.5rem; + height: 1.5rem; + } + + .node-meta { + align-items: baseline; + } +} + +// horizontal hierarchy tree view +#hierarchy-chart-wrapper { + padding-top: 30px; + + #arrows { + margin-top: -80px; + } +} + +.hierarchy { + display: flex; +} + +.hierarchy li { + list-style-type: none; +} + +.child-node { + margin: 0px 0px 16px 0px; +} + +.hierarchy, .hierarchy-mobile { + .level { + margin-right: 8px; + align-items: flex-start; + flex-direction: column; + } +} + +#arrows { + position: absolute; + overflow: visible; +} + +.active-connector { + stroke: var(--blue-500); +} + +.collapsed-connector { + stroke: var(--blue-300); +} + +// mobile + +.hierarchy-mobile { + display: flex; + flex-direction: column; + align-items: center; + padding-top: 10px; + padding-left: 0px; +} + +.hierarchy-mobile li { + list-style-type: none; + display: flex; + flex-direction: column; + align-items: flex-end; +} + +.mobile-node { + margin-left: 0; +} + +.mobile-node.active-path { + width: 12.25rem; +} + +.active-child { + width: 15.5rem; +} + +.mobile-node .node-connections { + max-width: 80px; +} + +.hierarchy-mobile .node-children { + margin-top: 16px; +} + +.root-level .node-card { + margin: 0 0 16px; +} + +// node group + +.collapsed-level { + margin-bottom: 16px; + width: 18rem; +} + +.node-group { + background: white; + border: 1px solid var(--gray-300); + box-shadow: var(--shadow-sm); + border-radius: 0.5rem; + padding: 0.75rem; + width: 18rem; + height: 3rem; + overflow: hidden; + align-items: center; +} + +.node-group .avatar-group { + margin-left: 0px; +} + +.node-group .avatar-extra-count { + background-color: var(--blue-100); + color: var(--blue-500); +} + +.node-group .avatar-frame { + width: 1.5rem; + height: 1.5rem; +} + +.node-group.collapsed { + width: 5rem; + margin-left: 12px; +} + +.sibling-group { + display: flex; + flex-direction: column; + align-items: center; +} diff --git a/erpnext/public/scss/point-of-sale.scss b/erpnext/public/scss/point-of-sale.scss index c77b2ce3dfa..1677e9b3de4 100644 --- a/erpnext/public/scss/point-of-sale.scss +++ b/erpnext/public/scss/point-of-sale.scss @@ -860,6 +860,8 @@ .invoice-fields { overflow-y: scroll; + height: 100%; + padding-right: var(--padding-sm); } } diff --git a/erpnext/public/scss/shopping_cart.scss b/erpnext/public/scss/shopping_cart.scss index 5962859be5a..490a7c4af73 100644 --- a/erpnext/public/scss/shopping_cart.scss +++ b/erpnext/public/scss/shopping_cart.scss @@ -483,4 +483,3 @@ body.product-page { border: 1px solid var(--dark-border-color); } } - diff --git a/erpnext/public/scss/website.scss b/erpnext/public/scss/website.scss index f4325c03f5b..9ea84160342 100644 --- a/erpnext/public/scss/website.scss +++ b/erpnext/public/scss/website.scss @@ -67,4 +67,4 @@ .card-body > .card-title { line-height: 1.3; } -} \ No newline at end of file +} diff --git a/erpnext/quality_management/doctype/quality_action/quality_action.js b/erpnext/quality_management/doctype/quality_action/quality_action.js index e216a7539c8..b44f2a20344 100644 --- a/erpnext/quality_management/doctype/quality_action/quality_action.js +++ b/erpnext/quality_management/doctype/quality_action/quality_action.js @@ -3,4 +3,4 @@ frappe.ui.form.on('Quality Action', { -}); \ No newline at end of file +}); diff --git a/erpnext/quality_management/doctype/quality_action/quality_action.py b/erpnext/quality_management/doctype/quality_action/quality_action.py index d6fa5051ee6..02401ba689d 100644 --- a/erpnext/quality_management/doctype/quality_action/quality_action.py +++ b/erpnext/quality_management/doctype/quality_action/quality_action.py @@ -8,4 +8,4 @@ from frappe.model.document import Document class QualityAction(Document): def validate(self): - self.status = 'Open' if any([d.status=='Open' for d in self.resolutions]) else 'Completed' \ No newline at end of file + self.status = 'Open' if any([d.status=='Open' for d in self.resolutions]) else 'Completed' diff --git a/erpnext/quality_management/doctype/quality_action/test_quality_action.py b/erpnext/quality_management/doctype/quality_action/test_quality_action.py index 24b97ca3a09..98d665f3910 100644 --- a/erpnext/quality_management/doctype/quality_action/test_quality_action.py +++ b/erpnext/quality_management/doctype/quality_action/test_quality_action.py @@ -8,4 +8,4 @@ import unittest class TestQualityAction(unittest.TestCase): # quality action has no code - pass \ No newline at end of file + pass diff --git a/erpnext/quality_management/doctype/quality_feedback/quality_feedback.py b/erpnext/quality_management/doctype/quality_feedback/quality_feedback.py index 5a8ec73cfe1..d3e96cf2d94 100644 --- a/erpnext/quality_management/doctype/quality_feedback/quality_feedback.py +++ b/erpnext/quality_management/doctype/quality_feedback/quality_feedback.py @@ -21,4 +21,3 @@ class QualityFeedback(Document): self.document_type ='User' self.document_name = frappe.session.user self.set_parameters() - diff --git a/erpnext/quality_management/doctype/quality_feedback_template/test_quality_feedback_template.py b/erpnext/quality_management/doctype/quality_feedback_template/test_quality_feedback_template.py index b3eed103836..afed14b6ad0 100644 --- a/erpnext/quality_management/doctype/quality_feedback_template/test_quality_feedback_template.py +++ b/erpnext/quality_management/doctype/quality_feedback_template/test_quality_feedback_template.py @@ -7,4 +7,4 @@ import frappe import unittest class TestQualityFeedbackTemplate(unittest.TestCase): - pass \ No newline at end of file + pass diff --git a/erpnext/quality_management/doctype/quality_goal/quality_goal.py b/erpnext/quality_management/doctype/quality_goal/quality_goal.py index f3fe986d539..3e616b75ceb 100644 --- a/erpnext/quality_management/doctype/quality_goal/quality_goal.py +++ b/erpnext/quality_management/doctype/quality_goal/quality_goal.py @@ -9,4 +9,4 @@ from frappe.model.document import Document class QualityGoal(Document): def validate(self): - pass \ No newline at end of file + pass diff --git a/erpnext/quality_management/doctype/quality_goal/test_quality_goal.py b/erpnext/quality_management/doctype/quality_goal/test_quality_goal.py index f61d6e581d7..0e135b50212 100644 --- a/erpnext/quality_management/doctype/quality_goal/test_quality_goal.py +++ b/erpnext/quality_management/doctype/quality_goal/test_quality_goal.py @@ -22,4 +22,4 @@ def get_quality_goal(): objectives = [ dict(objective = 'Check test cases', target='100', uom='Percent') ] - )).insert() \ No newline at end of file + )).insert() diff --git a/erpnext/quality_management/doctype/quality_meeting/quality_meeting.py b/erpnext/quality_management/doctype/quality_meeting/quality_meeting.py index f8de22958bd..9e453ebfc2e 100644 --- a/erpnext/quality_management/doctype/quality_meeting/quality_meeting.py +++ b/erpnext/quality_management/doctype/quality_meeting/quality_meeting.py @@ -6,4 +6,4 @@ from __future__ import unicode_literals from frappe.model.document import Document class QualityMeeting(Document): - pass \ No newline at end of file + pass diff --git a/erpnext/quality_management/doctype/quality_meeting/quality_meeting_list.js b/erpnext/quality_management/doctype/quality_meeting/quality_meeting_list.js index ff85c84dc9d..5fd1b30eb45 100644 --- a/erpnext/quality_management/doctype/quality_meeting/quality_meeting_list.js +++ b/erpnext/quality_management/doctype/quality_meeting/quality_meeting_list.js @@ -8,4 +8,4 @@ frappe.listview_settings['Quality Meeting'] = { return [__("Close"), "green", ",status=,Close"]; } } -}; \ No newline at end of file +}; diff --git a/erpnext/quality_management/doctype/quality_meeting/test_quality_meeting.py b/erpnext/quality_management/doctype/quality_meeting/test_quality_meeting.py index 754bccb06e0..6bf4c179c6b 100644 --- a/erpnext/quality_management/doctype/quality_meeting/test_quality_meeting.py +++ b/erpnext/quality_management/doctype/quality_meeting/test_quality_meeting.py @@ -8,4 +8,4 @@ import unittest class TestQualityMeeting(unittest.TestCase): # nothing to test - pass \ No newline at end of file + pass diff --git a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.js b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.js index ac876229ecb..fd2b6a4eaa0 100644 --- a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.js +++ b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.js @@ -19,4 +19,4 @@ frappe.ui.form.on('Quality Procedure', { }; }); } -}); \ No newline at end of file +}); diff --git a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.py b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.py index 53f4e6c70fe..117db0012ba 100644 --- a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.py +++ b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.py @@ -77,4 +77,4 @@ def add_node(): if args.parent_quality_procedure == 'All Quality Procedures': args.parent_quality_procedure = None - return frappe.get_doc(args).insert() \ No newline at end of file + return frappe.get_doc(args).insert() diff --git a/erpnext/quality_management/doctype/quality_procedure/quality_procedure_tree.js b/erpnext/quality_management/doctype/quality_procedure/quality_procedure_tree.js index eeb4cf617c3..2851fcc5969 100644 --- a/erpnext/quality_management/doctype/quality_procedure/quality_procedure_tree.js +++ b/erpnext/quality_management/doctype/quality_procedure/quality_procedure_tree.js @@ -31,4 +31,4 @@ frappe.treeview_settings["Quality Procedure"] = { onload: function(treeview) { treeview.make_tree(); }, -}; \ No newline at end of file +}; diff --git a/erpnext/quality_management/doctype/quality_procedure/test_quality_procedure.py b/erpnext/quality_management/doctype/quality_procedure/test_quality_procedure.py index 36bdf26acf5..4fa7734bc68 100644 --- a/erpnext/quality_management/doctype/quality_procedure/test_quality_procedure.py +++ b/erpnext/quality_management/doctype/quality_procedure/test_quality_procedure.py @@ -47,4 +47,4 @@ def create_procedure(): processes = [ dict(process_description = 'Test Step 1') ] - )).insert() \ No newline at end of file + )).insert() diff --git a/erpnext/quality_management/doctype/quality_review/quality_review.js b/erpnext/quality_management/doctype/quality_review/quality_review.js index 67371bfc5c6..0e6b7034101 100644 --- a/erpnext/quality_management/doctype/quality_review/quality_review.js +++ b/erpnext/quality_management/doctype/quality_review/quality_review.js @@ -22,4 +22,4 @@ frappe.ui.form.on('Quality Review', { } }); }, -}); \ No newline at end of file +}); diff --git a/erpnext/quality_management/doctype/quality_review/quality_review.py b/erpnext/quality_management/doctype/quality_review/quality_review.py index e3a8b073f0f..34cc890e219 100644 --- a/erpnext/quality_management/doctype/quality_review/quality_review.py +++ b/erpnext/quality_management/doctype/quality_review/quality_review.py @@ -61,4 +61,4 @@ def get_quarter(month): if month in ["January", "April", "July", "October"]: return True else: - return False \ No newline at end of file + return False diff --git a/erpnext/quality_management/doctype/quality_review/quality_review_list.js b/erpnext/quality_management/doctype/quality_review/quality_review_list.js index e2eb31b55a3..b0be783de56 100644 --- a/erpnext/quality_management/doctype/quality_review/quality_review_list.js +++ b/erpnext/quality_management/doctype/quality_review/quality_review_list.js @@ -9,4 +9,4 @@ frappe.listview_settings['Quality Review'] = { return [__("Action Initialised"), "red", "action,=,Action Initialised"]; } } -}; \ No newline at end of file +}; diff --git a/erpnext/quality_management/doctype/quality_review/test_quality_review.py b/erpnext/quality_management/doctype/quality_review/test_quality_review.py index a7d92da8ace..161ecd01ef1 100644 --- a/erpnext/quality_management/doctype/quality_review/test_quality_review.py +++ b/erpnext/quality_management/doctype/quality_review/test_quality_review.py @@ -19,4 +19,4 @@ class TestQualityReview(unittest.TestCase): self.assertEqual(quality_goal.objectives[0].target, quality_review.reviews[0].target) quality_review.delete() - quality_goal.delete() \ No newline at end of file + quality_goal.delete() diff --git a/erpnext/regional/address_template/setup.py b/erpnext/regional/address_template/setup.py index 9f318de3451..1b4087d77ba 100644 --- a/erpnext/regional/address_template/setup.py +++ b/erpnext/regional/address_template/setup.py @@ -10,7 +10,7 @@ def set_up_address_templates(default_country=None): def get_address_templates(): """ Return country and path for all HTML files in this directory. - + Returns a list of dicts. """ def country(file_name): diff --git a/erpnext/regional/address_template/templates/france.html b/erpnext/regional/address_template/templates/france.html new file mode 100644 index 00000000000..752331eeec9 --- /dev/null +++ b/erpnext/regional/address_template/templates/france.html @@ -0,0 +1,5 @@ +{% if address_line1 %}{{ address_line1 }}{% endif -%} +{% if address_line2 %}
          {{ address_line2 }}{% endif -%} +{% if pincode %}
          {{ pincode }}{% endif -%} +{% if city %} {{ city }}{% endif -%} +{% if country %}
          {{ country }}{% endif -%} diff --git a/erpnext/regional/address_template/templates/germany.html b/erpnext/regional/address_template/templates/germany.html index 7fa4c32612a..25c9c9d32e6 100644 --- a/erpnext/regional/address_template/templates/germany.html +++ b/erpnext/regional/address_template/templates/germany.html @@ -3,6 +3,6 @@ {% if country in ["Germany", "Deutschland"] %} {{ pincode }} {{ city }} {% else %} - {{ pincode }} {{ city | upper }}
          + {{ pincode }} {{ city | upper }}
          {{ country | upper }} {% endif %} diff --git a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.js b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.js index cc2d9f06d2d..54e488610df 100644 --- a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.js +++ b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.js @@ -3,7 +3,7 @@ frappe.ui.form.on('E Invoice Settings', { refresh(frm) { - const docs_link = 'https://docs.erpnext.com/docs/user/manual/en/regional/india/setup-e-invoicing'; + const docs_link = 'https://docs.erpnext.com/docs/v13/user/manual/en/regional/india/setup-e-invoicing'; frm.dashboard.set_headline( __("Read {0} for more information on E Invoicing features.", [`documentation`]) ); diff --git a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py index c24ad886ea1..4f6b3eca7a6 100644 --- a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py +++ b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py @@ -11,4 +11,3 @@ class EInvoiceSettings(Document): def validate(self): if self.enable and not self.credentials: frappe.throw(_('You must add atleast one credentials to be able to use E Invoicing.')) - diff --git a/erpnext/regional/doctype/gst_hsn_code/gst_hsn_code.js b/erpnext/regional/doctype/gst_hsn_code/gst_hsn_code.js index 7ff4de48639..347fdfe61b6 100644 --- a/erpnext/regional/doctype/gst_hsn_code/gst_hsn_code.js +++ b/erpnext/regional/doctype/gst_hsn_code/gst_hsn_code.js @@ -25,4 +25,4 @@ frappe.ui.form.on('GST HSN Code', { }); } } -}); \ No newline at end of file +}); diff --git a/erpnext/regional/doctype/gst_hsn_code/gst_hsn_code.py b/erpnext/regional/doctype/gst_hsn_code/gst_hsn_code.py index 86cd4d1545d..4791dc26753 100644 --- a/erpnext/regional/doctype/gst_hsn_code/gst_hsn_code.py +++ b/erpnext/regional/doctype/gst_hsn_code/gst_hsn_code.py @@ -30,4 +30,4 @@ def update_item_document(items, taxes): 'tax_category': tax.tax_category, 'valid_from': tax.valid_from }) - item_to_be_updated.save() \ No newline at end of file + item_to_be_updated.save() diff --git a/erpnext/regional/doctype/gst_hsn_code/test_gst_hsn_code.js b/erpnext/regional/doctype/gst_hsn_code/test_gst_hsn_code.js deleted file mode 100644 index 24c5fd355ff..00000000000 --- a/erpnext/regional/doctype/gst_hsn_code/test_gst_hsn_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: GST HSN Code", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new GST HSN Code - () => frappe.tests.make('GST HSN Code', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/regional/doctype/gst_settings/test_gst_settings.js b/erpnext/regional/doctype/gst_settings/test_gst_settings.js deleted file mode 100644 index 00fcca6f326..00000000000 --- a/erpnext/regional/doctype/gst_settings/test_gst_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: GST Settings", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new GST Settings - () => frappe.tests.make('GST Settings', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.html b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.html index 3b6a45a3b42..f3fc60fdb63 100644 --- a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.html +++ b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.html @@ -294,4 +294,4 @@ text-align: right; } - \ No newline at end of file + diff --git a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py index 641520437fb..0ee5b097b54 100644 --- a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py +++ b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py @@ -214,9 +214,8 @@ class GSTR3BReport(Document): for d in item_details: if d.item_code not in self.invoice_items.get(d.parent, {}): - self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, - sum((i.get('taxable_value', 0) or i.get('base_net_amount', 0)) for i in item_details - if i.item_code == d.item_code and i.parent == d.parent)) + self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, 0.0) + self.invoice_items[d.parent][d.item_code] += d.get('taxable_value', 0) or d.get('base_net_amount', 0) if d.is_nil_exempt and d.item_code not in self.is_nil_exempt: self.is_nil_exempt.append(d.item_code) @@ -281,9 +280,15 @@ class GSTR3BReport(Document): if self.get('invoice_items'): # Build itemised tax for export invoices, nil and exempted where tax table is blank for invoice, items in iteritems(self.invoice_items): - if invoice not in self.items_based_on_tax_rate and (self.invoice_detail_map.get(invoice, {}).get('export_type') - == "Without Payment of Tax"): + if invoice not in self.items_based_on_tax_rate and self.invoice_detail_map.get(invoice, {}).get('export_type') \ + == "Without Payment of Tax" and self.invoice_detail_map.get(invoice, {}).get('gst_category') == "Overseas": self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault(0, items.keys()) + else: + for item in items.keys(): + if item in self.is_nil_exempt + self.is_non_gst and \ + item not in self.items_based_on_tax_rate.get(invoice, {}).get(0, []): + self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault(0, []) + self.items_based_on_tax_rate[invoice][0].append(item) def set_outward_taxable_supplies(self): inter_state_supply_details = {} @@ -322,6 +327,9 @@ class GSTR3BReport(Document): inter_state_supply_details[(gst_category, place_of_supply)]['txval'] += taxable_value inter_state_supply_details[(gst_category, place_of_supply)]['iamt'] += (taxable_value * rate /100) + if self.invoice_cess.get(inv): + self.report_dict['sup_details']['osup_det']['csamt'] += flt(self.invoice_cess.get(inv), 2) + self.set_inter_state_supply(inter_state_supply_details) def set_supplies_liable_to_reverse_charge(self): diff --git a/erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.js b/erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.js index c2d6edfc773..5918ec8b316 100644 --- a/erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.js +++ b/erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.js @@ -43,4 +43,4 @@ frappe.ui.form.on('Import Supplier Invoice', { } } -}); \ No newline at end of file +}); diff --git a/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py index ad60db05595..656c3296e58 100644 --- a/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py +++ b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py @@ -13,7 +13,7 @@ class LowerDeductionCertificate(Document): def validate(self): self.validate_dates() self.validate_supplier_against_section_code() - + def validate_dates(self): if getdate(self.valid_upto) < getdate(self.valid_from): frappe.throw(_("Valid Upto date cannot be before Valid From date")) @@ -44,4 +44,4 @@ class LowerDeductionCertificate(Document): return True elif getdate(self.valid_from) <= valid_from and valid_upto <= getdate(self.valid_upto): return True - return False \ No newline at end of file + return False diff --git a/erpnext/regional/doctype/south_africa_vat_settings/__init__.py b/erpnext/regional/doctype/south_africa_vat_settings/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/regional/doctype/south_africa_vat_settings/south_africa_vat_settings.js b/erpnext/regional/doctype/south_africa_vat_settings/south_africa_vat_settings.js new file mode 100644 index 00000000000..e37a61ac853 --- /dev/null +++ b/erpnext/regional/doctype/south_africa_vat_settings/south_africa_vat_settings.js @@ -0,0 +1,23 @@ +// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('South Africa VAT Settings', { + refresh: function(frm) { + frm.set_query("company", function() { + return { + filters: { + country: "South Africa", + } + }; + }); + frm.set_query("account", "vat_accounts", function() { + return { + filters: { + company: frm.doc.company, + account_type: "Tax", + is_group: 0 + } + }; + }); + } +}); diff --git a/erpnext/regional/doctype/south_africa_vat_settings/south_africa_vat_settings.json b/erpnext/regional/doctype/south_africa_vat_settings/south_africa_vat_settings.json new file mode 100644 index 00000000000..8a51829c419 --- /dev/null +++ b/erpnext/regional/doctype/south_africa_vat_settings/south_africa_vat_settings.json @@ -0,0 +1,76 @@ +{ + "actions": [], + "autoname": "field:company", + "creation": "2021-07-08 22:34:33.668015", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "company", + "vat_accounts" + ], + "fields": [ + { + "fieldname": "company", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Company", + "options": "Company", + "reqd": 1, + "unique": 1 + }, + { + "fieldname": "vat_accounts", + "fieldtype": "Table", + "label": "VAT Accounts", + "options": "South Africa VAT Account", + "reqd": 1 + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2021-07-14 02:17:52.476762", + "modified_by": "Administrator", + "module": "Regional", + "name": "South Africa VAT Settings", + "owner": "Administrator", + "permissions": [ + { + "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 + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Auditor", + "share": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/regional/doctype/south_africa_vat_settings/south_africa_vat_settings.py b/erpnext/regional/doctype/south_africa_vat_settings/south_africa_vat_settings.py new file mode 100644 index 00000000000..d74154bfe78 --- /dev/null +++ b/erpnext/regional/doctype/south_africa_vat_settings/south_africa_vat_settings.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 SouthAfricaVATSettings(Document): + pass diff --git a/erpnext/regional/doctype/south_africa_vat_settings/test_south_africa_vat_settings.py b/erpnext/regional/doctype/south_africa_vat_settings/test_south_africa_vat_settings.py new file mode 100644 index 00000000000..1c36652ad6e --- /dev/null +++ b/erpnext/regional/doctype/south_africa_vat_settings/test_south_africa_vat_settings.py @@ -0,0 +1,8 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +import unittest + +class TestSouthAfricaVATSettings(unittest.TestCase): + pass diff --git a/erpnext/regional/doctype/tax_exemption_80g_certificate/test_tax_exemption_80g_certificate.py b/erpnext/regional/doctype/tax_exemption_80g_certificate/test_tax_exemption_80g_certificate.py index c478b0f3228..41b42036687 100644 --- a/erpnext/regional/doctype/tax_exemption_80g_certificate/test_tax_exemption_80g_certificate.py +++ b/erpnext/regional/doctype/tax_exemption_80g_certificate/test_tax_exemption_80g_certificate.py @@ -98,4 +98,4 @@ def create_80g_certificate(args): certificate.update(args) - return certificate \ No newline at end of file + return certificate diff --git a/erpnext/regional/germany/utils/datev/datev_constants.py b/erpnext/regional/germany/utils/datev/datev_constants.py index 63f9a777bb5..be3d7a3e542 100644 --- a/erpnext/regional/germany/utils/datev/datev_constants.py +++ b/erpnext/regional/germany/utils/datev/datev_constants.py @@ -455,7 +455,7 @@ ACCOUNT_NAME_COLUMNS = [ "Konto", # Account name "Kontenbeschriftung", - # Language of the account name + # Language of the account name # "de-DE" or "en-GB" "Sprach-ID" ] diff --git a/erpnext/regional/germany/utils/datev/datev_csv.py b/erpnext/regional/germany/utils/datev/datev_csv.py index 122c15fd811..c5c2bc41f4d 100644 --- a/erpnext/regional/germany/utils/datev/datev_csv.py +++ b/erpnext/regional/germany/utils/datev/datev_csv.py @@ -33,6 +33,14 @@ def get_datev_csv(data, filters, csv_class): if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS: result['Belegdatum'] = pd.to_datetime(result['Belegdatum']) + result['Beleginfo - Inhalt 6'] = pd.to_datetime(result['Beleginfo - Inhalt 6']) + result['Beleginfo - Inhalt 6'] = result['Beleginfo - Inhalt 6'].dt.strftime('%d%m%Y') + + result['Fälligkeit'] = pd.to_datetime(result['Fälligkeit']) + result['Fälligkeit'] = result['Fälligkeit'].dt.strftime('%d%m%y') + + result.sort_values(by='Belegdatum', inplace=True, kind='stable', ignore_index=True) + if csv_class.DATA_CATEGORY == DataCategory.ACCOUNT_NAMES: result['Sprach-ID'] = 'de-DE' diff --git a/erpnext/regional/india/e_invoice/einvoice.js b/erpnext/regional/india/e_invoice/einvoice.js index 23d4fe9030b..348f0c6feed 100644 --- a/erpnext/regional/india/e_invoice/einvoice.js +++ b/erpnext/regional/india/e_invoice/einvoice.js @@ -1,6 +1,8 @@ erpnext.setup_einvoice_actions = (doctype) => { frappe.ui.form.on(doctype, { async refresh(frm) { + if (frm.doc.docstatus == 2) return; + const res = await frappe.call({ method: 'erpnext.regional.india.e_invoice.utils.validate_eligibility', args: { doc: frm.doc } @@ -111,7 +113,7 @@ erpnext.setup_einvoice_actions = (doctype) => { if (irn && ewaybill && !irn_cancelled && !eway_bill_cancelled) { const action = () => { - let message = __('Cancellation of e-way bill is currently not supported. '); + let message = __('Cancellation of e-way bill is currently not supported.') + ' '; message += '

          '; message += __('You must first use the portal to cancel the e-way bill and then update the cancelled status in the ERPNext system.'); @@ -287,4 +289,4 @@ const show_einvoice_preview = (frm, einvoice) => { } } }); -}; \ No newline at end of file +}; diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 11ebef724c4..fe4c172e237 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -42,7 +42,10 @@ def validate_eligibility(doc): invalid_company = not frappe.db.get_value('E Invoice User', { 'company': doc.get('company') }) invalid_supply_type = doc.get('gst_category') not in ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export'] company_transaction = doc.get('billing_address_gstin') == doc.get('company_gstin') - no_taxes_applied = not doc.get('taxes') + + # if export invoice, then taxes can be empty + # invoice can only be ineligible if no taxes applied and is not an export invoice + no_taxes_applied = not doc.get('taxes') and not doc.get('gst_category') == 'Overseas' has_non_gst_item = any(d for d in doc.get('items', []) if d.get('is_non_gst')) if invalid_company or invalid_supply_type or company_transaction or no_taxes_applied or has_non_gst_item: @@ -187,10 +190,13 @@ def get_item_list(invoice): item.description = sanitize_for_json(d.item_name) item.qty = abs(item.qty) - - item.unit_rate = abs((abs(item.taxable_value) - item.discount_amount)/ item.qty) - item.gross_amount = abs(item.taxable_value) + item.discount_amount + if flt(item.qty) != 0.0: + item.unit_rate = abs(item.taxable_value / item.qty) + else: + item.unit_rate = abs(item.taxable_value) + item.gross_amount = abs(item.taxable_value) item.taxable_value = abs(item.taxable_value) + item.discount_amount = 0 item.batch_expiry_date = frappe.db.get_value('Batch', d.batch_no, 'expiry_date') if d.batch_no else None item.batch_expiry_date = format_date(item.batch_expiry_date, 'dd/mm/yyyy') if item.batch_expiry_date else None @@ -312,10 +318,6 @@ def get_payment_details(invoice): )) def get_return_doc_reference(invoice): - if not invoice.return_against: - frappe.throw(_('For generating IRN, reference to the original invoice is mandatory for a credit note. Please set {} field to generate e-invoice.') - .format(frappe.bold('Return Against')), title=_('Missing Field')) - invoice_date = frappe.db.get_value('Sales Invoice', invoice.return_against, 'posting_date') return frappe._dict(dict( invoice_name=invoice.return_against, invoice_date=format_date(invoice_date, 'dd/mm/yyyy') @@ -431,7 +433,7 @@ def make_einvoice(invoice): if invoice.is_pos and invoice.base_paid_amount: payment_details = get_payment_details(invoice) - if invoice.is_return: + if invoice.is_return and invoice.return_against: prev_doc_details = get_return_doc_reference(invoice) if invoice.transporter and not invoice.is_return: @@ -962,7 +964,7 @@ class GSPConnector(): "attached_to_doctype": doctype, "attached_to_name": docname, "attached_to_field": "qrcode_image", - "is_private": 1, + "is_private": 0, "content": qr_image.getvalue()}) _file.save() frappe.db.commit() @@ -1123,4 +1125,4 @@ def check_scheduler_status(): def job_already_enqueued(job_name): enqueued_jobs = [d.get("job_name") for d in get_info()] if job_name in enqueued_jobs: - return True \ No newline at end of file + return True diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index 5b11665858e..2e34f694ffb 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -12,7 +12,10 @@ from erpnext.accounts.utils import get_fiscal_year, FiscalYearError from frappe.utils import today def setup(company=None, patch=True): - setup_company_independent_fixtures(patch=patch) + # Company independent fixtures should be called only once at the first company setup + if frappe.db.count('Company', {'country': 'India'}) <=1: + setup_company_independent_fixtures(patch=patch) + if not patch: make_fixtures(company) @@ -454,7 +457,7 @@ def make_custom_fields(update=True): depends_on='eval:in_list(["Registered Regular", "SEZ", "Overseas", "Deemed Export"], doc.gst_category) && doc.irn_cancelled === 0'), dict(fieldname='irn_cancelled', label='IRN Cancelled', fieldtype='Check', no_copy=1, print_hide=1, - depends_on='eval:(doc.irn_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'), + depends_on='eval: doc.irn', allow_on_submit=1, insert_after='customer'), dict(fieldname='eway_bill_validity', label='E-Way Bill Validity', fieldtype='Data', no_copy=1, print_hide=1, depends_on='ewaybill', read_only=1, allow_on_submit=1, insert_after='ewaybill'), @@ -659,9 +662,9 @@ def make_custom_fields(update=True): 'label': 'Export Type', 'fieldtype': 'Select', 'insert_after': 'gst_category', - 'default': 'Without Payment of Tax', 'depends_on':'eval:in_list(["SEZ", "Overseas"], doc.gst_category)', - 'options': '\nWith Payment of Tax\nWithout Payment of Tax' + 'options': '\nWith Payment of Tax\nWithout Payment of Tax', + 'mandatory_depends_on': 'eval:in_list(["SEZ", "Overseas"], doc.gst_category)' } ], 'Customer': [ @@ -678,9 +681,9 @@ def make_custom_fields(update=True): 'label': 'Export Type', 'fieldtype': 'Select', 'insert_after': 'gst_category', - 'default': 'Without Payment of Tax', 'depends_on':'eval:in_list(["SEZ", "Overseas", "Deemed Export"], doc.gst_category)', - 'options': '\nWith Payment of Tax\nWithout Payment of Tax' + 'options': '\nWith Payment of Tax\nWithout Payment of Tax', + 'mandatory_depends_on': 'eval:in_list(["SEZ", "Overseas", "Deemed Export"], doc.gst_category)' } ], 'Member': [ @@ -809,7 +812,7 @@ def set_tax_withholding_category(company): doc.flags.ignore_mandatory = True doc.insert() else: - doc = frappe.get_doc("Tax Withholding Category", d.get("name")) + doc = frappe.get_doc("Tax Withholding Category", d.get("name"), for_update=True) if accounts: doc.append("accounts", accounts[0]) @@ -1005,4 +1008,4 @@ def create_gratuity_rule(): def update_accounts_settings_for_taxes(): if frappe.db.count('Company') == 1: - frappe.db.set_value('Accounts Settings', None, "add_taxes_from_item_tax_template", 0) \ No newline at end of file + frappe.db.set_value('Accounts Settings', None, "add_taxes_from_item_tax_template", 0) diff --git a/erpnext/regional/india/taxes.js b/erpnext/regional/india/taxes.js index d3b7ea3b1a0..5f6dcdeb922 100644 --- a/erpnext/regional/india/taxes.js +++ b/erpnext/regional/india/taxes.js @@ -49,4 +49,3 @@ erpnext.setup_auto_gst_taxation = (doctype) => { } }); } - diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index a6f4ee91d04..94910fed188 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -431,9 +431,11 @@ def get_ewb_data(dt, dn): company_address = frappe.get_doc('Address', doc.company_address) billing_address = frappe.get_doc('Address', doc.customer_address) + #added dispatch address + dispatch_address = frappe.get_doc('Address', doc.dispatch_address_name) if doc.dispatch_address_name else company_address shipping_address = frappe.get_doc('Address', doc.shipping_address_name) - data = get_address_details(data, doc, company_address, billing_address) + data = get_address_details(data, doc, company_address, billing_address, dispatch_address) data.itemList = [] data.totalValue = doc.total @@ -473,7 +475,7 @@ def get_ewb_data(dt, dn): ewaybills.append(data) data = { - 'version': '1.0.1118', + 'version': '1.0.0421', 'billLists': ewaybills } @@ -519,10 +521,10 @@ def get_gstins_for_company(company): `tabDynamic Link`.link_name = %(company)s""", {"company": company}) return company_gstins -def get_address_details(data, doc, company_address, billing_address): +def get_address_details(data, doc, company_address, billing_address, dispatch_address): data.fromPincode = validate_pincode(company_address.pincode, 'Company Address') - data.fromStateCode = data.actualFromStateCode = validate_state_code( - company_address.gst_state_number, 'Company Address') + data.fromStateCode = validate_state_code(company_address.gst_state_number, 'Company Address') + data.actualFromStateCode = validate_state_code(dispatch_address.gst_state_number, 'Dispatch Address') if not doc.billing_address_gstin or len(doc.billing_address_gstin) < 15: data.toGstin = 'URP' @@ -843,14 +845,22 @@ def get_depreciation_amount(asset, depreciable_value, row): depreciation_left = flt(row.total_number_of_depreciations) - flt(asset.number_of_depreciations_booked) if row.depreciation_method in ("Straight Line", "Manual"): - depreciation_amount = (flt(row.value_after_depreciation) - - flt(row.expected_value_after_useful_life)) / depreciation_left + # if the Depreciation Schedule is being prepared for the first time + if not asset.flags.increase_in_asset_life: + depreciation_amount = (flt(row.value_after_depreciation) - + flt(row.expected_value_after_useful_life)) / depreciation_left + + # if the Depreciation Schedule is being modified after Asset Repair + else: + depreciation_amount = (flt(row.value_after_depreciation) - + flt(row.expected_value_after_useful_life)) / (date_diff(asset.to_date, asset.available_for_use_date) / 365) + else: rate_of_depreciation = row.rate_of_depreciation # if its the first depreciation if depreciable_value == asset.gross_purchase_amount: # as per IT act, if the asset is purchased in the 2nd half of fiscal year, then rate is divided by 2 - diff = date_diff(asset.available_for_use_date, row.depreciation_start_date) + diff = date_diff(row.depreciation_start_date, asset.available_for_use_date) if diff <= 180: rate_of_depreciation = rate_of_depreciation / 2 frappe.msgprint( @@ -858,4 +868,32 @@ def get_depreciation_amount(asset, depreciable_value, row): depreciation_amount = flt(depreciable_value * (flt(rate_of_depreciation) / 100)) - return depreciation_amount \ No newline at end of file + return depreciation_amount + +def set_item_tax_from_hsn_code(item): + if not item.taxes and item.gst_hsn_code: + hsn_doc = frappe.get_doc("GST HSN Code", item.gst_hsn_code) + + for tax in hsn_doc.taxes: + item.append('taxes', { + 'item_tax_template': tax.item_tax_template, + 'tax_category': tax.tax_category, + 'valid_from': tax.valid_from + }) + +def delete_gst_settings_for_company(doc, method): + if doc.country != 'India': + return + + gst_settings = frappe.get_doc("GST Settings") + records_to_delete = [] + + for d in reversed(gst_settings.get('gst_accounts')): + if d.company == doc.name: + records_to_delete.append(d) + + for d in records_to_delete: + gst_settings.remove(d) + + gst_settings.save() + diff --git a/erpnext/regional/italy/__init__.py b/erpnext/regional/italy/__init__.py index ef1d5822ba1..4932f660ca5 100644 --- a/erpnext/regional/italy/__init__.py +++ b/erpnext/regional/italy/__init__.py @@ -76,4 +76,4 @@ state_codes = {'Siracusa': 'SR', 'Bologna': 'BO', 'Grosseto': 'GR', 'Caserta': ' 'Cagliari': 'CA', 'Siena': 'SI', 'Vibo Valentia': 'VV', 'Reggio Calabria': 'RC', 'Ascoli Piceno': 'AP', 'Carbonia-Iglesias': 'CI', 'Oristano': 'OR', 'Asti': 'AT', 'Ravenna': 'RA', 'Vicenza': 'VI', 'Savona': 'SV', 'Biella': 'BI', 'Rimini': 'RN', 'Agrigento': 'AG', 'Prato': 'PO', 'Cuneo': 'CN', 'Cosenza': 'CS', 'Livorno or Leghorn': 'LI', 'Sondrio': 'SO', 'Cremona': 'CR', 'Isernia': 'IS', 'Trento': 'TN', 'Terni': 'TR', 'Bolzano/Bozen': 'BZ', - 'Parma': 'PR', 'Varese': 'VA', 'Venezia': 'VE', 'Sassari': 'SS', 'Arezzo': 'AR'} \ No newline at end of file + 'Parma': 'PR', 'Varese': 'VA', 'Venezia': 'VE', 'Sassari': 'SS', 'Arezzo': 'AR'} diff --git a/erpnext/regional/print_format/detailed_tax_invoice/detailed_tax_invoice.json b/erpnext/regional/print_format/detailed_tax_invoice/detailed_tax_invoice.json index ab56c6bc3cf..f67e2450a29 100644 --- a/erpnext/regional/print_format/detailed_tax_invoice/detailed_tax_invoice.json +++ b/erpnext/regional/print_format/detailed_tax_invoice/detailed_tax_invoice.json @@ -16,7 +16,7 @@ "name": "Detailed Tax Invoice", "owner": "Administrator", "print_format_builder": 1, - "print_format_type": "Server", + "print_format_type": "Jinja", "show_section_headings": 0, "standard": "Yes" } \ No newline at end of file diff --git a/erpnext/regional/print_format/gst_tax_invoice/gst_tax_invoice.json b/erpnext/regional/print_format/gst_tax_invoice/gst_tax_invoice.json index 7d8e675e9fe..b23206b0bdd 100644 --- a/erpnext/regional/print_format/gst_tax_invoice/gst_tax_invoice.json +++ b/erpnext/regional/print_format/gst_tax_invoice/gst_tax_invoice.json @@ -16,7 +16,7 @@ "name": "GST Tax Invoice", "owner": "Administrator", "print_format_builder": 1, - "print_format_type": "Server", + "print_format_type": "Jinja", "show_section_headings": 0, "standard": "Yes" } \ No newline at end of file diff --git a/erpnext/regional/print_format/simplified_tax_invoice/simplified_tax_invoice.json b/erpnext/regional/print_format/simplified_tax_invoice/simplified_tax_invoice.json index b324f6ee683..aed2e89ed79 100644 --- a/erpnext/regional/print_format/simplified_tax_invoice/simplified_tax_invoice.json +++ b/erpnext/regional/print_format/simplified_tax_invoice/simplified_tax_invoice.json @@ -16,7 +16,7 @@ "name": "Simplified Tax Invoice", "owner": "Administrator", "print_format_builder": 1, - "print_format_type": "Server", + "print_format_type": "Jinja", "show_section_headings": 0, "standard": "Yes" } \ No newline at end of file diff --git a/erpnext/regional/print_format/tax_invoice/tax_invoice.json b/erpnext/regional/print_format/tax_invoice/tax_invoice.json index 74db06727bf..7479891595e 100644 --- a/erpnext/regional/print_format/tax_invoice/tax_invoice.json +++ b/erpnext/regional/print_format/tax_invoice/tax_invoice.json @@ -16,7 +16,7 @@ "name": "Tax Invoice", "owner": "Administrator", "print_format_builder": 1, - "print_format_type": "Server", + "print_format_type": "Jinja", "show_section_headings": 0, "standard": "Yes" } \ No newline at end of file diff --git a/erpnext/regional/report/datev/datev.py b/erpnext/regional/report/datev/datev.py index a5ca7eee5d4..8f077e3de0b 100644 --- a/erpnext/regional/report/datev/datev.py +++ b/erpnext/regional/report/datev/datev.py @@ -43,6 +43,12 @@ COLUMNS = [ "fieldtype": "Data", "width": 100 }, + { + "label": "BU-Schlüssel", + "fieldname": "BU-Schlüssel", + "fieldtype": "Data", + "width": 100 + }, { "label": "Belegdatum", "fieldname": "Belegdatum", @@ -114,6 +120,36 @@ COLUMNS = [ "fieldname": "Beleginfo - Inhalt 4", "fieldtype": "Data", "width": 150 + }, + { + "label": "Beleginfo - Art 5", + "fieldname": "Beleginfo - Art 5", + "fieldtype": "Data", + "width": 150 + }, + { + "label": "Beleginfo - Inhalt 5", + "fieldname": "Beleginfo - Inhalt 5", + "fieldtype": "Data", + "width": 100 + }, + { + "label": "Beleginfo - Art 6", + "fieldname": "Beleginfo - Art 6", + "fieldtype": "Data", + "width": 150 + }, + { + "label": "Beleginfo - Inhalt 6", + "fieldname": "Beleginfo - Inhalt 6", + "fieldtype": "Date", + "width": 100 + }, + { + "label": "Fälligkeit", + "fieldname": "Fälligkeit", + "fieldtype": "Date", + "width": 100 } ] @@ -161,6 +197,125 @@ def validate_fiscal_year(from_date, to_date, company): def get_transactions(filters, as_dict=1): + def run(params_method, filters): + extra_fields, extra_joins, extra_filters = params_method(filters) + return run_query(filters, extra_fields, extra_joins, extra_filters, as_dict=as_dict) + + def sort_by(row): + # "Belegdatum" is in the fifth column when list format is used + return row["Belegdatum" if as_dict else 5] + + type_map = { + # specific query methods for some voucher types + "Payment Entry": get_payment_entry_params, + "Sales Invoice": get_sales_invoice_params, + "Purchase Invoice": get_purchase_invoice_params + } + + only_voucher_type = filters.get("voucher_type") + transactions = [] + + for voucher_type, get_voucher_params in type_map.items(): + if only_voucher_type and only_voucher_type != voucher_type: + continue + + transactions.extend(run(params_method=get_voucher_params, filters=filters)) + + if not only_voucher_type or only_voucher_type not in type_map: + # generic query method for all other voucher types + filters["exclude_voucher_types"] = type_map.keys() + transactions.extend(run(params_method=get_generic_params, filters=filters)) + + return sorted(transactions, key=sort_by) + + +def get_payment_entry_params(filters): + extra_fields = """ + , 'Zahlungsreferenz' as 'Beleginfo - Art 5' + , pe.reference_no as 'Beleginfo - Inhalt 5' + , 'Buchungstag' as 'Beleginfo - Art 6' + , pe.reference_date as 'Beleginfo - Inhalt 6' + , '' as 'Fälligkeit' + """ + + extra_joins = """ + LEFT JOIN `tabPayment Entry` pe + ON gl.voucher_no = pe.name + """ + + extra_filters = """ + AND gl.voucher_type = 'Payment Entry' + """ + + return extra_fields, extra_joins, extra_filters + + +def get_sales_invoice_params(filters): + extra_fields = """ + , '' as 'Beleginfo - Art 5' + , '' as 'Beleginfo - Inhalt 5' + , '' as 'Beleginfo - Art 6' + , '' as 'Beleginfo - Inhalt 6' + , si.due_date as 'Fälligkeit' + """ + + extra_joins = """ + LEFT JOIN `tabSales Invoice` si + ON gl.voucher_no = si.name + """ + + extra_filters = """ + AND gl.voucher_type = 'Sales Invoice' + """ + + return extra_fields, extra_joins, extra_filters + + +def get_purchase_invoice_params(filters): + extra_fields = """ + , 'Lieferanten-Rechnungsnummer' as 'Beleginfo - Art 5' + , pi.bill_no as 'Beleginfo - Inhalt 5' + , 'Lieferanten-Rechnungsdatum' as 'Beleginfo - Art 6' + , pi.bill_date as 'Beleginfo - Inhalt 6' + , pi.due_date as 'Fälligkeit' + """ + + extra_joins = """ + LEFT JOIN `tabPurchase Invoice` pi + ON gl.voucher_no = pi.name + """ + + extra_filters = """ + AND gl.voucher_type = 'Purchase Invoice' + """ + + return extra_fields, extra_joins, extra_filters + + +def get_generic_params(filters): + # produce empty fields so all rows will have the same length + extra_fields = """ + , '' as 'Beleginfo - Art 5' + , '' as 'Beleginfo - Inhalt 5' + , '' as 'Beleginfo - Art 6' + , '' as 'Beleginfo - Inhalt 6' + , '' as 'Fälligkeit' + """ + extra_joins = "" + + if filters.get("exclude_voucher_types"): + # exclude voucher types that are queried by a dedicated method + exclude = "({})".format(', '.join("'{}'".format(key) for key in filters.get("exclude_voucher_types"))) + extra_filters = "AND gl.voucher_type NOT IN {}".format(exclude) + + # if voucher type filter is set, allow only this type + if filters.get("voucher_type"): + extra_filters += " AND gl.voucher_type = %(voucher_type)s" + + return extra_fields, extra_joins, extra_filters + + +def run_query(filters, extra_fields, extra_joins, extra_filters, as_dict=1): """ Get a list of accounting entries. @@ -171,8 +326,7 @@ def get_transactions(filters, as_dict=1): filters -- dict of filters to be passed to the sql query as_dict -- return as list of dicts [0,1] """ - filter_by_voucher = 'AND gl.voucher_type = %(voucher_type)s' if filters.get('voucher_type') else '' - gl_entries = frappe.db.sql(""" + query = """ SELECT /* either debit or credit amount; always positive */ @@ -187,6 +341,9 @@ def get_transactions(filters, as_dict=1): /* against number or, if empty, party against number */ %(temporary_against_account_number)s as 'Gegenkonto (ohne BU-Schlüssel)', + /* disable automatic VAT deduction */ + '40' as 'BU-Schlüssel', + gl.posting_date as 'Belegdatum', gl.voucher_no as 'Belegfeld 1', LEFT(gl.remarks, 60) as 'Buchungstext', @@ -199,30 +356,34 @@ def get_transactions(filters, as_dict=1): case gl.party_type when 'Customer' then 'Debitorennummer' when 'Supplier' then 'Kreditorennummer' else NULL end as 'Beleginfo - Art 4', par.debtor_creditor_number as 'Beleginfo - Inhalt 4' + {extra_fields} + FROM `tabGL Entry` gl /* Kontonummer */ - left join `tabAccount` acc - on gl.account = acc.name + LEFT JOIN `tabAccount` acc + ON gl.account = acc.name - left join `tabCustomer` cus - on gl.party_type = 'Customer' - and gl.party = cus.name + LEFT JOIN `tabParty Account` par + ON par.parent = gl.party + AND par.parenttype = gl.party_type + AND par.company = %(company)s - left join `tabSupplier` sup - on gl.party_type = 'Supplier' - and gl.party = sup.name + {extra_joins} - left join `tabParty Account` par - on par.parent = gl.party - and par.parenttype = gl.party_type - and par.company = %(company)s - - WHERE gl.company = %(company)s + WHERE gl.company = %(company)s AND DATE(gl.posting_date) >= %(from_date)s AND DATE(gl.posting_date) <= %(to_date)s - {} - ORDER BY 'Belegdatum', gl.voucher_no""".format(filter_by_voucher), filters, as_dict=as_dict) + + {extra_filters} + + ORDER BY 'Belegdatum', gl.voucher_no""".format( + extra_fields=extra_fields, + extra_joins=extra_joins, + extra_filters=extra_filters + ) + + gl_entries = frappe.db.sql(query, filters, as_dict=as_dict) return gl_entries diff --git a/erpnext/regional/report/e_invoice_summary/e_invoice_summary.py b/erpnext/regional/report/e_invoice_summary/e_invoice_summary.py index 47acf291a39..66ffceae539 100644 --- a/erpnext/regional/report/e_invoice_summary/e_invoice_summary.py +++ b/erpnext/regional/report/e_invoice_summary/e_invoice_summary.py @@ -54,53 +54,53 @@ def get_columns(): "width": 0 }, { - "fieldtype": "Link", - "fieldname": "name", + "fieldtype": "Link", + "fieldname": "name", "label": _("Sales Invoice"), "options": "Sales Invoice", "width": 140 }, - { - "fieldtype": "Data", - "fieldname": "einvoice_status", - "label": _("Status"), + { + "fieldtype": "Data", + "fieldname": "einvoice_status", + "label": _("Status"), "width": 100 }, - { + { "fieldtype": "Link", "fieldname": "customer", "options": "Customer", "label": _("Customer") }, - { + { "fieldtype": "Check", "fieldname": "is_return", "label": _("Is Return"), "width": 85 }, { - "fieldtype": "Data", - "fieldname": "ack_no", - "label": "Ack. No.", + "fieldtype": "Data", + "fieldname": "ack_no", + "label": "Ack. No.", "width": 145 }, - { - "fieldtype": "Data", - "fieldname": "ack_date", - "label": "Ack. Date", + { + "fieldtype": "Data", + "fieldname": "ack_date", + "label": "Ack. Date", "width": 165 }, { - "fieldtype": "Data", - "fieldname": "irn", + "fieldtype": "Data", + "fieldname": "irn", "label": _("IRN No."), "width": 250 }, { "fieldtype": "Currency", - "options": "Company:company:default_currency", - "fieldname": "base_grand_total", + "options": "Company:company:default_currency", + "fieldname": "base_grand_total", "label": _("Grand Total"), "width": 120 } - ] \ No newline at end of file + ] diff --git a/erpnext/regional/report/electronic_invoice_register/electronic_invoice_register.js b/erpnext/regional/report/electronic_invoice_register/electronic_invoice_register.js index 67297f757ca..d7e3ac9a5d3 100644 --- a/erpnext/regional/report/electronic_invoice_register/electronic_invoice_register.js +++ b/erpnext/regional/report/electronic_invoice_register/electronic_invoice_register.js @@ -41,7 +41,7 @@ frappe.query_reports["Electronic Invoice Register"] = { var w = window.open( frappe.urllib.get_full_url( - "/api/method/erpnext.regional.italy.utils.export_invoices?" + "/api/method/erpnext.regional.italy.utils.export_invoices?" + "filters=" + JSON.stringify(reportview.get_filter_values()) ) ); diff --git a/erpnext/regional/report/eway_bill/eway_bill.py b/erpnext/regional/report/eway_bill/eway_bill.py index 5b9896be2a1..4f777fcf7e3 100644 --- a/erpnext/regional/report/eway_bill/eway_bill.py +++ b/erpnext/regional/report/eway_bill/eway_bill.py @@ -388,4 +388,4 @@ def get_columns(): }, ] - return columns \ No newline at end of file + return columns diff --git a/erpnext/regional/report/gst_purchase_register/gst_purchase_register.js b/erpnext/regional/report/gst_purchase_register/gst_purchase_register.js index 2b4359a7493..bbcd355d13b 100644 --- a/erpnext/regional/report/gst_purchase_register/gst_purchase_register.js +++ b/erpnext/regional/report/gst_purchase_register/gst_purchase_register.js @@ -4,4 +4,4 @@ {% include "erpnext/accounts/report/purchase_register/purchase_register.js" %} -frappe.query_reports["GST Purchase Register"] = frappe.query_reports["Purchase Register"] \ No newline at end of file +frappe.query_reports["GST Purchase Register"] = frappe.query_reports["Purchase Register"] diff --git a/erpnext/regional/report/gst_purchase_register/gst_purchase_register.py b/erpnext/regional/report/gst_purchase_register/gst_purchase_register.py index 7274e0accea..12e9676b4ba 100644 --- a/erpnext/regional/report/gst_purchase_register/gst_purchase_register.py +++ b/erpnext/regional/report/gst_purchase_register/gst_purchase_register.py @@ -21,4 +21,3 @@ def execute(filters=None): 'export_type', 'ecommerce_gstin' ]) - diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py index 803128d3ae3..312f7e2564f 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.py +++ b/erpnext/regional/report/gstr_1/gstr_1.py @@ -252,9 +252,8 @@ class Gstr1Report(object): for d in items: if d.item_code not in self.invoice_items.get(d.parent, {}): - self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, - sum((i.get('taxable_value', 0) or i.get('base_net_amount', 0)) for i in items - if i.item_code == d.item_code and i.parent == d.parent)) + self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, 0.0) + self.invoice_items[d.parent][d.item_code] += d.get('taxable_value', 0) or d.get('base_net_amount', 0) item_tax_rate = {} @@ -322,7 +321,8 @@ class Gstr1Report(object): # Build itemised tax for export invoices where tax table is blank for invoice, items in iteritems(self.invoice_items): if invoice not in self.items_based_on_tax_rate and invoice not in unidentified_gst_accounts_invoice \ - and frappe.db.get_value(self.doctype, invoice, "export_type") == "Without Payment of Tax": + and self.invoices.get(invoice, {}).get('export_type') == "Without Payment of Tax" \ + and self.invoices.get(invoice, {}).get('gst_category') == "Overseas": self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault(0, items.keys()) def get_columns(self): @@ -720,7 +720,7 @@ def get_json(filters, report_name, data): fp = "%02d%s" % (getdate(filters["to_date"]).month, getdate(filters["to_date"]).year) - gst_json = {"version": "GST2.2.9", + gst_json = {"version": "GST3.0.4", "hash": "hash", "gstin": gstin, "fp": fp} res = {} @@ -948,7 +948,7 @@ def get_cdnr_reg_json(res, gstin): "ntty": invoice[0]["document_type"], "pos": "%02d" % int(invoice[0]["place_of_supply"].split('-')[0]), "rchrg": invoice[0]["reverse_charge"], - "inv_type": get_invoice_type_for_cdnr(invoice[0]) + "inv_typ": get_invoice_type_for_cdnr(invoice[0]) } inv_item["itms"] = [] diff --git a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py index 59389ce3269..1adddbdae57 100644 --- a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py +++ b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py @@ -285,5 +285,3 @@ def get_hsn_wise_json_data(filters, report_data): count +=1 return data - - diff --git a/erpnext/regional/report/india_gst_common/india_gst_common.js b/erpnext/regional/report/india_gst_common/india_gst_common.js index 49606013946..bddc32096f1 100644 --- a/erpnext/regional/report/india_gst_common/india_gst_common.js +++ b/erpnext/regional/report/india_gst_common/india_gst_common.js @@ -18,4 +18,4 @@ function fetch_gstins(report) { company_gstins.df.options = [""]; company_gstins.refresh(); } -} \ No newline at end of file +} diff --git a/erpnext/regional/report/irs_1099/irs_1099.py b/erpnext/regional/report/irs_1099/irs_1099.py index 4e57ff7ea37..f67d622fdf8 100644 --- a/erpnext/regional/report/irs_1099/irs_1099.py +++ b/erpnext/regional/report/irs_1099/irs_1099.py @@ -52,7 +52,7 @@ def execute(filters=None): AND gl.party_type = "Supplier" AND gl.company = %(company)s {conditions} - + GROUP BY gl.party diff --git a/erpnext/regional/report/professional_tax_deductions/professional_tax_deductions.js b/erpnext/regional/report/professional_tax_deductions/professional_tax_deductions.js index 29c7dbf43c6..bb75238b8c0 100644 --- a/erpnext/regional/report/professional_tax_deductions/professional_tax_deductions.js +++ b/erpnext/regional/report/professional_tax_deductions/professional_tax_deductions.js @@ -4,4 +4,4 @@ frappe.require("assets/erpnext/js/salary_slip_deductions_report_filters.js", function() { frappe.query_reports["Professional Tax Deductions"] = erpnext.salary_slip_deductions_report_filters; -}); \ No newline at end of file +}); diff --git a/erpnext/regional/report/professional_tax_deductions/professional_tax_deductions.py b/erpnext/regional/report/professional_tax_deductions/professional_tax_deductions.py index acde68a942b..54808e59e1a 100644 --- a/erpnext/regional/report/professional_tax_deductions/professional_tax_deductions.py +++ b/erpnext/regional/report/professional_tax_deductions/professional_tax_deductions.py @@ -69,4 +69,4 @@ def get_data(filters): data.append(employee) - return data \ No newline at end of file + return data diff --git a/erpnext/regional/report/provident_fund_deductions/provident_fund_deductions.js b/erpnext/regional/report/provident_fund_deductions/provident_fund_deductions.js index b4dc28d177d..a91a30796bc 100644 --- a/erpnext/regional/report/provident_fund_deductions/provident_fund_deductions.js +++ b/erpnext/regional/report/provident_fund_deductions/provident_fund_deductions.js @@ -4,4 +4,4 @@ frappe.require("assets/erpnext/js/salary_slip_deductions_report_filters.js", function() { frappe.query_reports["Provident Fund Deductions"] = erpnext.salary_slip_deductions_report_filters; -}); \ No newline at end of file +}); diff --git a/erpnext/regional/report/provident_fund_deductions/provident_fund_deductions.py b/erpnext/regional/report/provident_fund_deductions/provident_fund_deductions.py index 597072c53a1..82423f005cc 100644 --- a/erpnext/regional/report/provident_fund_deductions/provident_fund_deductions.py +++ b/erpnext/regional/report/provident_fund_deductions/provident_fund_deductions.py @@ -165,4 +165,4 @@ def get_years(): if not year_list: year_list = [getdate().year] - return "\n".join(str(year) for year in year_list) \ No newline at end of file + return "\n".join(str(year) for year in year_list) diff --git a/erpnext/regional/report/uae_vat_201/uae_vat_201.html b/erpnext/regional/report/uae_vat_201/uae_vat_201.html index d9b9968d90c..7328f3f218e 100644 --- a/erpnext/regional/report/uae_vat_201/uae_vat_201.html +++ b/erpnext/regional/report/uae_vat_201/uae_vat_201.html @@ -74,4 +74,4 @@ {% } %} - \ No newline at end of file + diff --git a/erpnext/regional/report/vat_audit_report/__init__.py b/erpnext/regional/report/vat_audit_report/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/regional/report/vat_audit_report/test_vat_audit_report.py b/erpnext/regional/report/vat_audit_report/test_vat_audit_report.py new file mode 100644 index 00000000000..dea17a66fda --- /dev/null +++ b/erpnext/regional/report/vat_audit_report/test_vat_audit_report.py @@ -0,0 +1,193 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from unittest import TestCase +from frappe.utils import today + +from erpnext.accounts.doctype.account.test_account import create_account +from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice +from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice + +from erpnext.regional.report.vat_audit_report.vat_audit_report import execute + +class TestVATAuditReport(TestCase): + def setUp(self): + frappe.set_user("Administrator") + make_company("_Test Company SA VAT", "_TCSV") + + create_account(account_name="VAT - 0%", account_type="Tax", + parent_account="Duties and Taxes - _TCSV", company="_Test Company SA VAT") + create_account(account_name="VAT - 15%", account_type="Tax", + parent_account="Duties and Taxes - _TCSV", company="_Test Company SA VAT") + set_sa_vat_accounts() + + make_item("_Test SA VAT Item") + make_item("_Test SA VAT Zero Rated Item", properties = {"is_zero_rated": 1}) + + make_customer() + make_supplier() + + make_sales_invoices() + create_purchase_invoices() + + def tearDown(self): + frappe.db.sql("delete from `tabSales Invoice` where company='_Test Company SA VAT'") + frappe.db.sql("delete from `tabPurchase Invoice` where company='_Test Company SA VAT'") + + def test_vat_audit_report(self): + filters = { + "company": "_Test Company SA VAT", + "from_date": today(), + "to_date": today() + } + columns, data = execute(filters) + total_tax_amount = 0 + total_row_tax = 0 + for row in data: + keys = row.keys() + # skips total row tax_amount in if.. and skips section header in elif.. + if 'voucher_no' in keys: + total_tax_amount = total_tax_amount + row['tax_amount'] + elif 'tax_amount' in keys: + total_row_tax = total_row_tax + row['tax_amount'] + + self.assertEqual(total_tax_amount, total_row_tax) + +def make_company(company_name, abbr): + if not frappe.db.exists("Company", company_name): + company = frappe.get_doc({ + "doctype": "Company", + "company_name": company_name, + "abbr": abbr, + "default_currency": "ZAR", + "country": "South Africa", + "create_chart_of_accounts_based_on": "Standard Template" + }) + company.insert() + else: + company = frappe.get_doc("Company", company_name) + + company.create_default_warehouses() + + if not frappe.db.get_value("Cost Center", {"is_group": 0, "company": company.name}): + company.create_default_cost_center() + + company.save() + + return company + +def set_sa_vat_accounts(): + if not frappe.db.exists("South Africa VAT Settings", "_Test Company SA VAT"): + vat_accounts = frappe.get_all( + "Account", + fields=["name"], + filters = { + "company": "_Test Company SA VAT", + "is_group": 0, + "account_type": "Tax" + } + ) + + sa_vat_accounts = [] + for account in vat_accounts: + sa_vat_accounts.append({ + "doctype": "South Africa VAT Account", + "account": account.name + }) + + frappe.get_doc({ + "company": "_Test Company SA VAT", + "vat_accounts": sa_vat_accounts, + "doctype": "South Africa VAT Settings", + }).insert() + +def make_customer(): + if not frappe.db.exists("Customer", "_Test SA Customer"): + frappe.get_doc({ + "doctype": "Customer", + "customer_name": "_Test SA Customer", + "customer_type": "Company", + }).insert() + +def make_supplier(): + if not frappe.db.exists("Supplier", "_Test SA Supplier"): + frappe.get_doc({ + "doctype": "Supplier", + "supplier_name": "_Test SA Supplier", + "supplier_type": "Company", + "supplier_group":"All Supplier Groups" + }).insert() + +def make_item(item_code, properties=None): + if not frappe.db.exists("Item", item_code): + item = frappe.get_doc({ + "doctype": "Item", + "item_code": item_code, + "item_name": item_code, + "description": item_code, + "item_group": "Products" + }) + + if properties: + item.update(properties) + + item.insert() + +def make_sales_invoices(): + def make_sales_invoices_wrapper(item, rate, tax_account, tax_rate, tax=True): + si = create_sales_invoice( + company="_Test Company SA VAT", + customer = "_Test SA Customer", + currency = "ZAR", + item=item, + rate=rate, + warehouse = "Finished Goods - _TCSV", + debit_to = "Debtors - _TCSV", + income_account = "Sales - _TCSV", + expense_account = "Cost of Goods Sold - _TCSV", + cost_center = "Main - _TCSV", + do_not_save=1 + ) + if tax: + si.append("taxes", { + "charge_type": "On Net Total", + "account_head": tax_account, + "cost_center": "Main - _TCSV", + "description": "VAT 15% @ 15.0", + "rate": tax_rate + }) + + si.submit() + + test_item = "_Test SA VAT Item" + test_zero_rated_item = "_Test SA VAT Zero Rated Item" + + make_sales_invoices_wrapper(test_item, 100.0, "VAT - 15% - _TCSV", 15.0) + make_sales_invoices_wrapper(test_zero_rated_item, 100.0, "VAT - 0% - _TCSV", 0.0) + +def create_purchase_invoices(): + pi = make_purchase_invoice( + company = "_Test Company SA VAT", + supplier = "_Test SA Supplier", + supplier_warehouse = "Finished Goods - _TCSV", + warehouse = "Finished Goods - _TCSV", + currency = "ZAR", + cost_center = "Main - _TCSV", + expense_account = "Cost of Goods Sold - _TCSV", + item = "_Test SA VAT Item", + qty = 1, + rate = 100, + uom = "Nos", + do_not_save = 1 + ) + pi.append("taxes", { + "charge_type": "On Net Total", + "account_head": "VAT - 15% - _TCSV", + "cost_center": "Main - _TCSV", + "description": "VAT 15% @ 15.0", + "rate": 15.0 + }) + + pi.submit() diff --git a/erpnext/regional/report/vat_audit_report/vat_audit_report.js b/erpnext/regional/report/vat_audit_report/vat_audit_report.js new file mode 100644 index 00000000000..39ef9b563ac --- /dev/null +++ b/erpnext/regional/report/vat_audit_report/vat_audit_report.js @@ -0,0 +1,31 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["VAT Audit Report"] = { + "filters": [ + { + "fieldname": "company", + "label": __("Company"), + "fieldtype": "Link", + "options": "Company", + "reqd": 1, + "default": frappe.defaults.get_user_default("Company") + }, + { + "fieldname": "from_date", + "label": __("From Date"), + "fieldtype": "Date", + "reqd": 1, + "default": frappe.datetime.add_months(frappe.datetime.get_today(), -2), + "width": "80" + }, + { + "fieldname": "to_date", + "label": __("To Date"), + "fieldtype": "Date", + "reqd": 1, + "default": frappe.datetime.get_today() + } + ] +}; diff --git a/erpnext/regional/report/vat_audit_report/vat_audit_report.json b/erpnext/regional/report/vat_audit_report/vat_audit_report.json new file mode 100644 index 00000000000..a8be7bf64c0 --- /dev/null +++ b/erpnext/regional/report/vat_audit_report/vat_audit_report.json @@ -0,0 +1,22 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2021-07-09 11:07:43.473518", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "modified": "2021-07-09 11:07:43.473518", + "modified_by": "Administrator", + "module": "Regional", + "name": "VAT Audit Report", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "GL Entry", + "report_name": "VAT Audit Report", + "report_type": "Script Report", + "roles": [] +} \ No newline at end of file diff --git a/erpnext/regional/report/vat_audit_report/vat_audit_report.py b/erpnext/regional/report/vat_audit_report/vat_audit_report.py new file mode 100644 index 00000000000..ebf297113d7 --- /dev/null +++ b/erpnext/regional/report/vat_audit_report/vat_audit_report.py @@ -0,0 +1,270 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +import json +from frappe import _ +from frappe.utils import formatdate, get_link_to_form + +def execute(filters=None): + return VATAuditReport(filters).run() + +class VATAuditReport(object): + + def __init__(self, filters=None): + self.filters = frappe._dict(filters or {}) + self.columns = [] + self.data = [] + self.doctypes = ["Purchase Invoice", "Sales Invoice"] + + def run(self): + self.get_sa_vat_accounts() + self.get_columns() + for doctype in self.doctypes: + self.select_columns = """ + name as voucher_no, + posting_date, remarks""" + columns = ", supplier as party, credit_to as account" if doctype=="Purchase Invoice" \ + else ", customer as party, debit_to as account" + self.select_columns += columns + + self.get_invoice_data(doctype) + + if self.invoices: + self.get_invoice_items(doctype) + self.get_items_based_on_tax_rate(doctype) + self.get_data(doctype) + + return self.columns, self.data + + def get_sa_vat_accounts(self): + self.sa_vat_accounts = frappe.get_list("South Africa VAT Account", + filters = {"parent": self.filters.company}, pluck="account") + if not self.sa_vat_accounts and not frappe.flags.in_test and not frappe.flags.in_migrate: + link_to_settings = get_link_to_form("South Africa VAT Settings", "", label="South Africa VAT Settings") + frappe.throw(_("Please set VAT Accounts in {0}").format(link_to_settings)) + + def get_invoice_data(self, doctype): + conditions = self.get_conditions() + self.invoices = frappe._dict() + + invoice_data = frappe.db.sql(""" + SELECT + {select_columns} + FROM + `tab{doctype}` + WHERE + docstatus = 1 {where_conditions} + and is_opening = "No" + ORDER BY + posting_date DESC + """.format(select_columns=self.select_columns, doctype=doctype, + where_conditions=conditions), self.filters, as_dict=1) + + for d in invoice_data: + self.invoices.setdefault(d.voucher_no, d) + + def get_invoice_items(self, doctype): + self.invoice_items = frappe._dict() + + items = frappe.db.sql(""" + SELECT + item_code, parent, base_net_amount, is_zero_rated + FROM + `tab%s Item` + WHERE + parent in (%s) + """ % (doctype, ", ".join(["%s"]*len(self.invoices))), tuple(self.invoices), as_dict=1) + for d in items: + if d.item_code not in self.invoice_items.get(d.parent, {}): + self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, { + 'net_amount': 0.0}) + self.invoice_items[d.parent][d.item_code]['net_amount'] += d.get('base_net_amount', 0) + self.invoice_items[d.parent][d.item_code]['is_zero_rated'] = d.is_zero_rated + + def get_items_based_on_tax_rate(self, doctype): + self.items_based_on_tax_rate = frappe._dict() + self.item_tax_rate = frappe._dict() + self.tax_doctype = "Purchase Taxes and Charges" if doctype=="Purchase Invoice" \ + else "Sales Taxes and Charges" + + self.tax_details = frappe.db.sql(""" + SELECT + parent, account_head, item_wise_tax_detail, base_tax_amount_after_discount_amount + FROM + `tab%s` + WHERE + parenttype = %s and docstatus = 1 + and parent in (%s) + ORDER BY + account_head + """ % (self.tax_doctype, "%s", ", ".join(["%s"]*len(self.invoices.keys()))), + tuple([doctype] + list(self.invoices.keys()))) + + for parent, account, item_wise_tax_detail, tax_amount in self.tax_details: + if item_wise_tax_detail: + try: + if account in self.sa_vat_accounts: + item_wise_tax_detail = json.loads(item_wise_tax_detail) + else: + continue + for item_code, taxes in item_wise_tax_detail.items(): + is_zero_rated = self.invoice_items.get(parent).get(item_code).get("is_zero_rated") + #to skip items with non-zero tax rate in multiple rows + if taxes[0] == 0 and not is_zero_rated: + continue + tax_rate, item_amount_map = self.get_item_amount_map(parent, item_code, taxes) + + if tax_rate is not None: + rate_based_dict = self.items_based_on_tax_rate.setdefault(parent, {}) \ + .setdefault(tax_rate, []) + if item_code not in rate_based_dict: + rate_based_dict.append(item_code) + except ValueError: + continue + + def get_item_amount_map(self, parent, item_code, taxes): + net_amount = self.invoice_items.get(parent).get(item_code).get("net_amount") + tax_rate = taxes[0] + tax_amount = taxes[1] + gross_amount = net_amount + tax_amount + item_amount_map = self.item_tax_rate.setdefault(parent, {}) \ + .setdefault(item_code, []) + amount_dict = { + "tax_rate": tax_rate, + "gross_amount": gross_amount, + "tax_amount": tax_amount, + "net_amount": net_amount + } + item_amount_map.append(amount_dict) + + return tax_rate, item_amount_map + + def get_conditions(self): + conditions = "" + for opts in (("company", " and company=%(company)s"), + ("from_date", " and posting_date>=%(from_date)s"), + ("to_date", " and posting_date<=%(to_date)s")): + if self.filters.get(opts[0]): + conditions += opts[1] + + return conditions + + def get_data(self, doctype): + consolidated_data = self.get_consolidated_data(doctype) + section_name = _("Purchases") if doctype == "Purchase Invoice" else _("Sales") + + for rate, section in consolidated_data.items(): + rate = int(rate) + label = frappe.bold(section_name + "- " + "Rate" + " " + str(rate) + "%") + section_head = {"posting_date": label} + total_gross = total_tax = total_net = 0 + self.data.append(section_head) + for row in section.get("data"): + self.data.append(row) + total_gross += row["gross_amount"] + total_tax += row["tax_amount"] + total_net += row["net_amount"] + + total = { + "posting_date": frappe.bold(_("Total")), + "gross_amount": total_gross, + "tax_amount": total_tax, + "net_amount": total_net, + "bold":1 + } + self.data.append(total) + self.data.append({}) + + def get_consolidated_data(self, doctype): + consolidated_data_map={} + for inv, inv_data in self.invoices.items(): + if self.items_based_on_tax_rate.get(inv): + for rate, items in self.items_based_on_tax_rate.get(inv).items(): + consolidated_data_map.setdefault(rate, {"data": []}) + for item in items: + row = {} + item_details = self.item_tax_rate.get(inv).get(item) + row["account"] = inv_data.get("account") + row["posting_date"] = formatdate(inv_data.get("posting_date"), "dd-mm-yyyy") + row["voucher_type"] = doctype + row["voucher_no"] = inv + row["party_type"] = "Customer" if doctype == "Sales Invoice" else "Supplier" + row["party"] = inv_data.get("party") + row["remarks"] = inv_data.get("remarks") + row["gross_amount"]= item_details[0].get("gross_amount") + row["tax_amount"]= item_details[0].get("tax_amount") + row["net_amount"]= item_details[0].get("net_amount") + consolidated_data_map[rate]["data"].append(row) + + return consolidated_data_map + + def get_columns(self): + self.columns = [ + { + "fieldname": "posting_date", + "label": "Posting Date", + "fieldtype": "Data", + "width": 200 + }, + { + "fieldname": "account", + "label": "Account", + "fieldtype": "Link", + "options": "Account", + "width": 150 + }, + { + "fieldname": "voucher_type", + "label": "Voucher Type", + "fieldtype": "Data", + "width": 140, + "hidden": 1 + }, + { + "fieldname": "voucher_no", + "label": "Reference", + "fieldtype": "Dynamic Link", + "options": "voucher_type", + "width": 150 + }, + { + "fieldname": "party_type", + "label": "Party Type", + "fieldtype": "Data", + "width": 140, + "hidden": 1 + }, + { + "fieldname": "party", + "label": "Party", + "fieldtype": "Dynamic Link", + "options": "party_type", + "width": 150 + }, + { + "fieldname": "remarks", + "label": "Details", + "fieldtype": "Data", + "width": 150 + }, + { + "fieldname": "net_amount", + "label": "Net Amount", + "fieldtype": "Currency", + "width": 130 + }, + { + "fieldname": "tax_amount", + "label": "Tax Amount", + "fieldtype": "Currency", + "width": 130 + }, + { + "fieldname": "gross_amount", + "label": "Gross Amount", + "fieldtype": "Currency", + "width": 130 + }, + ] diff --git a/erpnext/regional/south_africa/__init__.py b/erpnext/regional/south_africa/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/regional/south_africa/setup.py b/erpnext/regional/south_africa/setup.py new file mode 100644 index 00000000000..4657ff833dd --- /dev/null +++ b/erpnext/regional/south_africa/setup.py @@ -0,0 +1,50 @@ +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals + +import frappe +from frappe.custom.doctype.custom_field.custom_field import create_custom_fields +from frappe.permissions import add_permission, update_permission_property + +def setup(company=None, patch=True): + make_custom_fields() + add_permissions() + +def make_custom_fields(update=True): + is_zero_rated = dict(fieldname='is_zero_rated', label='Is Zero Rated', + fieldtype='Check', fetch_from='item_code.is_zero_rated', + insert_after='description', print_hide=1) + custom_fields = { + 'Item': [ + dict(fieldname='is_zero_rated', label='Is Zero Rated', + fieldtype='Check', insert_after='item_group', + print_hide=1) + ], + 'Sales Invoice Item': is_zero_rated, + 'Purchase Invoice Item': is_zero_rated + } + + create_custom_fields(custom_fields, update=update) + +def add_permissions(): + """Add Permissions for South Africa VAT Settings and South Africa VAT Account + and VAT Audit Report""" + for doctype in ('South Africa VAT Settings', 'South Africa VAT Account'): + add_permission(doctype, 'All', 0) + for role in ('Accounts Manager', 'Accounts User', 'System Manager'): + add_permission(doctype, role, 0) + update_permission_property(doctype, role, 0, 'write', 1) + update_permission_property(doctype, role, 0, 'create', 1) + + + if not frappe.db.get_value('Custom Role', dict(report="VAT Audit Report")): + frappe.get_doc(dict( + doctype='Custom Role', + report="VAT Audit Report", + roles= [ + dict(role='Accounts User'), + dict(role='Accounts Manager'), + dict(role='Auditor') + ] + )).insert() \ No newline at end of file diff --git a/erpnext/regional/turkey/setup.py b/erpnext/regional/turkey/setup.py index ebf3b2bee1f..2396aab91f5 100644 --- a/erpnext/regional/turkey/setup.py +++ b/erpnext/regional/turkey/setup.py @@ -1,4 +1,4 @@ from __future__ import unicode_literals def setup(company=None, patch=True): - pass \ No newline at end of file + pass diff --git a/erpnext/restaurant/doctype/restaurant/restaurant_dashboard.py b/erpnext/restaurant/doctype/restaurant/restaurant_dashboard.py index ec62ba22b4d..adce5c73352 100644 --- a/erpnext/restaurant/doctype/restaurant/restaurant_dashboard.py +++ b/erpnext/restaurant/doctype/restaurant/restaurant_dashboard.py @@ -14,4 +14,4 @@ def get_data(): 'items': ['Restaurant Reservation', 'Sales Invoice'] } ] - } \ No newline at end of file + } diff --git a/erpnext/restaurant/doctype/restaurant/test_restaurant.js b/erpnext/restaurant/doctype/restaurant/test_restaurant.js index 26de5d04aaa..8fe4e7b84d5 100644 --- a/erpnext/restaurant/doctype/restaurant/test_restaurant.js +++ b/erpnext/restaurant/doctype/restaurant/test_restaurant.js @@ -18,7 +18,7 @@ QUnit.test("test: Restaurant", function (assert) { frappe.run_serially([ // insert a new Restaurant - () => frappe.tests.setup_doctype('Customer', customer), + () => frappe.tests.setup_doctype('Customer', customer), () => { return frappe.tests.make('Restaurant', [ // values to be set diff --git a/erpnext/restaurant/doctype/restaurant_menu/restaurant_menu.py b/erpnext/restaurant/doctype/restaurant_menu/restaurant_menu.py index 83020b6cca8..952c46769b7 100644 --- a/erpnext/restaurant/doctype/restaurant_menu/restaurant_menu.py +++ b/erpnext/restaurant/doctype/restaurant_menu/restaurant_menu.py @@ -57,5 +57,3 @@ class RestaurantMenu(Document): price_list.save() return price_list - - diff --git a/erpnext/selling/doctype/campaign/test_campaign.py b/erpnext/selling/doctype/campaign/test_campaign.py index 4d062ff84c6..8c6617fe79a 100644 --- a/erpnext/selling/doctype/campaign/test_campaign.py +++ b/erpnext/selling/doctype/campaign/test_campaign.py @@ -4,4 +4,4 @@ from __future__ import unicode_literals import frappe -test_records = frappe.get_test_records('Campaign') \ No newline at end of file +test_records = frappe.get_test_records('Campaign') diff --git a/erpnext/selling/doctype/customer/customer.js b/erpnext/selling/doctype/customer/customer.js index 825b170a901..8416901b7cb 100644 --- a/erpnext/selling/doctype/customer/customer.js +++ b/erpnext/selling/doctype/customer/customer.js @@ -111,7 +111,6 @@ frappe.ui.form.on("Customer", { } frappe.dynamic_link = {doc: frm.doc, fieldname: 'name', doctype: 'Customer'} - frm.toggle_display(['address_html','contact_html'], !frm.doc.__islocal); if(!frm.doc.__islocal) { frappe.contacts.render_address_and_contact(frm); @@ -130,6 +129,10 @@ frappe.ui.form.on("Customer", { erpnext.utils.make_pricing_rule(frm.doc.doctype, frm.doc.name); }, __('Create')); + frm.add_custom_button(__('Get Customer Group Details'), function () { + frm.trigger("get_customer_group_details"); + }, __('Actions')); + // indicator erpnext.utils.set_party_dashboard_indicators(frm); @@ -145,4 +148,15 @@ frappe.ui.form.on("Customer", { if(frm.doc.lead_name) frappe.model.clear_doc("Lead", frm.doc.lead_name); }, + + get_customer_group_details: function(frm) { + frappe.call({ + method: "get_customer_group_details", + doc: frm.doc, + callback: function() { + frm.refresh(); + } + }); + + } }); \ No newline at end of file diff --git a/erpnext/selling/doctype/customer/customer.json b/erpnext/selling/doctype/customer/customer.json index cd94ee101af..2acc64cb433 100644 --- a/erpnext/selling/doctype/customer/customer.json +++ b/erpnext/selling/doctype/customer/customer.json @@ -20,6 +20,7 @@ "tax_withholding_category", "default_bank_account", "lead_name", + "opportunity_name", "image", "column_break0", "account_manager", @@ -267,6 +268,7 @@ "options": "fa fa-map-marker" }, { + "depends_on": "eval: !doc.__islocal", "fieldname": "address_html", "fieldtype": "HTML", "label": "Address HTML", @@ -283,6 +285,7 @@ "width": "50%" }, { + "depends_on": "eval: !doc.__islocal", "fieldname": "contact_html", "fieldtype": "HTML", "label": "Contact HTML", @@ -493,6 +496,14 @@ "fieldtype": "Link", "label": "Tax Withholding Category", "options": "Tax Withholding Category" + }, + { + "fieldname": "opportunity_name", + "fieldtype": "Link", + "label": "From Opportunity", + "no_copy": 1, + "options": "Opportunity", + "print_hide": 1 } ], "icon": "fa fa-user", @@ -500,7 +511,7 @@ "image_field": "image", "index_web_pages_for_search": 1, "links": [], - "modified": "2021-01-28 12:54:57.258959", + "modified": "2021-08-25 18:56:09.929905", "modified_by": "Administrator", "module": "Selling", "name": "Customer", diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 818888c0c12..27e9f08e8d6 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -78,6 +78,29 @@ class Customer(TransactionBase): if sum(member.allocated_percentage or 0 for member in self.sales_team) != 100: frappe.throw(_("Total contribution percentage should be equal to 100")) + @frappe.whitelist() + def get_customer_group_details(self): + doc = frappe.get_doc('Customer Group', self.customer_group) + self.accounts = self.credit_limits = [] + self.payment_terms = self.default_price_list = "" + + tables = [["accounts", "account"], ["credit_limits", "credit_limit"]] + fields = ["payment_terms", "default_price_list"] + + for row in tables: + table, field = row[0], row[1] + if not doc.get(table): continue + + for entry in doc.get(table): + child = self.append(table) + child.update({"company": entry.company, field: entry.get(field)}) + + for field in fields: + if not doc.get(field): continue + self.update({field: doc.get(field)}) + + self.save() + def check_customer_group_change(self): frappe.flags.customer_group_changed = False @@ -127,16 +150,20 @@ class Customer(TransactionBase): self.db_set('email_id', self.email_id) def create_primary_address(self): + from frappe.contacts.doctype.address.address import get_address_display + if self.flags.is_new_doc and self.get('address_line1'): - make_address(self) + address = make_address(self) + address_display = get_address_display(address.name) + + self.db_set("customer_primary_address", address.name) + self.db_set("primary_address", address_display) def update_lead_status(self): '''If Customer created from Lead, update lead status to "Converted" update Customer link in Quotation, Opportunity''' if self.lead_name: - lead = frappe.get_doc('Lead', self.lead_name) - lead.status = 'Converted' - lead.save() + frappe.db.set_value("Lead", self.lead_name, "status", "Converted") def create_lead_address_contact(self): if self.lead_name: @@ -225,9 +252,15 @@ class Customer(TransactionBase): def on_trash(self): if self.customer_primary_contact: - frappe.db.sql("""update `tabCustomer` - set customer_primary_contact=null, mobile_no=null, email_id=null - where name=%s""", self.name) + frappe.db.sql(""" + UPDATE `tabCustomer` + SET + customer_primary_contact=null, + customer_primary_address=null, + mobile_no=null, + email_id=null, + primary_address=null + WHERE name=%(name)s""", {"name": self.name}) delete_contact_and_address('Customer', self.name) if self.lead_name: diff --git a/erpnext/selling/doctype/customer/regional/india.js b/erpnext/selling/doctype/customer/regional/india.js index edb83838b61..cad9a27ace6 100644 --- a/erpnext/selling/doctype/customer/regional/india.js +++ b/erpnext/selling/doctype/customer/regional/india.js @@ -1,3 +1,3 @@ {% include "erpnext/regional/india/party.js" %} -erpnext.setup_gst_reminder_button('Customer') \ No newline at end of file +erpnext.setup_gst_reminder_button('Customer') diff --git a/erpnext/selling/doctype/customer/test_customer.js b/erpnext/selling/doctype/customer/test_customer.js deleted file mode 100644 index 65b81af32c1..00000000000 --- a/erpnext/selling/doctype/customer/test_customer.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: Customer", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Customer - () => frappe.tests.make('Customer', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/selling/doctype/customer/test_customer.py b/erpnext/selling/doctype/customer/test_customer.py index 7761aa70fb2..5b337313d3d 100644 --- a/erpnext/selling/doctype/customer/test_customer.py +++ b/erpnext/selling/doctype/customer/test_customer.py @@ -27,6 +27,42 @@ class TestCustomer(unittest.TestCase): def tearDown(self): set_credit_limit('_Test Customer', '_Test Company', 0) + def test_get_customer_group_details(self): + doc = frappe.new_doc("Customer Group") + doc.customer_group_name = "_Testing Customer Group" + doc.payment_terms = "_Test Payment Term Template 3" + doc.accounts = [] + doc.default_price_list = "Standard Buying" + doc.credit_limits = [] + test_account_details = { + "company": "_Test Company", + "account": "Creditors - _TC", + } + test_credit_limits = { + "company": "_Test Company", + "credit_limit": 350000 + } + doc.append("accounts", test_account_details) + doc.append("credit_limits", test_credit_limits) + doc.insert() + + c_doc = frappe.new_doc("Customer") + c_doc.customer_name = "Testing Customer" + c_doc.customer_group = "_Testing Customer Group" + c_doc.payment_terms = c_doc.default_price_list = "" + c_doc.accounts = c_doc.credit_limits= [] + c_doc.insert() + c_doc.get_customer_group_details() + self.assertEqual(c_doc.payment_terms, "_Test Payment Term Template 3") + + self.assertEqual(c_doc.accounts[0].company, "_Test Company") + self.assertEqual(c_doc.accounts[0].account, "Creditors - _TC") + + self.assertEqual(c_doc.credit_limits[0].company, "_Test Company") + self.assertEqual(c_doc.credit_limits[0].credit_limit, 350000) + c_doc.delete() + doc.delete() + def test_party_details(self): from erpnext.accounts.party import get_party_details @@ -316,3 +352,26 @@ def set_credit_limit(customer, company, credit_limit): 'credit_limit': credit_limit }) customer.credit_limits[-1].db_insert() + +def create_internal_customer(customer_name, represents_company, allowed_to_interact_with): + if not frappe.db.exists("Customer", customer_name): + customer = frappe.get_doc({ + "doctype": "Customer", + "customer_group": "_Test Customer Group", + "customer_name": customer_name, + "customer_type": "Individual", + "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 \ No newline at end of file diff --git a/erpnext/selling/doctype/industry_type/industry_type.js b/erpnext/selling/doctype/industry_type/industry_type.js index 3878a791db6..3680906057f 100644 --- a/erpnext/selling/doctype/industry_type/industry_type.js +++ b/erpnext/selling/doctype/industry_type/industry_type.js @@ -1,13 +1,13 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt - + //--------- ONLOAD ------------- cur_frm.cscript.onload = function(doc, cdt, cdn) { - + } cur_frm.cscript.refresh = function(doc, cdt, cdn) { - -} \ No newline at end of file + +} diff --git a/erpnext/selling/doctype/industry_type/industry_type.py b/erpnext/selling/doctype/industry_type/industry_type.py index 65b17e976a6..7a30d6524a0 100644 --- a/erpnext/selling/doctype/industry_type/industry_type.py +++ b/erpnext/selling/doctype/industry_type/industry_type.py @@ -7,4 +7,4 @@ import frappe from frappe.model.document import Document class IndustryType(Document): - pass \ No newline at end of file + pass diff --git a/erpnext/selling/doctype/industry_type/test_industry_type.py b/erpnext/selling/doctype/industry_type/test_industry_type.py index 1246a241c7e..ebc6366155e 100644 --- a/erpnext/selling/doctype/industry_type/test_industry_type.py +++ b/erpnext/selling/doctype/industry_type/test_industry_type.py @@ -4,4 +4,4 @@ from __future__ import unicode_literals import frappe -test_records = frappe.get_test_records('Industry Type') \ No newline at end of file +test_records = frappe.get_test_records('Industry Type') diff --git a/erpnext/selling/doctype/installation_note/installation_note.js b/erpnext/selling/doctype/installation_note/installation_note.js index 7fd0877d11a..12e258c5384 100644 --- a/erpnext/selling/doctype/installation_note/installation_note.js +++ b/erpnext/selling/doctype/installation_note/installation_note.js @@ -57,4 +57,4 @@ erpnext.selling.InstallationNote = frappe.ui.form.Controller.extend({ }, }); -$.extend(cur_frm.cscript, new erpnext.selling.InstallationNote({frm: cur_frm})); \ No newline at end of file +$.extend(cur_frm.cscript, new erpnext.selling.InstallationNote({frm: cur_frm})); diff --git a/erpnext/selling/doctype/installation_note_item/installation_note_item.py b/erpnext/selling/doctype/installation_note_item/installation_note_item.py index 681b8171e2c..7e1205231bb 100644 --- a/erpnext/selling/doctype/installation_note_item/installation_note_item.py +++ b/erpnext/selling/doctype/installation_note_item/installation_note_item.py @@ -7,4 +7,4 @@ import frappe from frappe.model.document import Document class InstallationNoteItem(Document): - pass \ No newline at end of file + pass diff --git a/erpnext/selling/doctype/product_bundle/test_product_bundle.js b/erpnext/selling/doctype/product_bundle/test_product_bundle.js index ba5ba0dc3ba..0dc90ec2114 100644 --- a/erpnext/selling/doctype/product_bundle/test_product_bundle.js +++ b/erpnext/selling/doctype/product_bundle/test_product_bundle.js @@ -33,4 +33,3 @@ QUnit.test("test sales order", function(assert) { () => done() ]); }); - diff --git a/erpnext/selling/doctype/quotation/quotation_dashboard.py b/erpnext/selling/doctype/quotation/quotation_dashboard.py index f1ac951ef94..d1bb788937b 100644 --- a/erpnext/selling/doctype/quotation/quotation_dashboard.py +++ b/erpnext/selling/doctype/quotation/quotation_dashboard.py @@ -17,4 +17,4 @@ def get_data(): 'items': ['Auto Repeat'] }, ] - } \ No newline at end of file + } diff --git a/erpnext/selling/doctype/quotation/tests/test_quotation_with_discount_on_grand_total.js b/erpnext/selling/doctype/quotation/tests/test_quotation_with_discount_on_grand_total.js index aeb5d1b9eb7..b59bb0510e8 100644 --- a/erpnext/selling/doctype/quotation/tests/test_quotation_with_discount_on_grand_total.js +++ b/erpnext/selling/doctype/quotation/tests/test_quotation_with_discount_on_grand_total.js @@ -41,4 +41,3 @@ QUnit.test("test quotation with additional discount in grand total", function(as () => done() ]); }); - diff --git a/erpnext/selling/doctype/quotation/tests/test_quotation_with_item_wise_discount.js b/erpnext/selling/doctype/quotation/tests/test_quotation_with_item_wise_discount.js index e7349e3201c..f5172fbae2e 100644 --- a/erpnext/selling/doctype/quotation/tests/test_quotation_with_item_wise_discount.js +++ b/erpnext/selling/doctype/quotation/tests/test_quotation_with_item_wise_discount.js @@ -35,4 +35,3 @@ QUnit.test("test quotation with item wise discount", function(assert) { () => done() ]); }); - diff --git a/erpnext/selling/doctype/quotation/tests/test_quotation_with_margin.js b/erpnext/selling/doctype/quotation/tests/test_quotation_with_margin.js index 5b4224dfe9a..0d340997ad9 100644 --- a/erpnext/selling/doctype/quotation/tests/test_quotation_with_margin.js +++ b/erpnext/selling/doctype/quotation/tests/test_quotation_with_margin.js @@ -33,4 +33,3 @@ QUnit.test("test quotation with margin", function(assert) { () => done() ]); }); - diff --git a/erpnext/selling/doctype/quotation/tests/test_quotation_with_multi_uom.js b/erpnext/selling/doctype/quotation/tests/test_quotation_with_multi_uom.js index 50b8a8396d7..84be56f4605 100644 --- a/erpnext/selling/doctype/quotation/tests/test_quotation_with_multi_uom.js +++ b/erpnext/selling/doctype/quotation/tests/test_quotation_with_multi_uom.js @@ -36,4 +36,3 @@ QUnit.test("test quotation with multi uom", function(assert) { () => done() ]); }); - diff --git a/erpnext/selling/doctype/quotation/tests/test_quotation_with_taxes_and_charges.js b/erpnext/selling/doctype/quotation/tests/test_quotation_with_taxes_and_charges.js index ac7ed65ec02..5e21f817573 100644 --- a/erpnext/selling/doctype/quotation/tests/test_quotation_with_taxes_and_charges.js +++ b/erpnext/selling/doctype/quotation/tests/test_quotation_with_taxes_and_charges.js @@ -38,4 +38,3 @@ QUnit.test("test quotation with taxes and charges", function(assert) { () => done() ]); }); - diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json index 762b6f1d6c9..38ea5c81d49 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.json +++ b/erpnext/selling/doctype/sales_order/sales_order.json @@ -38,6 +38,8 @@ "col_break46", "shipping_address_name", "shipping_address", + "dispatch_address_name", + "dispatch_address", "customer_group", "territory", "currency_and_price_list", @@ -569,7 +571,8 @@ "fieldtype": "Data", "hide_days": 1, "hide_seconds": 1, - "label": "Scan Barcode" + "label": "Scan Barcode", + "options": "Barcode" }, { "allow_bulk_edit": 1, @@ -1486,13 +1489,29 @@ "fieldname": "disable_rounded_total", "fieldtype": "Check", "label": "Disable Rounded Total" + }, + { + "allow_on_submit": 1, + "fieldname": "dispatch_address_name", + "fieldtype": "Link", + "label": "Dispatch Address Name", + "options": "Address", + "print_hide": 1 + }, + { + "allow_on_submit": 1, + "depends_on": "dispatch_address_name", + "fieldname": "dispatch_address", + "fieldtype": "Small Text", + "label": "Dispatch Address", + "read_only": 1 } ], "icon": "fa fa-file-text", "idx": 105, "is_submittable": 1, "links": [], - "modified": "2021-04-15 23:55:13.439068", + "modified": "2021-08-17 20:15:26.531553", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order", diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 41f57a34d3d..5d44582aab4 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -670,6 +670,7 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False): "party_account_currency": "party_account_currency", "payment_terms_template": "payment_terms_template" }, + "field_no_map": ["payment_terms_template"], "validation": { "docstatus": ["=", 1] } @@ -693,6 +694,10 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False): } }, target_doc, postprocess, ignore_permissions=ignore_permissions) + automatically_fetch_payment_terms = cint(frappe.db.get_single_value('Accounts Settings', 'automatically_fetch_payment_terms')) + if automatically_fetch_payment_terms: + doclist.set_payment_schedule() + return doclist @frappe.whitelist() @@ -713,8 +718,7 @@ def make_maintenance_schedule(source_name, target_doc=None): "doctype": "Maintenance Schedule Item", "field_map": { "parent": "sales_order" - }, - "add_if_empty": True + } } }, target_doc) @@ -740,8 +744,7 @@ def make_maintenance_visit(source_name, target_doc=None): "field_map": { "parent": "prevdoc_docname", "parenttype": "prevdoc_doctype" - }, - "add_if_empty": True + } } }, target_doc) diff --git a/erpnext/selling/doctype/sales_order/sales_order_dashboard.py b/erpnext/selling/doctype/sales_order/sales_order_dashboard.py index 05a760de273..2a71c27009f 100644 --- a/erpnext/selling/doctype/sales_order/sales_order_dashboard.py +++ b/erpnext/selling/doctype/sales_order/sales_order_dashboard.py @@ -41,4 +41,4 @@ def get_data(): 'items': ['Payment Entry', 'Payment Request', 'Journal Entry'] }, ] - } \ No newline at end of file + } diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.js b/erpnext/selling/doctype/sales_order/test_sales_order.js deleted file mode 100644 index 57ed19b6965..00000000000 --- a/erpnext/selling/doctype/sales_order/test_sales_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: Sales Order", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially('Sales Order', [ - // insert a new Sales Order - () => frappe.tests.make([ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 974648d6d44..d685fbff82b 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -5,7 +5,7 @@ import json import unittest import frappe import frappe.permissions -from frappe.utils import flt, add_days, nowdate +from frappe.utils import flt, add_days, nowdate, getdate from frappe.core.doctype.user_permission.test_user_permission import create_user from erpnext.selling.doctype.sales_order.sales_order \ import make_material_request, make_delivery_note, make_sales_invoice, WarehouseRequired @@ -673,6 +673,8 @@ class TestSalesOrder(unittest.TestCase): so.cancel() + dn.load_from_db() + self.assertRaises(frappe.CancelledLinkError, dn.submit) def test_service_type_product_bundle(self): @@ -1220,7 +1222,7 @@ class TestSalesOrder(unittest.TestCase): def test_so_cancellation_when_si_drafted(self): """ Test to check if Sales Order gets cancelled if Sales Invoice is in Draft state - Expected result: sales order should not get cancelled + Expected result: sales order should not get cancelled """ so = make_sales_order() so.submit() @@ -1229,7 +1231,38 @@ class TestSalesOrder(unittest.TestCase): self.assertRaises(frappe.ValidationError, so.cancel) + def test_payment_terms_are_fetched_when_creating_sales_invoice(self): + from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_terms_template + from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice + automatically_fetch_payment_terms() + + so = make_sales_order(uom="Nos", do_not_save=1) + create_payment_terms_template() + so.payment_terms_template = 'Test Receivable Template' + so.submit() + + si = create_sales_invoice(qty=10, do_not_save=1) + si.items[0].sales_order = so.name + si.items[0].so_detail = so.items[0].name + si.insert() + + self.assertEqual(so.payment_terms_template, si.payment_terms_template) + compare_payment_schedules(self, so, si) + + automatically_fetch_payment_terms(enable=0) + +def automatically_fetch_payment_terms(enable=1): + accounts_settings = frappe.get_doc("Accounts Settings") + accounts_settings.automatically_fetch_payment_terms = enable + accounts_settings.save() + +def compare_payment_schedules(doc, doc1, doc2): + for index, schedule in enumerate(doc1.get('payment_schedule')): + doc.assertEqual(schedule.payment_term, doc2.payment_schedule[index].payment_term) + doc.assertEqual(getdate(schedule.due_date), doc2.payment_schedule[index].due_date) + doc.assertEqual(schedule.invoice_portion, doc2.payment_schedule[index].invoice_portion) + doc.assertEqual(schedule.payment_amount, doc2.payment_schedule[index].payment_amount) def make_sales_order(**args): so = frappe.new_doc("Sales Order") diff --git a/erpnext/selling/doctype/sales_order/tests/test_sales_order_with_margin.js b/erpnext/selling/doctype/sales_order/tests/test_sales_order_with_margin.js index 74268685079..9eebfdaf21a 100644 --- a/erpnext/selling/doctype/sales_order/tests/test_sales_order_with_margin.js +++ b/erpnext/selling/doctype/sales_order/tests/test_sales_order_with_margin.js @@ -35,4 +35,3 @@ QUnit.test("test sales order with margin", function(assert) { () => done() ]); }); - diff --git a/erpnext/selling/doctype/sales_order/tests/test_sales_order_with_multiple_delivery_date.js b/erpnext/selling/doctype/sales_order/tests/test_sales_order_with_multiple_delivery_date.js index 8e0538511af..be76c49f845 100644 --- a/erpnext/selling/doctype/sales_order/tests/test_sales_order_with_multiple_delivery_date.js +++ b/erpnext/selling/doctype/sales_order/tests/test_sales_order_with_multiple_delivery_date.js @@ -56,4 +56,4 @@ QUnit.test("test: Sales Order", function (assert) { }, () => done() ]); -}); \ No newline at end of file +}); diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.py b/erpnext/selling/doctype/sales_order_item/sales_order_item.py index 27f303d43b1..62afef3e170 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.py +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.py @@ -10,4 +10,4 @@ class SalesOrderItem(Document): pass def on_doctype_update(): - frappe.db.add_index("Sales Order Item", ["item_code", "warehouse"]) \ No newline at end of file + frappe.db.add_index("Sales Order Item", ["item_code", "warehouse"]) diff --git a/erpnext/selling/doctype/sales_partner_type/test_sales_partner_type.js b/erpnext/selling/doctype/sales_partner_type/test_sales_partner_type.js deleted file mode 100644 index 3ed7b46e1d9..00000000000 --- a/erpnext/selling/doctype/sales_partner_type/test_sales_partner_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: Sales Partner Type", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Sales Partner Type - () => frappe.tests.make('Sales Partner Type', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/selling/doctype/sales_team/sales_team.py b/erpnext/selling/doctype/sales_team/sales_team.py index 1832108399f..28bea254d68 100644 --- a/erpnext/selling/doctype/sales_team/sales_team.py +++ b/erpnext/selling/doctype/sales_team/sales_team.py @@ -7,4 +7,4 @@ import frappe from frappe.model.document import Document class SalesTeam(Document): - pass \ No newline at end of file + pass diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.js b/erpnext/selling/doctype/selling_settings/selling_settings.js index 95a4243fb45..d8d30515f8f 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.js +++ b/erpnext/selling/doctype/selling_settings/selling_settings.js @@ -28,4 +28,4 @@ frappe.tour['Selling Settings'] = [ title: "Delivery Note Required for Sales Invoice Creation", description: __("If this option is configured 'Yes', ERPNext will prevent you from creating a Sales Invoice without creating a Delivery Note first. This configuration can be overridden for a particular Customer by enabling the 'Allow Sales Invoice Creation Without Delivery Note' checkbox in the Customer master.") } -]; \ No newline at end of file +]; diff --git a/erpnext/selling/doctype/sms_center/sms_center.py b/erpnext/selling/doctype/sms_center/sms_center.py index d142d16248f..87846a84d30 100644 --- a/erpnext/selling/doctype/sms_center/sms_center.py +++ b/erpnext/selling/doctype/sms_center/sms_center.py @@ -83,4 +83,3 @@ class SMSCenter(Document): receiver_list = self.get_receiver_nos() if receiver_list: send_sms(receiver_list, cstr(self.message)) - diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.js b/erpnext/selling/page/point_of_sale/point_of_sale.js index e3405e0ce89..56b070bb285 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.js +++ b/erpnext/selling/page/point_of_sale/point_of_sale.js @@ -19,4 +19,4 @@ frappe.pages['point-of-sale'].refresh = function(wrapper) { wrapper.pos.wrapper.html(""); wrapper.pos.check_opening_entry(); } -}; \ No newline at end of file +}; diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.py b/erpnext/selling/page/point_of_sale/point_of_sale.py index 296c8c2fd9d..3dc9094ac27 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.py +++ b/erpnext/selling/page/point_of_sale/point_of_sale.py @@ -146,7 +146,7 @@ def filter_service_items(items): if not item['is_stock_item']: if not frappe.db.exists('Product Bundle', item['item_code']): items.remove(item) - + return items def get_conditions(search_term): diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index c827368dbf5..e61a634aaee 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -525,7 +525,7 @@ erpnext.PointOfSale.Controller = class { } } else { - if (!this.frm.doc.customer) + if (!this.frm.doc.customer) return this.raise_customer_selection_alert(); const { item_code, batch_no, serial_no, rate } = item; @@ -549,7 +549,7 @@ erpnext.PointOfSale.Controller = class { await this.check_stock_availability(item_row, value, this.frm.doc.set_warehouse); await this.trigger_new_item_events(item_row); - + this.update_cart_html(item_row); if (this.item_details.$component.is(':visible')) @@ -708,4 +708,3 @@ erpnext.PointOfSale.Controller = class { .catch(e => console.log(e)); } }; - diff --git a/erpnext/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js index 7cae0e47974..9d8338e5fed 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_cart.js +++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js @@ -367,15 +367,16 @@ erpnext.PointOfSale.ItemCart = class { `
          ` ); const me = this; + const frm = me.events.get_frm(); + let discount = frm.doc.additional_discount_percentage; this.discount_field = frappe.ui.form.make_control({ df: { label: __('Discount'), fieldtype: 'Data', - placeholder: __('Enter discount percentage.'), + placeholder: ( discount ? discount + '%' : __('Enter discount percentage.') ), input_class: 'input-xs', onchange: function() { - const frm = me.events.get_frm(); if (flt(this.value) != 0) { frappe.model.set_value(frm.doc.doctype, frm.doc.name, 'additional_discount_percentage', flt(this.value)); me.hide_discount_control(this.value); @@ -472,12 +473,7 @@ erpnext.PointOfSale.ItemCart = class { const grand_total = cint(frappe.sys_defaults.disable_rounded_total) ? frm.doc.grand_total : frm.doc.rounded_total; this.render_grand_total(grand_total); - const taxes = frm.doc.taxes.map(t => { - return { - description: t.description, rate: t.rate - }; - }); - this.render_taxes(frm.doc.total_taxes_and_charges, taxes); + this.render_taxes(frm.doc.taxes); } render_net_total(value) { @@ -502,14 +498,14 @@ erpnext.PointOfSale.ItemCart = class { ); } - render_taxes(value, taxes) { + render_taxes(taxes) { if (taxes.length) { const currency = this.events.get_frm().doc.currency; const taxes_html = taxes.map(t => { const description = /[0-9]+/.test(t.description) ? t.description : `${t.description} @ ${t.rate}%`; return `
          ${description}
          -
          ${format_currency(value, currency)}
          +
          ${format_currency(t.tax_amount_after_discount_amount, currency)}
          `; }).join(''); this.$totals_section.find('.taxes-container').css('display', 'flex').html(taxes_html); @@ -568,7 +564,6 @@ erpnext.PointOfSale.ItemCart = class { ) set_dynamic_rate_header_width(); - this.scroll_to_item($item_to_update); function set_dynamic_rate_header_width() { const rate_cols = Array.from(me.$cart_items_wrapper.find(".item-rate-amount")); @@ -643,12 +638,6 @@ erpnext.PointOfSale.ItemCart = class { $($img).parent().replaceWith(`
          ${item_abbr}
          `); } - scroll_to_item($item) { - if ($item.length === 0) return; - const scrollTop = $item.offset().top - this.$cart_items_wrapper.offset().top + this.$cart_items_wrapper.scrollTop(); - this.$cart_items_wrapper.animate({ scrollTop }); - } - update_selector_value_in_cart_item(selector, value, item) { const $item_to_update = this.get_cart_item(item); $item_to_update.attr(`data-${selector}`, escape(value)); @@ -970,8 +959,23 @@ erpnext.PointOfSale.ItemCart = class { }); } + attach_refresh_field_event(frm) { + $(frm.wrapper).off('refresh-fields'); + $(frm.wrapper).on('refresh-fields', () => { + if (frm.doc.items.length) { + frm.doc.items.forEach(item => { + this.update_item_html(item); + }); + } + this.update_totals_section(frm); + }); + } + load_invoice() { const frm = this.events.get_frm(); + + this.attach_refresh_field_event(frm); + this.fetch_customer_details(frm.doc.customer).then(() => { this.events.customer_details_updated(this.customer_info); this.update_customer_section(); diff --git a/erpnext/selling/page/point_of_sale/pos_item_details.js b/erpnext/selling/page/point_of_sale/pos_item_details.js index 6a4d3d5214d..d899c5c19b4 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_details.js +++ b/erpnext/selling/page/point_of_sale/pos_item_details.js @@ -65,7 +65,7 @@ erpnext.PointOfSale.ItemDetails = class { // if item is null or highlighted cart item is clicked twice const hide_item_details = !Boolean(item) || !current_item_changed; - + this.events.toggle_item_selector(!hide_item_details); this.toggle_component(!hide_item_details); @@ -127,7 +127,7 @@ erpnext.PointOfSale.ItemDetails = class { this.$item_price.html(format_currency(price_list_rate, this.currency)); if (!this.hide_images && image) { this.$item_image.html( - `${frappe.get_abbr(item_name)}${qty_to_display}
          - ${frappe.get_abbr(item.item_name)} { @@ -252,41 +253,6 @@ erpnext.PointOfSale.Payment = class { } } - setup_listener_for_payments() { - frappe.realtime.on("process_phone_payment", (data) => { - const doc = this.events.get_frm().doc; - const { response, amount, success, failure_message } = data; - let message, title; - - if (success) { - title = __("Payment Received"); - if (amount >= doc.grand_total) { - frappe.dom.unfreeze(); - message = __("Payment of {0} received successfully.", [format_currency(amount, doc.currency, 0)]); - this.events.submit_invoice(); - cur_frm.reload_doc(); - - } else { - message = __("Payment of {0} received successfully. Waiting for other requests to complete...", [format_currency(amount, doc.currency, 0)]); - } - } else if (failure_message) { - message = failure_message; - title = __("Payment Failed"); - } - - frappe.msgprint({ "message": message, "title": title }); - }); - } - - auto_set_remaining_amount() { - const doc = this.events.get_frm().doc; - const remaining_amount = doc.grand_total - doc.paid_amount; - const current_value = this.selected_mode ? this.selected_mode.get_value() : undefined; - if (!current_value && remaining_amount > 0 && this.selected_mode) { - this.selected_mode.set_value(remaining_amount); - } - } - attach_shortcuts() { const ctrl_label = frappe.utils.is_mac() ? '⌘' : 'Ctrl'; this.$component.find('.submit-order-btn').attr("title", `${ctrl_label}+Enter`); diff --git a/erpnext/selling/page/sales_funnel/sales_funnel.css b/erpnext/selling/page/sales_funnel/sales_funnel.css index 89e904fcfc9..60b2392ac43 100644 --- a/erpnext/selling/page/sales_funnel/sales_funnel.css +++ b/erpnext/selling/page/sales_funnel/sales_funnel.css @@ -1,3 +1,4 @@ .funnel-wrapper { margin: 15px; -} \ No newline at end of file + width: 100%; +} diff --git a/erpnext/selling/page/sales_funnel/sales_funnel.py b/erpnext/selling/page/sales_funnel/sales_funnel.py index b613718c7e2..78aaa49a668 100644 --- a/erpnext/selling/page/sales_funnel/sales_funnel.py +++ b/erpnext/selling/page/sales_funnel/sales_funnel.py @@ -32,7 +32,7 @@ def get_funnel_data(from_date, to_date, company): and (opportunity!="" or quotation_to="Lead") and company=%s""", (from_date, to_date, company))[0][0] converted = frappe.db.sql("""select count(*) from `tabCustomer` - JOIN `tabLead` ON `tabLead`.name = `tabCustomer`.lead_name + JOIN `tabLead` ON `tabLead`.name = `tabCustomer`.lead_name WHERE (date(`tabCustomer`.creation) between %s and %s) and `tabLead`.company=%s""", (from_date, to_date, company))[0][0] @@ -97,4 +97,4 @@ def get_pipeline_data(from_date, to_date, company): return result else: - return 'empty' \ No newline at end of file + return 'empty' diff --git a/erpnext/selling/report/address_and_contacts/address_and_contacts.py b/erpnext/selling/report/address_and_contacts/address_and_contacts.py index a9e43034b48..f295333322c 100644 --- a/erpnext/selling/report/address_and_contacts/address_and_contacts.py +++ b/erpnext/selling/report/address_and_contacts/address_and_contacts.py @@ -117,4 +117,4 @@ def get_party_group(party_type): "Sales Partner": "partner_type" } - return group[party_type] \ No newline at end of file + return group[party_type] diff --git a/erpnext/selling/report/available_stock_for_packing_items/available_stock_for_packing_items.py b/erpnext/selling/report/available_stock_for_packing_items/available_stock_for_packing_items.py index 056492a3274..5523bad5715 100644 --- a/erpnext/selling/report/available_stock_for_packing_items/available_stock_for_packing_items.py +++ b/erpnext/selling/report/available_stock_for_packing_items/available_stock_for_packing_items.py @@ -76,4 +76,4 @@ def get_item_warehouse_quantity_map(): last_sbom = line.get("parent") actual_dict = sbom_map.setdefault(last_sbom, {}) actual_dict.setdefault(line.get("warehouse"), line.get("qty")) - return sbom_map \ No newline at end of file + return sbom_map diff --git a/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.js b/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.js index d93ffb7266b..1b931e12de3 100644 --- a/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.js +++ b/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.js @@ -41,4 +41,4 @@ frappe.query_reports["Customer Acquisition and Loyalty"] = { } return value; } -} \ No newline at end of file +} diff --git a/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.js b/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.js index f47d67fe494..073be789791 100644 --- a/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.js +++ b/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.js @@ -60,4 +60,4 @@ frappe.query_reports["Item-wise Sales History"] = { } return value; } -}; \ No newline at end of file +}; diff --git a/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py b/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py index 84732760019..1700fc7bdd5 100644 --- a/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py +++ b/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py @@ -266,4 +266,4 @@ def get_chart_data(data): ] }, "type" : "bar" - } \ No newline at end of file + } diff --git a/erpnext/selling/report/quotation_trends/quotation_trends.js b/erpnext/selling/report/quotation_trends/quotation_trends.js index f00ca278b00..97a19315ec3 100644 --- a/erpnext/selling/report/quotation_trends/quotation_trends.js +++ b/erpnext/selling/report/quotation_trends/quotation_trends.js @@ -6,4 +6,3 @@ frappe.require("assets/erpnext/js/sales_trends_filters.js", function() { filters: erpnext.get_sales_trends_filters() } }); - diff --git a/erpnext/selling/report/sales_analytics/sales_analytics.js b/erpnext/selling/report/sales_analytics/sales_analytics.js index 9089b53fb04..6b03c7d92fe 100644 --- a/erpnext/selling/report/sales_analytics/sales_analytics.js +++ b/erpnext/selling/report/sales_analytics/sales_analytics.js @@ -141,5 +141,3 @@ frappe.query_reports["Sales Analytics"] = { }); }, } - - diff --git a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py index 8cb24460f7b..00dcd69c6e6 100644 --- a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py +++ b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py @@ -276,4 +276,4 @@ def get_columns(filters): }) - return columns \ No newline at end of file + return columns diff --git a/erpnext/selling/report/sales_order_trends/sales_order_trends.js b/erpnext/selling/report/sales_order_trends/sales_order_trends.js index ea320d6b481..b22ea8fa2ca 100644 --- a/erpnext/selling/report/sales_order_trends/sales_order_trends.js +++ b/erpnext/selling/report/sales_order_trends/sales_order_trends.js @@ -5,4 +5,4 @@ frappe.require("assets/erpnext/js/sales_trends_filters.js", function() { frappe.query_reports["Sales Order Trends"] = { filters: erpnext.get_sales_trends_filters() } -}); \ No newline at end of file +}); diff --git a/erpnext/selling/report/sales_partner_commission_summary/sales_partner_commission_summary.py b/erpnext/selling/report/sales_partner_commission_summary/sales_partner_commission_summary.py index 66f9aaeffc6..2c49d51a920 100644 --- a/erpnext/selling/report/sales_partner_commission_summary/sales_partner_commission_summary.py +++ b/erpnext/selling/report/sales_partner_commission_summary/sales_partner_commission_summary.py @@ -110,4 +110,4 @@ def get_conditions(filters, date_field): if filters.get("to_date"): conditions += " and {0} <= %(to_date)s".format(date_field) - return conditions \ No newline at end of file + return conditions diff --git a/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py b/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py index ae216ca5d69..89cfa16abe0 100644 --- a/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py +++ b/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py @@ -208,4 +208,4 @@ def get_parents_data(filters, partner_doctype): return frappe.get_all('Target Detail', filters = filters_dict, - fields = ["parent", "item_group", target_qty_amt_field, "fiscal_year", "distribution_id"]) \ No newline at end of file + fields = ["parent", "item_group", target_qty_amt_field, "fiscal_year", "distribution_id"]) diff --git a/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/sales_partner_target_variance_based_on_item_group.js b/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/sales_partner_target_variance_based_on_item_group.js index 38bb127e235..adae47b87da 100644 --- a/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/sales_partner_target_variance_based_on_item_group.js +++ b/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/sales_partner_target_variance_based_on_item_group.js @@ -47,9 +47,9 @@ frappe.query_reports["Sales Partner Target Variance based on Item Group"] = { ], "formatter": function (value, row, column, data, default_formatter) { value = default_formatter(value, row, column, data); - + if (column.fieldname.includes('variance')) { - + if (data[column.fieldname] < 0) { value = "" + value + ""; } diff --git a/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/sales_partner_target_variance_based_on_item_group.py b/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/sales_partner_target_variance_based_on_item_group.py index e41011fba2c..87ed5a8ea21 100644 --- a/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/sales_partner_target_variance_based_on_item_group.py +++ b/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/sales_partner_target_variance_based_on_item_group.py @@ -9,4 +9,3 @@ def execute(filters=None): data = [] return get_data_column(filters, "Sales Partner") - diff --git a/erpnext/selling/report/sales_partner_transaction_summary/sales_partner_transaction_summary.py b/erpnext/selling/report/sales_partner_transaction_summary/sales_partner_transaction_summary.py index 53560285b3f..f07293d8ec2 100644 --- a/erpnext/selling/report/sales_partner_transaction_summary/sales_partner_transaction_summary.py +++ b/erpnext/selling/report/sales_partner_transaction_summary/sales_partner_transaction_summary.py @@ -165,4 +165,4 @@ def get_conditions(filters, date_field): `tabItem Group` where lft >= %s and rgt <= %s)""" % (lft, rgt) - return conditions \ No newline at end of file + return conditions diff --git a/erpnext/selling/report/sales_person_commission_summary/sales_person_commission_summary.py b/erpnext/selling/report/sales_person_commission_summary/sales_person_commission_summary.py index 0c849096116..9917d72af86 100644 --- a/erpnext/selling/report/sales_person_commission_summary/sales_person_commission_summary.py +++ b/erpnext/selling/report/sales_person_commission_summary/sales_person_commission_summary.py @@ -101,7 +101,7 @@ def get_columns(filters): def get_entries(filters): date_field = filters["doc_type"] == "Sales Order" and "transaction_date" or "posting_date" - + conditions, values = get_conditions(filters, date_field) entries = frappe.db.sql(""" select @@ -111,7 +111,7 @@ def get_entries(filters): `tab%s` dt, `tabSales Team` st where st.parent = dt.name and st.parenttype = %s - and dt.docstatus = 1 %s order by dt.name desc,st.sales_person + and dt.docstatus = 1 %s order by dt.name desc,st.sales_person """ %(date_field, filters["doc_type"], '%s', conditions), tuple([filters["doc_type"]] + values), as_dict=1) @@ -138,5 +138,3 @@ def get_conditions(filters, date_field): values.append(filters["to_date"]) return " and ".join(conditions), values - - diff --git a/erpnext/selling/report/sales_person_target_variance_based_on_item_group/sales_person_target_variance_based_on_item_group.js b/erpnext/selling/report/sales_person_target_variance_based_on_item_group/sales_person_target_variance_based_on_item_group.js index a8e2fad3734..2b8443627d5 100644 --- a/erpnext/selling/report/sales_person_target_variance_based_on_item_group/sales_person_target_variance_based_on_item_group.js +++ b/erpnext/selling/report/sales_person_target_variance_based_on_item_group/sales_person_target_variance_based_on_item_group.js @@ -47,9 +47,9 @@ frappe.query_reports["Sales Person Target Variance Based On Item Group"] = { ], "formatter": function (value, row, column, data, default_formatter) { value = default_formatter(value, row, column, data); - + if (column.fieldname.includes('variance')) { - + if (data[column.fieldname] < 0) { value = "" + value + ""; } diff --git a/erpnext/selling/report/sales_person_target_variance_based_on_item_group/sales_person_target_variance_based_on_item_group.py b/erpnext/selling/report/sales_person_target_variance_based_on_item_group/sales_person_target_variance_based_on_item_group.py index 5166cc808e9..ea9bbab0c72 100644 --- a/erpnext/selling/report/sales_person_target_variance_based_on_item_group/sales_person_target_variance_based_on_item_group.py +++ b/erpnext/selling/report/sales_person_target_variance_based_on_item_group/sales_person_target_variance_based_on_item_group.py @@ -8,4 +8,4 @@ from erpnext.selling.report.sales_partner_target_variance_based_on_item_group.it def execute(filters=None): data = [] - return get_data_column(filters, "Sales Person") \ No newline at end of file + return get_data_column(filters, "Sales Person") diff --git a/erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.js b/erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.js index b236151bad9..e269f02d0ce 100644 --- a/erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.js +++ b/erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.js @@ -67,4 +67,4 @@ frappe.query_reports["Sales Person-wise Transaction Summary"] = { default: 0, }, ] -} \ No newline at end of file +} diff --git a/erpnext/selling/report/territory_target_variance_based_on_item_group/territory_target_variance_based_on_item_group.js b/erpnext/selling/report/territory_target_variance_based_on_item_group/territory_target_variance_based_on_item_group.js index 263391a7f72..9f3d255e662 100644 --- a/erpnext/selling/report/territory_target_variance_based_on_item_group/territory_target_variance_based_on_item_group.js +++ b/erpnext/selling/report/territory_target_variance_based_on_item_group/territory_target_variance_based_on_item_group.js @@ -47,9 +47,9 @@ frappe.query_reports["Territory Target Variance Based On Item Group"] = { ], "formatter": function (value, row, column, data, default_formatter) { value = default_formatter(value, row, column, data); - + if (column.fieldname.includes('variance')) { - + if (data[column.fieldname] < 0) { value = "" + value + ""; } diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js index 04285735abd..26ac630a32b 100644 --- a/erpnext/selling/sales_common.js +++ b/erpnext/selling/sales_common.js @@ -390,6 +390,10 @@ erpnext.selling.SellingController = erpnext.TransactionController.extend({ }, _set_batch_number: function(doc) { + if (doc.batch_no) { + return + } + let args = {'item_code': doc.item_code, 'warehouse': doc.warehouse, 'qty': flt(doc.qty) * flt(doc.conversion_factor)}; if (doc.has_serial_no && doc.serial_no) { args['serial_no'] = doc.serial_no diff --git a/erpnext/setup/default_energy_point_rules.py b/erpnext/setup/default_energy_point_rules.py index 94f5aa488dc..8dbccc497b7 100644 --- a/erpnext/setup/default_energy_point_rules.py +++ b/erpnext/setup/default_energy_point_rules.py @@ -55,4 +55,3 @@ def get_default_energy_point_rules(): 'points': rule.get('points'), 'user_field': rule.get('user_field') or 'owner' } for doctype, rule in doctype_rule_map.items()] - diff --git a/erpnext/setup/default_success_action.py b/erpnext/setup/default_success_action.py index b8b09cbc53f..827839f8b75 100644 --- a/erpnext/setup/default_success_action.py +++ b/erpnext/setup/default_success_action.py @@ -24,4 +24,3 @@ def get_default_success_action(): 'first_success_message': get_first_success_message(doctype), 'next_actions': 'new\nprint\nemail' } for doctype in doctype_list] - diff --git a/erpnext/setup/doctype/brand/brand.js b/erpnext/setup/doctype/brand/brand.js index 3878a791db6..3680906057f 100644 --- a/erpnext/setup/doctype/brand/brand.js +++ b/erpnext/setup/doctype/brand/brand.js @@ -1,13 +1,13 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt - + //--------- ONLOAD ------------- cur_frm.cscript.onload = function(doc, cdt, cdn) { - + } cur_frm.cscript.refresh = function(doc, cdt, cdn) { - -} \ No newline at end of file + +} diff --git a/erpnext/setup/doctype/brand/brand.py b/erpnext/setup/doctype/brand/brand.py index 12839d18aea..a8d1cf8ff2d 100644 --- a/erpnext/setup/doctype/brand/brand.py +++ b/erpnext/setup/doctype/brand/brand.py @@ -21,4 +21,4 @@ def get_brand_defaults(item, company): row.pop("name") return row - return frappe._dict() \ No newline at end of file + return frappe._dict() diff --git a/erpnext/setup/doctype/brand/test_brand.py b/erpnext/setup/doctype/brand/test_brand.py index 265d2fe577e..25ed86ef1dd 100644 --- a/erpnext/setup/doctype/brand/test_brand.py +++ b/erpnext/setup/doctype/brand/test_brand.py @@ -4,4 +4,4 @@ from __future__ import unicode_literals import frappe -test_records = frappe.get_test_records('Brand') \ No newline at end of file +test_records = frappe.get_test_records('Brand') diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js index d05541b6344..8f83d3cd73a 100644 --- a/erpnext/setup/doctype/company/company.js +++ b/erpnext/setup/doctype/company/company.js @@ -313,4 +313,3 @@ var disbale_coa_fields = function(frm, bool=true) { frm.set_df_property("chart_of_accounts", "read_only", bool); frm.set_df_property("existing_company", "read_only", bool); } - diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json index 061986d92d7..e6ec496a65e 100644 --- a/erpnext/setup/doctype/company/company.json +++ b/erpnext/setup/doctype/company/company.json @@ -74,7 +74,7 @@ "stock_received_but_not_billed", "service_received_but_not_billed", "expenses_included_in_valuation", - "fixed_asset_depreciation_settings", + "fixed_asset_defaults", "accumulated_depreciation_account", "depreciation_expense_account", "series_for_depreciation_entry", @@ -83,6 +83,7 @@ "disposal_account", "depreciation_cost_center", "capital_work_in_progress_account", + "repair_and_maintenance_account", "asset_received_but_not_billed", "budget_detail", "exception_budget_approver_role", @@ -519,12 +520,6 @@ "no_copy": 1, "options": "Account" }, - { - "collapsible": 1, - "fieldname": "fixed_asset_depreciation_settings", - "fieldtype": "Section Break", - "label": "Fixed Asset Depreciation Settings" - }, { "fieldname": "accumulated_depreciation_account", "fieldtype": "Link", @@ -734,6 +729,18 @@ "fieldtype": "Link", "label": "Default Payment Discount Account", "options": "Account" + }, + { + "collapsible": 1, + "fieldname": "fixed_asset_defaults", + "fieldtype": "Section Break", + "label": "Fixed Asset Defaults" + }, + { + "fieldname": "repair_and_maintenance_account", + "fieldtype": "Link", + "label": "Repair and Maintenance Account", + "options": "Account" } ], "icon": "fa fa-building", @@ -741,7 +748,7 @@ "image_field": "company_logo", "is_tree": 1, "links": [], - "modified": "2021-05-07 03:11:28.189740", + "modified": "2021-05-12 16:51:08.187233", "modified_by": "Administrator", "module": "Setup", "name": "Company", diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 915e6a4f316..45d5ce0c1c7 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -109,6 +109,9 @@ class Company(NestedSet): self.create_default_accounts() self.create_default_warehouses() + if not frappe.db.get_value("Cost Center", {"is_group": 0, "company": self.name}): + self.create_default_cost_center() + if frappe.flags.country_change: install_country_fixtures(self.name, self.country) self.create_default_tax_template() @@ -117,9 +120,6 @@ class Company(NestedSet): from erpnext.setup.setup_wizard.operations.install_fixtures import install_post_company_fixtures install_post_company_fixtures(frappe._dict({'company_name': self.name})) - if not frappe.db.get_value("Cost Center", {"is_group": 0, "company": self.name}): - self.create_default_cost_center() - if not frappe.local.flags.ignore_chart_of_accounts: self.set_default_accounts() if self.default_cash_account: @@ -291,7 +291,7 @@ class Company(NestedSet): cash = frappe.db.get_value('Mode of Payment', {'type': 'Cash'}, 'name') if cash and self.default_cash_account \ and not frappe.db.get_value('Mode of Payment Account', {'company': self.name, 'parent': cash}): - mode_of_payment = frappe.get_doc('Mode of Payment', cash) + mode_of_payment = frappe.get_doc('Mode of Payment', cash, for_update=True) mode_of_payment.append('accounts', { 'company': self.name, 'default_account': self.default_cash_account @@ -393,9 +393,13 @@ class Company(NestedSet): frappe.db.sql("delete from `tabPurchase Taxes and Charges Template` where company=%s", self.name) frappe.db.sql("delete from `tabItem Tax Template` where company=%s", self.name) + # delete Process Deferred Accounts if no GL Entry found + if not frappe.db.get_value('GL Entry', {'company': self.name}): + frappe.db.sql("delete from `tabProcess Deferred Accounting` where company=%s", self.name) + @frappe.whitelist() def enqueue_replace_abbr(company, old, new): - kwargs = dict(company=company, old=old, new=new) + kwargs = dict(queue="long", company=company, old=old, new=new) frappe.enqueue('erpnext.setup.doctype.company.company.replace_abbr', **kwargs) diff --git a/erpnext/setup/doctype/company/company_dashboard.py b/erpnext/setup/doctype/company/company_dashboard.py index 9b483dd55e5..2d760284e5a 100644 --- a/erpnext/setup/doctype/company/company_dashboard.py +++ b/erpnext/setup/doctype/company/company_dashboard.py @@ -37,4 +37,4 @@ def get_data(): 'items': ['Project'] } ] - } \ No newline at end of file + } diff --git a/erpnext/setup/doctype/company/company_tree.js b/erpnext/setup/doctype/company/company_tree.js index 19b276c77db..160481cc952 100644 --- a/erpnext/setup/doctype/company/company_tree.js +++ b/erpnext/setup/doctype/company/company_tree.js @@ -30,4 +30,4 @@ frappe.treeview_settings["Company"] = { onload: function(treeview) { treeview.make_tree(); } -}; \ No newline at end of file +}; diff --git a/erpnext/setup/doctype/company/test_company.py b/erpnext/setup/doctype/company/test_company.py index e1c803a038b..1b7fd4fd5c8 100644 --- a/erpnext/setup/doctype/company/test_company.py +++ b/erpnext/setup/doctype/company/test_company.py @@ -130,4 +130,3 @@ def create_test_lead_in_company(company): lead.company = company lead.save() return lead.name - diff --git a/erpnext/setup/doctype/company/tests/test_company.js b/erpnext/setup/doctype/company/tests/test_company.js index 8c0b609775a..b568494c84a 100644 --- a/erpnext/setup/doctype/company/tests/test_company.js +++ b/erpnext/setup/doctype/company/tests/test_company.js @@ -22,4 +22,4 @@ QUnit.test("Test: Company [SetUp]", function (assert) { 'chart of cost centers created'), () => done() ]); -}); \ No newline at end of file +}); diff --git a/erpnext/setup/doctype/company/tests/test_company_production.js b/erpnext/setup/doctype/company/tests/test_company_production.js index bf6e5405b42..a4c1e2e7dea 100644 --- a/erpnext/setup/doctype/company/tests/test_company_production.js +++ b/erpnext/setup/doctype/company/tests/test_company_production.js @@ -16,4 +16,4 @@ QUnit.test("Test: Company", function (assert) { () => done() ]); -}); \ No newline at end of file +}); diff --git a/erpnext/setup/doctype/currency_exchange/currency_exchange.js b/erpnext/setup/doctype/currency_exchange/currency_exchange.js index a8ea55ca0cb..6e212affdd7 100644 --- a/erpnext/setup/doctype/currency_exchange/currency_exchange.js +++ b/erpnext/setup/doctype/currency_exchange/currency_exchange.js @@ -7,24 +7,24 @@ $.extend(cur_frm.cscript, { cur_frm.set_value("to_currency", frappe.defaults.get_global_default("currency")); } }, - + refresh: function() { cur_frm.cscript.set_exchange_rate_label(); }, - + from_currency: function() { cur_frm.cscript.set_exchange_rate_label(); }, - + to_currency: function() { cur_frm.cscript.set_exchange_rate_label(); }, - + set_exchange_rate_label: function() { if(cur_frm.doc.from_currency && cur_frm.doc.to_currency) { var default_label = __(frappe.meta.docfield_map[cur_frm.doctype]["exchange_rate"].label); - cur_frm.fields_dict.exchange_rate.set_label(default_label + + cur_frm.fields_dict.exchange_rate.set_label(default_label + repl(" (1 %(from_currency)s = [?] %(to_currency)s)", cur_frm.doc)); } } -}); \ No newline at end of file +}); diff --git a/erpnext/setup/doctype/currency_exchange/test_currency_exchange.js b/erpnext/setup/doctype/currency_exchange/test_currency_exchange.js deleted file mode 100644 index 19fde2e1481..00000000000 --- a/erpnext/setup/doctype/currency_exchange/test_currency_exchange.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: Currency Exchange", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Currency Exchange - () => frappe.tests.make('Currency Exchange', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py b/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py index c5c01c57758..4ff2dd7e0e9 100644 --- a/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py +++ b/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py @@ -62,12 +62,12 @@ class TestCurrencyExchange(unittest.TestCase): exchange_rate = get_exchange_rate("USD", "INR", "2016-01-30", "for_selling") self.assertEqual(exchange_rate, 62.9) - - # Exchange rate as on 15th Dec, 2015, should be fetched from fixer.io + + # Exchange rate as on 15th Dec, 2015 self.clear_cache() exchange_rate = get_exchange_rate("USD", "INR", "2015-12-15", "for_selling") self.assertFalse(exchange_rate == 60) - self.assertEqual(flt(exchange_rate, 3), 66.894) + self.assertEqual(flt(exchange_rate, 3), 66.999) def test_exchange_rate_strict(self): # strict currency settings @@ -77,28 +77,17 @@ class TestCurrencyExchange(unittest.TestCase): exchange_rate = get_exchange_rate("USD", "INR", "2016-01-01", "for_buying") self.assertEqual(exchange_rate, 60.0) - # Will fetch from fixer.io self.clear_cache() exchange_rate = get_exchange_rate("USD", "INR", "2016-01-15", "for_buying") - self.assertEqual(flt(exchange_rate, 3), 67.79) + self.assertEqual(flt(exchange_rate, 3), 67.235) exchange_rate = get_exchange_rate("USD", "INR", "2016-01-30", "for_selling") self.assertEqual(exchange_rate, 62.9) - # Exchange rate as on 15th Dec, 2015, should be fetched from fixer.io + # Exchange rate as on 15th Dec, 2015 self.clear_cache() exchange_rate = get_exchange_rate("USD", "INR", "2015-12-15", "for_buying") - self.assertEqual(flt(exchange_rate, 3), 66.894) - - exchange_rate = get_exchange_rate("INR", "NGN", "2016-01-10", "for_selling") - self.assertEqual(exchange_rate, 65.1) - - # NGN is not available on fixer.io so these should return 0 - exchange_rate = get_exchange_rate("INR", "NGN", "2016-01-09", "for_selling") - self.assertEqual(exchange_rate, 0) - - exchange_rate = get_exchange_rate("INR", "NGN", "2016-01-11", "for_selling") - self.assertEqual(exchange_rate, 0) + self.assertEqual(flt(exchange_rate, 3), 66.999) def test_exchange_rate_strict_switched(self): # Start with allow_stale is True @@ -111,4 +100,4 @@ class TestCurrencyExchange(unittest.TestCase): # Will fetch from fixer.io self.clear_cache() exchange_rate = get_exchange_rate("USD", "INR", "2016-01-15", "for_buying") - self.assertEqual(flt(exchange_rate, 3), 67.79) + self.assertEqual(flt(exchange_rate, 3), 67.235) diff --git a/erpnext/setup/doctype/customer_group/customer_group.py b/erpnext/setup/doctype/customer_group/customer_group.py index 68e1ccb6356..c06669b16b4 100644 --- a/erpnext/setup/doctype/customer_group/customer_group.py +++ b/erpnext/setup/doctype/customer_group/customer_group.py @@ -30,4 +30,4 @@ def get_parent_customer_groups(customer_group): order by lft asc""", (lft, rgt), as_dict=True) def on_doctype_update(): - frappe.db.add_index("Customer Group", ["lft", "rgt"]) \ No newline at end of file + frappe.db.add_index("Customer Group", ["lft", "rgt"]) diff --git a/erpnext/setup/doctype/customer_group/customer_group_tree.js b/erpnext/setup/doctype/customer_group/customer_group_tree.js index b52c79c497b..d50e9c8835c 100644 --- a/erpnext/setup/doctype/customer_group/customer_group_tree.js +++ b/erpnext/setup/doctype/customer_group/customer_group_tree.js @@ -1,3 +1,3 @@ frappe.treeview_settings["Customer Group"] = { ignore_fields:["parent_customer_group"] -} \ No newline at end of file +} diff --git a/erpnext/setup/doctype/customer_group/test_customer_group.py b/erpnext/setup/doctype/customer_group/test_customer_group.py index ec1af7a6765..ec90b376cdc 100644 --- a/erpnext/setup/doctype/customer_group/test_customer_group.py +++ b/erpnext/setup/doctype/customer_group/test_customer_group.py @@ -7,4 +7,4 @@ test_ignore = ["Price List"] import frappe -test_records = frappe.get_test_records('Customer Group') \ No newline at end of file +test_records = frappe.get_test_records('Customer Group') diff --git a/erpnext/setup/doctype/email_digest/email_digest.js b/erpnext/setup/doctype/email_digest/email_digest.js index 1071ea20209..c2c2710b025 100644 --- a/erpnext/setup/doctype/email_digest/email_digest.js +++ b/erpnext/setup/doctype/email_digest/email_digest.js @@ -1,78 +1,31 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt -cur_frm.cscript.refresh = function(doc, dt, dn) { - doc = locals[dt][dn]; - cur_frm.add_custom_button(__('View Now'), function() { - frappe.call({ - method: 'erpnext.setup.doctype.email_digest.email_digest.get_digest_msg', - args: { - name: doc.name - }, - callback: function(r) { - var d = new frappe.ui.Dialog({ - title: __('Email Digest: ') + dn, - width: 800 +frappe.ui.form.on("Email Digest", { + refresh: function(frm) { + if (!frm.is_new()) { + frm.add_custom_button(__('View Now'), function() { + frappe.call({ + method: 'erpnext.setup.doctype.email_digest.email_digest.get_digest_msg', + args: { + name: frm.doc.name + }, + callback: function(r) { + let d = new frappe.ui.Dialog({ + title: __('Email Digest: {0}', [frm.doc.name]), + width: 800 + }); + $(d.body).html(r.message); + d.show(); + } }); - $(d.body).html(r.message); - d.show(); - } - }); - }, "fa fa-eye-open", "btn-default"); - - if (!cur_frm.is_new()) { - cur_frm.add_custom_button(__('Send Now'), function() { - return cur_frm.call('send', null, (r) => { - frappe.show_alert(__('Message Sent')); }); - }); + + frm.add_custom_button(__('Send Now'), function() { + return frm.call('send', null, () => { + frappe.show_alert({ message: __("Message Sent"), indicator: 'green'}); + }); + }); + } } -}; - -cur_frm.cscript.addremove_recipients = function(doc, dt, dn) { - // Get user list - - return cur_frm.call('get_users', null, function(r) { - // Open a dialog and display checkboxes against email addresses - doc = locals[dt][dn]; - var d = new frappe.ui.Dialog({ - title: __('Add/Remove Recipients'), - width: 400 - }); - - $.each(r.user_list, function(i, v) { - var fullname = frappe.user.full_name(v.name); - if(fullname !== v.name) fullname = fullname + " <" + v.name + ">"; - - if(v.enabled==0) { - fullname = repl(" %(name)s (" + __("disabled user") + ")", {name: v.name}); - } - - $('
          ').appendTo(d.body); - }); - - // Display add recipients button - d.set_primary_action("Update", function() { - cur_frm.cscript.add_to_rec_list(doc, d.body, r.user_list.length); - }); - - cur_frm.rec_dialog = d; - d.show(); - }); -} - -cur_frm.cscript.add_to_rec_list = function(doc, dialog, length) { - // add checked users to list of recipients - var rec_list = []; - $(dialog).find('input:checked').each(function(i, input) { - rec_list.push($(input).attr('data-id')); - }); - - doc.recipient_list = rec_list.join('\n'); - cur_frm.rec_dialog.hide(); - cur_frm.save(); - cur_frm.refresh_fields(); -} +}); diff --git a/erpnext/setup/doctype/email_digest/email_digest.json b/erpnext/setup/doctype/email_digest/email_digest.json index 125aca17c75..06c98e5ff94 100644 --- a/erpnext/setup/doctype/email_digest/email_digest.json +++ b/erpnext/setup/doctype/email_digest/email_digest.json @@ -1,1482 +1,338 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "Prompt", - "beta": 0, - "creation": "2018-09-16 22:00:00", - "custom": 0, - "description": "Send regular summary reports via Email.", - "docstatus": 0, - "doctype": "DocType", - "document_type": "System", - "editable_grid": 0, + "actions": [], + "autoname": "Prompt", + "creation": "2018-09-16 22:00:00", + "description": "Send regular summary reports via Email.", + "doctype": "DocType", + "document_type": "System", + "engine": "InnoDB", + "field_order": [ + "settings", + "column_break0", + "enabled", + "company", + "frequency", + "next_send", + "column_break1", + "recipients", + "accounts", + "accounts_module", + "income", + "expenses_booked", + "income_year_to_date", + "expense_year_to_date", + "column_break_16", + "bank_balance", + "credit_balance", + "invoiced_amount", + "payables", + "work_in_progress", + "sales_orders_to_bill", + "purchase_orders_to_bill", + "operation", + "column_break_21", + "sales_order", + "purchase_order", + "sales_orders_to_deliver", + "purchase_orders_to_receive", + "sales_invoice", + "purchase_invoice", + "column_break_operation", + "new_quotations", + "pending_quotations", + "issue", + "project", + "purchase_orders_items_overdue", + "other", + "tools", + "calendar_events", + "todo_list", + "notifications", + "column_break_32", + "add_quote" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "settings", - "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": "Email Digest Settings", - "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": "settings", + "fieldtype": "Section Break", + "label": "Email Digest Settings" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break0", - "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, - "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_break0", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "enabled", - "fieldtype": "Check", - "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": "Enabled", - "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 - }, + "default": "0", + "fieldname": "enabled", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Enabled" + }, { - "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": 1, - "in_standard_filter": 1, - "label": "For 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": 1, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "company", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "For Company", + "options": "Company", + "remember_last_selected_value": 1, + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "frequency", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 1, - "label": "How frequently?", - "length": 0, - "no_copy": 0, - "options": "Daily\nWeekly\nMonthly", - "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": "frequency", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "How frequently?", + "options": "Daily\nWeekly\nMonthly", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:doc.enabled", - "fieldname": "next_send", - "fieldtype": "Data", - "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": "Next email will be sent on:", - "length": 0, - "no_copy": 0, - "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 - }, + "depends_on": "eval:doc.enabled", + "fieldname": "next_send", + "fieldtype": "Data", + "label": "Next email will be sent on:", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_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, - "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_break1", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "Note: Email will not be sent to disabled users", - "fieldname": "recipient_list", - "fieldtype": "Code", - "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": "Recipients", - "length": 0, - "no_copy": 0, - "options": "Email", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "accounts", + "fieldtype": "Section Break", + "label": "Accounts" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "addremove_recipients", - "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": "Add/Remove Recipients", - "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": "accounts_module", + "fieldtype": "Column Break", + "hidden": 1, + "label": "Profit & Loss" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "accounts", - "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": "Accounts", - "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 - }, + "default": "0", + "fieldname": "income", + "fieldtype": "Check", + "label": "New Income" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "accounts_module", - "fieldtype": "Column Break", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Profit & Loss", - "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 - }, + "default": "0", + "fieldname": "expenses_booked", + "fieldtype": "Check", + "label": "New Expenses" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "", - "fieldname": "income", - "fieldtype": "Check", - "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": "New Income", - "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 - }, + "default": "0", + "fieldname": "income_year_to_date", + "fieldtype": "Check", + "label": "Annual Income" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "", - "fieldname": "expenses_booked", - "fieldtype": "Check", - "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": "New Expenses", - "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 - }, + "default": "0", + "fieldname": "expense_year_to_date", + "fieldtype": "Check", + "label": "Annual Expenses" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "income_year_to_date", - "fieldtype": "Check", - "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": "Annual Income", - "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_16", + "fieldtype": "Column Break", + "label": "Balance Sheet" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "expense_year_to_date", - "fieldtype": "Check", - "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": "Annual Expenses", - "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 - }, + "default": "0", + "fieldname": "bank_balance", + "fieldtype": "Check", + "label": "Bank Balance" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "", - "fieldname": "column_break_16", - "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": "Balance Sheet", - "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 - }, + "default": "0", + "fieldname": "credit_balance", + "fieldtype": "Check", + "label": "Bank Credit Balance" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "", - "fieldname": "bank_balance", - "fieldtype": "Check", - "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": "Bank Balance", - "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 - }, + "default": "0", + "fieldname": "invoiced_amount", + "fieldtype": "Check", + "label": "Receivables" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "credit_balance", - "fieldtype": "Check", - "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": "Bank Credit Balance", - "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 - }, + "default": "0", + "fieldname": "payables", + "fieldtype": "Check", + "label": "Payables" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "", - "fieldname": "invoiced_amount", - "fieldtype": "Check", - "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": "Receivables", - "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": "work_in_progress", + "fieldtype": "Column Break", + "label": "Work in Progress" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "", - "fieldname": "payables", - "fieldtype": "Check", - "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": "Payables", - "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 - }, + "default": "0", + "fieldname": "sales_orders_to_bill", + "fieldtype": "Check", + "label": "Sales Orders to Bill" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "work_in_progress", - "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": "Work in Progress", - "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 - }, + "default": "0", + "fieldname": "purchase_orders_to_bill", + "fieldtype": "Check", + "label": "Purchase Orders to Bill" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "sales_orders_to_bill", - "fieldtype": "Check", - "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": "Sales Orders to Bill", - "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": "operation", + "fieldtype": "Section Break", + "label": "Operations" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "purchase_orders_to_bill", - "fieldtype": "Check", - "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": "Purchase Orders to Bill", - "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_21", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "operation", - "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": "Operations", - "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 - }, + "default": "0", + "fieldname": "sales_order", + "fieldtype": "Check", + "label": "New Sales Orders" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_21", - "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, - "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 - }, + "default": "0", + "fieldname": "purchase_order", + "fieldtype": "Check", + "label": "New Purchase Orders" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "sales_order", - "fieldtype": "Check", - "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": "New Sales Orders", - "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 - }, + "default": "0", + "fieldname": "sales_orders_to_deliver", + "fieldtype": "Check", + "label": "Sales Orders to Deliver" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "purchase_order", - "fieldtype": "Check", - "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": "New Purchase Orders", - "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 - }, + "default": "0", + "fieldname": "purchase_orders_to_receive", + "fieldtype": "Check", + "label": "Purchase Orders to Receive" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "sales_orders_to_deliver", - "fieldtype": "Check", - "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": "Sales Orders to Deliver", - "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 - }, + "default": "0", + "fieldname": "sales_invoice", + "fieldtype": "Check", + "label": "New Sales Invoice" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "purchase_orders_to_receive", - "fieldtype": "Check", - "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": "Purchase Orders to Receive", - "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 - }, + "default": "0", + "fieldname": "purchase_invoice", + "fieldtype": "Check", + "label": "New Purchase Invoice" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "sales_invoice", - "fieldtype": "Check", - "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": "New Sales Invoice", - "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_operation", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "purchase_invoice", - "fieldtype": "Check", - "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": "New Purchase Invoice", - "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 - }, + "default": "0", + "fieldname": "new_quotations", + "fieldtype": "Check", + "label": "New Quotations" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_operation", - "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, - "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 - }, + "default": "0", + "fieldname": "pending_quotations", + "fieldtype": "Check", + "label": "Open Quotations" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "new_quotations", - "fieldtype": "Check", - "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": "New Quotations", - "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 - }, + "default": "0", + "fieldname": "issue", + "fieldtype": "Check", + "label": "Open Issues" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "pending_quotations", - "fieldtype": "Check", - "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": "Open Quotations", - "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 - }, + "default": "0", + "fieldname": "project", + "fieldtype": "Check", + "label": "Open Projects" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "issue", - "fieldtype": "Check", - "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": "Open Issues", - "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 - }, + "default": "0", + "fieldname": "purchase_orders_items_overdue", + "fieldtype": "Check", + "label": "Purchase Orders Items Overdue" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "project", - "fieldtype": "Check", - "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": "Open Projects", - "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": "other", + "fieldtype": "Section Break", + "label": "Other" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "purchase_orders_items_overdue", - "fieldtype": "Check", - "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": "Purchase Orders Items Overdue", - "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": "tools", + "fieldtype": "Column Break", + "label": "Tools" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "other", - "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": "Other", - "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 - }, + "default": "0", + "fieldname": "calendar_events", + "fieldtype": "Check", + "label": "Upcoming Calendar Events" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "tools", - "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": "Tools", - "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 - }, + "default": "0", + "fieldname": "todo_list", + "fieldtype": "Check", + "label": "Open To Do" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "calendar_events", - "fieldtype": "Check", - "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": "Upcoming Calendar Events", - "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 - }, + "default": "0", + "fieldname": "notifications", + "fieldtype": "Check", + "label": "Open Notifications" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "todo_list", - "fieldtype": "Check", - "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": "Open To Do", - "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_32", + "fieldtype": "Column Break", + "label": " " + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "notifications", - "fieldtype": "Check", - "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": "Open Notifications", - "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 - }, + "default": "0", + "fieldname": "add_quote", + "fieldtype": "Check", + "label": "Add Quote" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_32", - "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, - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "add_quote", - "fieldtype": "Check", - "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": "Add Quote", - "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 + "description": "Note: Email will not be sent to disabled users", + "fieldname": "recipients", + "fieldtype": "Table MultiSelect", + "label": "Recipients", + "options": "Email Digest Recipient", + "reqd": 1 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "icon": "fa fa-envelope", - "idx": 1, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "menu_index": 0, - "modified": "2019-01-16 09:52:15.149908", - "modified_by": "Administrator", - "module": "Setup", - "name": "Email Digest", - "owner": "Administrator", + ], + "icon": "fa fa-envelope", + "idx": 1, + "index_web_pages_for_search": 1, + "links": [], + "modified": "2020-08-24 23:49:00.081695", + "modified_by": "Administrator", + "module": "Setup", + "name": "Email Digest", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, "write": 1 - }, + }, { - "amend": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 0, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 1, - "print": 0, - "read": 1, - "report": 0, - "role": "System Manager", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 + "permlevel": 1, + "read": 1, + "role": "System Manager" } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 0, - "track_seen": 0, - "track_views": 0 + ], + "sort_field": "modified", + "sort_order": "DESC" } \ No newline at end of file diff --git a/erpnext/setup/doctype/email_digest/email_digest.py b/erpnext/setup/doctype/email_digest/email_digest.py index 340d89bdf8e..217829f1269 100644 --- a/erpnext/setup/doctype/email_digest/email_digest.py +++ b/erpnext/setup/doctype/email_digest/email_digest.py @@ -47,28 +47,19 @@ class EmailDigest(Document): # send email only to enabled users valid_users = [p[0] for p in frappe.db.sql("""select name from `tabUser` where enabled=1""")] - recipients = list(filter(lambda r: r in valid_users, - self.recipient_list.split("\n"))) - original_user = frappe.session.user - - if recipients: - for user_id in recipients: - frappe.set_user(user_id) - frappe.set_user_lang(user_id) + if self.recipients: + for row in self.recipients: msg_for_this_recipient = self.get_msg_html() - if msg_for_this_recipient: + if msg_for_this_recipient and row.recipient in valid_users: frappe.sendmail( - recipients=user_id, + recipients=row.recipient, subject=_("{0} Digest").format(self.frequency), message=msg_for_this_recipient, reference_doctype = self.doctype, reference_name = self.name, unsubscribe_message = _("Unsubscribe from this Email Digest")) - frappe.set_user(original_user) - frappe.set_user_lang(original_user) - def get_msg_html(self): """Build email digest content""" frappe.flags.ignore_account_permission = True diff --git a/erpnext/setup/doctype/email_digest/quotes.py b/erpnext/setup/doctype/email_digest/quotes.py index 95afe974b29..5451ee1daff 100644 --- a/erpnext/setup/doctype/email_digest/quotes.py +++ b/erpnext/setup/doctype/email_digest/quotes.py @@ -32,4 +32,3 @@ def get_random_quote(): ] return random.choice(quotes) - diff --git a/erpnext/setup/doctype/email_digest/templates/default.html b/erpnext/setup/doctype/email_digest/templates/default.html index 4ee4b0ff166..666301a643e 100644 --- a/erpnext/setup/doctype/email_digest/templates/default.html +++ b/erpnext/setup/doctype/email_digest/templates/default.html @@ -180,8 +180,8 @@
          {% endif %} - - + + {% if purchase_orders_items_overdue_list %}

          {{ _("Purchase Order Items not received on time") }}

          @@ -254,6 +254,6 @@


          Please take necessary action
          -{% endif %} - +{% endif %} +
          diff --git a/erpnext/setup/doctype/email_digest_recipient/__init__.py b/erpnext/setup/doctype/email_digest_recipient/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/setup/doctype/email_digest_recipient/email_digest_recipient.json b/erpnext/setup/doctype/email_digest_recipient/email_digest_recipient.json new file mode 100644 index 00000000000..8b2a6dcf49f --- /dev/null +++ b/erpnext/setup/doctype/email_digest_recipient/email_digest_recipient.json @@ -0,0 +1,33 @@ +{ + "actions": [], + "creation": "2020-06-08 12:19:40.428949", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "recipient" + ], + "fields": [ + { + "fieldname": "recipient", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Recipient", + "options": "User", + "reqd": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2020-08-24 23:10:23.217572", + "modified_by": "Administrator", + "module": "Setup", + "name": "Email Digest Recipient", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/setup/doctype/email_digest_recipient/email_digest_recipient.py b/erpnext/setup/doctype/email_digest_recipient/email_digest_recipient.py new file mode 100644 index 00000000000..968c51c345b --- /dev/null +++ b/erpnext/setup/doctype/email_digest_recipient/email_digest_recipient.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class EmailDigestRecipient(Document): + pass diff --git a/erpnext/setup/doctype/global_defaults/test_global_defaults.js b/erpnext/setup/doctype/global_defaults/test_global_defaults.js deleted file mode 100644 index 33634eb0f88..00000000000 --- a/erpnext/setup/doctype/global_defaults/test_global_defaults.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: Global Defaults", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Global Defaults - () => frappe.tests.make('Global Defaults', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py index 1c72cebfa9d..5fcad00af16 100644 --- a/erpnext/setup/doctype/item_group/item_group.py +++ b/erpnext/setup/doctype/item_group/item_group.py @@ -87,8 +87,8 @@ class ItemGroup(NestedSet, WebsiteGenerator): if not field_filters: field_filters = {} - # Ensure the query remains within current item group - field_filters['item_group'] = self.name + # Ensure the query remains within current item group & sub group + field_filters['item_group'] = [ig[0] for ig in get_child_groups(self.name)] engine = ProductQuery() context.items = engine.query(attribute_filters, field_filters, search, start, item_group=self.name) diff --git a/erpnext/setup/doctype/item_group/item_group_tree.js b/erpnext/setup/doctype/item_group/item_group_tree.js index 57afe02d796..b2628f4f4f8 100644 --- a/erpnext/setup/doctype/item_group/item_group_tree.js +++ b/erpnext/setup/doctype/item_group/item_group_tree.js @@ -1,3 +1,3 @@ frappe.treeview_settings["Item Group"] = { ignore_fields:["parent_item_group"] -} \ No newline at end of file +} diff --git a/erpnext/setup/doctype/item_group/test_item_group.js b/erpnext/setup/doctype/item_group/test_item_group.js deleted file mode 100644 index ea322e23d65..00000000000 --- a/erpnext/setup/doctype/item_group/test_item_group.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 Group", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Item Group - () => frappe.tests.make('Item Group', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/setup/doctype/naming_series/test_naming_series.js b/erpnext/setup/doctype/naming_series/test_naming_series.js deleted file mode 100644 index 22b664b2e6d..00000000000 --- a/erpnext/setup/doctype/naming_series/test_naming_series.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: Naming Series", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Naming Series - () => frappe.tests.make('Naming Series', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/setup/doctype/party_type/test_party_type.js b/erpnext/setup/doctype/party_type/test_party_type.js deleted file mode 100644 index c97dbc58c80..00000000000 --- a/erpnext/setup/doctype/party_type/test_party_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: Party Type", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Party Type - () => frappe.tests.make('Party Type', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/setup/doctype/print_heading/print_heading.js b/erpnext/setup/doctype/print_heading/print_heading.js index 3878a791db6..3680906057f 100644 --- a/erpnext/setup/doctype/print_heading/print_heading.js +++ b/erpnext/setup/doctype/print_heading/print_heading.js @@ -1,13 +1,13 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt - + //--------- ONLOAD ------------- cur_frm.cscript.onload = function(doc, cdt, cdn) { - + } cur_frm.cscript.refresh = function(doc, cdt, cdn) { - -} \ No newline at end of file + +} diff --git a/erpnext/setup/doctype/print_heading/print_heading.py b/erpnext/setup/doctype/print_heading/print_heading.py index 00dc0f3d919..3d5cd2d6f9e 100644 --- a/erpnext/setup/doctype/print_heading/print_heading.py +++ b/erpnext/setup/doctype/print_heading/print_heading.py @@ -7,4 +7,4 @@ import frappe from frappe.model.document import Document class PrintHeading(Document): - pass \ No newline at end of file + pass diff --git a/erpnext/setup/doctype/print_heading/test_print_heading.py b/erpnext/setup/doctype/print_heading/test_print_heading.py index 59455d2b1dd..b2be2e375e4 100644 --- a/erpnext/setup/doctype/print_heading/test_print_heading.py +++ b/erpnext/setup/doctype/print_heading/test_print_heading.py @@ -4,4 +4,4 @@ from __future__ import unicode_literals import frappe -test_records = frappe.get_test_records('Print Heading') \ No newline at end of file +test_records = frappe.get_test_records('Print Heading') diff --git a/erpnext/setup/doctype/quotation_lost_reason/quotation_lost_reason.js b/erpnext/setup/doctype/quotation_lost_reason/quotation_lost_reason.js index 3878a791db6..3680906057f 100644 --- a/erpnext/setup/doctype/quotation_lost_reason/quotation_lost_reason.js +++ b/erpnext/setup/doctype/quotation_lost_reason/quotation_lost_reason.js @@ -1,13 +1,13 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt - + //--------- ONLOAD ------------- cur_frm.cscript.onload = function(doc, cdt, cdn) { - + } cur_frm.cscript.refresh = function(doc, cdt, cdn) { - -} \ No newline at end of file + +} diff --git a/erpnext/setup/doctype/quotation_lost_reason/quotation_lost_reason.py b/erpnext/setup/doctype/quotation_lost_reason/quotation_lost_reason.py index 2cc6235b946..42c5a5a54fb 100644 --- a/erpnext/setup/doctype/quotation_lost_reason/quotation_lost_reason.py +++ b/erpnext/setup/doctype/quotation_lost_reason/quotation_lost_reason.py @@ -7,4 +7,4 @@ import frappe from frappe.model.document import Document class QuotationLostReason(Document): - pass \ No newline at end of file + pass diff --git a/erpnext/setup/doctype/quotation_lost_reason/test_quotation_lost_reason.py b/erpnext/setup/doctype/quotation_lost_reason/test_quotation_lost_reason.py index ff4c7885bc7..f6b30b649b5 100644 --- a/erpnext/setup/doctype/quotation_lost_reason/test_quotation_lost_reason.py +++ b/erpnext/setup/doctype/quotation_lost_reason/test_quotation_lost_reason.py @@ -4,4 +4,4 @@ from __future__ import unicode_literals import frappe -test_records = frappe.get_test_records('Quotation Lost Reason') \ No newline at end of file +test_records = frappe.get_test_records('Quotation Lost Reason') diff --git a/erpnext/setup/doctype/sales_person/sales_person_dashboard.py b/erpnext/setup/doctype/sales_person/sales_person_dashboard.py index 3d0b2ff7f8d..662008ec8db 100644 --- a/erpnext/setup/doctype/sales_person/sales_person_dashboard.py +++ b/erpnext/setup/doctype/sales_person/sales_person_dashboard.py @@ -12,4 +12,4 @@ def get_data(): 'items': ['Sales Order', 'Delivery Note', 'Sales Invoice'] }, ] - } \ No newline at end of file + } diff --git a/erpnext/setup/doctype/sales_person/sales_person_tree.js b/erpnext/setup/doctype/sales_person/sales_person_tree.js index bcdfac926c4..00056fde869 100644 --- a/erpnext/setup/doctype/sales_person/sales_person_tree.js +++ b/erpnext/setup/doctype/sales_person/sales_person_tree.js @@ -9,4 +9,4 @@ frappe.treeview_settings["Sales Person"] = { {fieldtype:'Check', fieldname:'is_group', label:__('Group Node'), description: __("Further nodes can be only created under 'Group' type nodes")} ], -} \ No newline at end of file +} diff --git a/erpnext/setup/doctype/supplier_group/supplier_group_tree.js b/erpnext/setup/doctype/supplier_group/supplier_group_tree.js index 0788e2e167a..728793eb25f 100644 --- a/erpnext/setup/doctype/supplier_group/supplier_group_tree.js +++ b/erpnext/setup/doctype/supplier_group/supplier_group_tree.js @@ -1,4 +1,4 @@ frappe.treeview_settings["Supplier Group"] = { breadcrumbs: "Buying", ignore_fields:["parent_supplier_group"] -}; \ No newline at end of file +}; diff --git a/erpnext/setup/doctype/supplier_group/test_supplier_group.js b/erpnext/setup/doctype/supplier_group/test_supplier_group.js deleted file mode 100644 index 976dd2cc4f3..00000000000 --- a/erpnext/setup/doctype/supplier_group/test_supplier_group.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: Supplier Group", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Supplier Group - () => frappe.tests.make('Supplier Group', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/setup/doctype/target_detail/target_detail.py b/erpnext/setup/doctype/target_detail/target_detail.py index d2e2597cb47..633be45d20b 100644 --- a/erpnext/setup/doctype/target_detail/target_detail.py +++ b/erpnext/setup/doctype/target_detail/target_detail.py @@ -7,4 +7,4 @@ import frappe from frappe.model.document import Document class TargetDetail(Document): - pass \ No newline at end of file + pass diff --git a/erpnext/setup/doctype/terms_and_conditions/terms_and_conditions.js b/erpnext/setup/doctype/terms_and_conditions/terms_and_conditions.js index 3878a791db6..3680906057f 100644 --- a/erpnext/setup/doctype/terms_and_conditions/terms_and_conditions.js +++ b/erpnext/setup/doctype/terms_and_conditions/terms_and_conditions.js @@ -1,13 +1,13 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt - + //--------- ONLOAD ------------- cur_frm.cscript.onload = function(doc, cdt, cdn) { - + } cur_frm.cscript.refresh = function(doc, cdt, cdn) { - -} \ No newline at end of file + +} diff --git a/erpnext/setup/doctype/terms_and_conditions/terms_and_conditions.py b/erpnext/setup/doctype/terms_and_conditions/terms_and_conditions.py index 372cc6d3e3e..5b00ccbdbb3 100644 --- a/erpnext/setup/doctype/terms_and_conditions/terms_and_conditions.py +++ b/erpnext/setup/doctype/terms_and_conditions/terms_and_conditions.py @@ -24,6 +24,6 @@ def get_terms_and_conditions(template_name, doc): doc = json.loads(doc) terms_and_conditions = frappe.get_doc("Terms and Conditions", template_name) - + if terms_and_conditions.terms: - return frappe.render_template(terms_and_conditions.terms, doc) \ No newline at end of file + return frappe.render_template(terms_and_conditions.terms, doc) diff --git a/erpnext/setup/doctype/territory/territory.js b/erpnext/setup/doctype/territory/territory.js index ceec47ae8c6..3caf814c90b 100644 --- a/erpnext/setup/doctype/territory/territory.js +++ b/erpnext/setup/doctype/territory/territory.js @@ -36,4 +36,4 @@ cur_frm.fields_dict['parent_territory'].get_query = function(doc,cdt,cdn) { ['Territory', 'name', '!=', doc.territory_name] ] } -} \ No newline at end of file +} diff --git a/erpnext/setup/doctype/territory/territory.py b/erpnext/setup/doctype/territory/territory.py index 05e8f666cfb..7eefe77495c 100644 --- a/erpnext/setup/doctype/territory/territory.py +++ b/erpnext/setup/doctype/territory/territory.py @@ -24,4 +24,4 @@ class Territory(NestedSet): self.validate_one_root() def on_doctype_update(): - frappe.db.add_index("Territory", ["lft", "rgt"]) \ No newline at end of file + frappe.db.add_index("Territory", ["lft", "rgt"]) diff --git a/erpnext/setup/doctype/territory/territory_tree.js b/erpnext/setup/doctype/territory/territory_tree.js index edd11dfa69b..dadeeef09e9 100644 --- a/erpnext/setup/doctype/territory/territory_tree.js +++ b/erpnext/setup/doctype/territory/territory_tree.js @@ -1,3 +1,3 @@ frappe.treeview_settings["Territory"] = { ignore_fields:["parent_territory"] -} \ No newline at end of file +} diff --git a/erpnext/setup/doctype/transaction_deletion_record/test_transaction_deletion_record.py b/erpnext/setup/doctype/transaction_deletion_record/test_transaction_deletion_record.py index bbe68369ffd..933a8c3bed8 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/test_transaction_deletion_record.py +++ b/erpnext/setup/doctype/transaction_deletion_record/test_transaction_deletion_record.py @@ -23,7 +23,7 @@ class TestTransactionDeletionRecord(unittest.TestCase): contains_company = True break self.assertTrue(contains_company) - + def test_no_of_docs_is_correct(self): for i in range(5): create_task('Dunder Mifflin Paper Co') @@ -40,13 +40,13 @@ class TestTransactionDeletionRecord(unittest.TestCase): 'company' : 'Dunder Mifflin Paper Co' }) self.assertEqual(tasks_containing_company, []) - + def create_company(company_name): company = frappe.get_doc({ 'doctype': 'Company', 'company_name': company_name, 'default_currency': 'INR' - }) + }) company.insert(ignore_if_duplicate = True) def create_transaction_deletion_request(company): diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.js b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.js index 20caa15ee41..6a50ef8bbd9 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.js +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.js @@ -4,7 +4,7 @@ frappe.ui.form.on('Transaction Deletion Record', { onload: function(frm) { if (frm.doc.docstatus == 0) { - let doctypes_to_be_ignored_array; + let doctypes_to_be_ignored_array; frappe.call({ method: 'erpnext.setup.doctype.transaction_deletion_record.transaction_deletion_record.get_doctypes_to_be_ignored', callback: function(r) { @@ -25,15 +25,15 @@ frappe.ui.form.on('Transaction Deletion Record', { frm.fields_dict['doctypes_to_be_ignored'].grid.set_column_disp('no_of_docs', false); frm.refresh_field('doctypes_to_be_ignored'); } - + }); function populate_doctypes_to_be_ignored(doctypes_to_be_ignored_array, frm) { if (!(frm.doc.doctypes_to_be_ignored)) { var i; - for (i = 0; i < doctypes_to_be_ignored_array.length; i++) { + for (i = 0; i < doctypes_to_be_ignored_array.length; i++) { frm.add_child('doctypes_to_be_ignored', { - doctype_name: doctypes_to_be_ignored_array[i] + doctype_name: doctypes_to_be_ignored_array[i] }); } } diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json index 9313f955167..23e59472a6d 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json @@ -54,7 +54,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-05-08 23:13:48.049879", + "modified": "2021-08-04 20:15:59.071493", "modified_by": "Administrator", "module": "Setup", "name": "Transaction Deletion Record", @@ -70,6 +70,7 @@ "report": 1, "role": "System Manager", "share": 1, + "submit": 1, "write": 1 } ], diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py index ece9fb56992..8a491554801 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py @@ -12,10 +12,14 @@ from frappe.desk.notifications import clear_notifications class TransactionDeletionRecord(Document): def validate(self): frappe.only_for('System Manager') + self.validate_doctypes_to_be_ignored() + + def validate_doctypes_to_be_ignored(self): doctypes_to_be_ignored_list = get_doctypes_to_be_ignored() for doctype in self.doctypes_to_be_ignored: if doctype.doctype_name not in doctypes_to_be_ignored_list: - frappe.throw(_("DocTypes should not be added manually to the 'Excluded DocTypes' table. You are only allowed to remove entries from it. "), title=_("Not Allowed")) + frappe.throw(_("DocTypes should not be added manually to the 'Excluded DocTypes' table. You are only allowed to remove entries from it."), + title=_("Not Allowed")) def before_submit(self): if not self.doctypes_to_be_ignored: @@ -23,62 +27,122 @@ class TransactionDeletionRecord(Document): self.delete_bins() self.delete_lead_addresses() - - company_obj = frappe.get_doc('Company', self.company) - # reset company values - company_obj.total_monthly_sales = 0 - company_obj.sales_monthly_history = None - company_obj.save() - # Clear notification counts + self.reset_company_values() clear_notifications() + self.delete_company_transactions() - singles = frappe.get_all('DocType', filters = {'issingle': 1}, pluck = 'name') - tables = frappe.get_all('DocType', filters = {'istable': 1}, pluck = 'name') - doctypes_to_be_ignored_list = singles - for doctype in self.doctypes_to_be_ignored: - doctypes_to_be_ignored_list.append(doctype.doctype_name) - - docfields = frappe.get_all('DocField', - filters = { - 'fieldtype': 'Link', - 'options': 'Company', - 'parent': ['not in', doctypes_to_be_ignored_list]}, - fields=['parent', 'fieldname']) - - for docfield in docfields: - if docfield['parent'] != self.doctype: - no_of_docs = frappe.db.count(docfield['parent'], { - docfield['fieldname'] : self.company - }) - - if no_of_docs > 0: - self.delete_version_log(docfield['parent'], docfield['fieldname']) - self.delete_communications(docfield['parent'], docfield['fieldname']) - - # populate DocTypes table - if docfield['parent'] not in tables: - self.append('doctypes', { - 'doctype_name' : docfield['parent'], - 'no_of_docs' : no_of_docs - }) - - # delete the docs linked with the specified company - frappe.db.delete(docfield['parent'], { - docfield['fieldname'] : self.company - }) - - naming_series = frappe.db.get_value('DocType', docfield['parent'], 'autoname') - if naming_series: - if '#' in naming_series: - self.update_naming_series(naming_series, docfield['parent']) - - def populate_doctypes_to_be_ignored_table(self): + def populate_doctypes_to_be_ignored_table(self): doctypes_to_be_ignored_list = get_doctypes_to_be_ignored() for doctype in doctypes_to_be_ignored_list: self.append('doctypes_to_be_ignored', { 'doctype_name' : doctype }) + def delete_bins(self): + frappe.db.sql("""delete from tabBin where warehouse in + (select name from tabWarehouse where company=%s)""", self.company) + + def delete_lead_addresses(self): + """Delete addresses to which leads are linked""" + leads = frappe.get_all('Lead', filters={'company': self.company}) + leads = ["'%s'" % row.get("name") for row in leads] + addresses = [] + if leads: + addresses = frappe.db.sql_list("""select parent from `tabDynamic Link` where link_name + in ({leads})""".format(leads=",".join(leads))) + + if addresses: + addresses = ["%s" % frappe.db.escape(addr) for addr in addresses] + + frappe.db.sql("""delete from tabAddress where name in ({addresses}) and + name not in (select distinct dl1.parent from `tabDynamic Link` dl1 + inner join `tabDynamic Link` dl2 on dl1.parent=dl2.parent + and dl1.link_doctype<>dl2.link_doctype)""".format(addresses=",".join(addresses))) + + frappe.db.sql("""delete from `tabDynamic Link` where link_doctype='Lead' + and parenttype='Address' and link_name in ({leads})""".format(leads=",".join(leads))) + + frappe.db.sql("""update tabCustomer set lead_name=NULL where lead_name in ({leads})""".format(leads=",".join(leads))) + + def reset_company_values(self): + company_obj = frappe.get_doc('Company', self.company) + company_obj.total_monthly_sales = 0 + company_obj.sales_monthly_history = None + company_obj.save() + + def delete_company_transactions(self): + doctypes_to_be_ignored_list = self.get_doctypes_to_be_ignored_list() + docfields = self.get_doctypes_with_company_field(doctypes_to_be_ignored_list) + + tables = self.get_all_child_doctypes() + for docfield in docfields: + if docfield['parent'] != self.doctype: + no_of_docs = self.get_number_of_docs_linked_with_specified_company(docfield['parent'], docfield['fieldname']) + + if no_of_docs > 0: + self.delete_version_log(docfield['parent'], docfield['fieldname']) + self.delete_communications(docfield['parent'], docfield['fieldname']) + self.populate_doctypes_table(tables, docfield['parent'], no_of_docs) + + self.delete_child_tables(docfield['parent'], docfield['fieldname']) + self.delete_docs_linked_with_specified_company(docfield['parent'], docfield['fieldname']) + + naming_series = frappe.db.get_value('DocType', docfield['parent'], 'autoname') + if naming_series: + if '#' in naming_series: + self.update_naming_series(naming_series, docfield['parent']) + + def get_doctypes_to_be_ignored_list(self): + singles = frappe.get_all('DocType', filters = {'issingle': 1}, pluck = 'name') + doctypes_to_be_ignored_list = singles + for doctype in self.doctypes_to_be_ignored: + doctypes_to_be_ignored_list.append(doctype.doctype_name) + + return doctypes_to_be_ignored_list + + def get_doctypes_with_company_field(self, doctypes_to_be_ignored_list): + docfields = frappe.get_all('DocField', + filters = { + 'fieldtype': 'Link', + 'options': 'Company', + 'parent': ['not in', doctypes_to_be_ignored_list]}, + fields=['parent', 'fieldname']) + + return docfields + + def get_all_child_doctypes(self): + return frappe.get_all('DocType', filters = {'istable': 1}, pluck = 'name') + + def get_number_of_docs_linked_with_specified_company(self, doctype, company_fieldname): + return frappe.db.count(doctype, {company_fieldname : self.company}) + + def populate_doctypes_table(self, tables, doctype, no_of_docs): + if doctype not in tables: + self.append('doctypes', { + 'doctype_name' : doctype, + 'no_of_docs' : no_of_docs + }) + + def delete_child_tables(self, doctype, company_fieldname): + parent_docs_to_be_deleted = frappe.get_all(doctype, { + company_fieldname : self.company + }, pluck = 'name') + + child_tables = frappe.get_all('DocField', filters = { + 'fieldtype': 'Table', + 'parent': doctype + }, pluck = 'options') + + for table in child_tables: + frappe.db.delete(table, { + 'parent': ['in', parent_docs_to_be_deleted] + }) + + def delete_docs_linked_with_specified_company(self, doctype, company_fieldname): + frappe.db.delete(doctype, { + company_fieldname : self.company + }) + def update_naming_series(self, naming_series, doctype_name): if '.' in naming_series: prefix, hashes = naming_series.rsplit('.', 1) @@ -107,32 +171,6 @@ class TransactionDeletionRecord(Document): frappe.delete_doc('Communication', communication_names, ignore_permissions=True) - def delete_bins(self): - frappe.db.sql("""delete from tabBin where warehouse in - (select name from tabWarehouse where company=%s)""", self.company) - - def delete_lead_addresses(self): - """Delete addresses to which leads are linked""" - leads = frappe.get_all('Lead', filters={'company': self.company}) - leads = ["'%s'" % row.get("name") for row in leads] - addresses = [] - if leads: - addresses = frappe.db.sql_list("""select parent from `tabDynamic Link` where link_name - in ({leads})""".format(leads=",".join(leads))) - - if addresses: - addresses = ["%s" % frappe.db.escape(addr) for addr in addresses] - - frappe.db.sql("""delete from tabAddress where name in ({addresses}) and - name not in (select distinct dl1.parent from `tabDynamic Link` dl1 - inner join `tabDynamic Link` dl2 on dl1.parent=dl2.parent - and dl1.link_doctype<>dl2.link_doctype)""".format(addresses=",".join(addresses))) - - frappe.db.sql("""delete from `tabDynamic Link` where link_doctype='Lead' - and parenttype='Address' and link_name in ({leads})""".format(leads=",".join(leads))) - - frappe.db.sql("""update tabCustomer set lead_name=NULL where lead_name in ({leads})""".format(leads=",".join(leads))) - @frappe.whitelist() def get_doctypes_to_be_ignored(): doctypes_to_be_ignored_list = ['Account', 'Cost Center', 'Warehouse', 'Budget', diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record_list.js b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record_list.js index d7175ddac43..c238f18abad 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record_list.js +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record_list.js @@ -9,4 +9,4 @@ frappe.listview_settings['Transaction Deletion Record'] = { return [__("Completed"), "green"]; } } -}; \ No newline at end of file +}; diff --git a/erpnext/setup/doctype/uom/uom.js b/erpnext/setup/doctype/uom/uom.js index 3878a791db6..3680906057f 100644 --- a/erpnext/setup/doctype/uom/uom.js +++ b/erpnext/setup/doctype/uom/uom.js @@ -1,13 +1,13 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt - + //--------- ONLOAD ------------- cur_frm.cscript.onload = function(doc, cdt, cdn) { - + } cur_frm.cscript.refresh = function(doc, cdt, cdn) { - -} \ No newline at end of file + +} diff --git a/erpnext/setup/doctype/uom/uom.py b/erpnext/setup/doctype/uom/uom.py index f7f86d67509..404b84b1134 100644 --- a/erpnext/setup/doctype/uom/uom.py +++ b/erpnext/setup/doctype/uom/uom.py @@ -7,4 +7,4 @@ import frappe from frappe.model.document import Document class UOM(Document): - pass \ No newline at end of file + pass diff --git a/erpnext/setup/doctype/uom_conversion_factor/test_uom_conversion_factor.js b/erpnext/setup/doctype/uom_conversion_factor/test_uom_conversion_factor.js deleted file mode 100644 index afcf74ccb47..00000000000 --- a/erpnext/setup/doctype/uom_conversion_factor/test_uom_conversion_factor.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: UOM Conversion Factor", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new UOM Conversion Factor - () => frappe.tests.make('UOM Conversion Factor', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/setup/doctype/website_item_group/website_item_group.py b/erpnext/setup/doctype/website_item_group/website_item_group.py index 9ac7df2c667..e416b509b98 100644 --- a/erpnext/setup/doctype/website_item_group/website_item_group.py +++ b/erpnext/setup/doctype/website_item_group/website_item_group.py @@ -9,4 +9,4 @@ import frappe from frappe.model.document import Document class WebsiteItemGroup(Document): - pass \ No newline at end of file + pass diff --git a/erpnext/setup/setup_wizard/operations/company_setup.py b/erpnext/setup/setup_wizard/operations/company_setup.py index 4edf9485dc1..4833d93c4a7 100644 --- a/erpnext/setup/setup_wizard/operations/company_setup.py +++ b/erpnext/setup/setup_wizard/operations/company_setup.py @@ -45,9 +45,16 @@ def enable_shopping_cart(args): def create_email_digest(): from frappe.utils.user import get_system_managers system_managers = get_system_managers(only_name=True) + if not system_managers: return + recipients = [] + for d in system_managers: + recipients.append({ + 'recipient': d + }) + companies = frappe.db.sql_list("select name FROM `tabCompany`") for company in companies: if not frappe.db.exists("Email Digest", "Default Weekly Digest - " + company): @@ -56,7 +63,7 @@ def create_email_digest(): "name": "Default Weekly Digest - " + company, "company": company, "frequency": "Weekly", - "recipient_list": "\n".join(system_managers) + "recipients": recipients }) for df in edigest.meta.get("fields", {"fieldtype": "Check"}): @@ -72,7 +79,7 @@ def create_email_digest(): "name": "Scheduler Errors", "company": companies[0], "frequency": "Daily", - "recipient_list": "\n".join(system_managers), + "recipients": recipients, "scheduler_errors": 1, "enabled": 1 }) diff --git a/erpnext/setup/setup_wizard/operations/sample_data.py b/erpnext/setup/setup_wizard/operations/sample_data.py index c11a3885c9b..c6d9f0851b1 100644 --- a/erpnext/setup/setup_wizard/operations/sample_data.py +++ b/erpnext/setup/setup_wizard/operations/sample_data.py @@ -173,4 +173,4 @@ def test_sample(): frappe.db.sql('delete from tabProject') frappe.db.sql('delete from tabTask') make_projects('Education') - import_notification() \ No newline at end of file + import_notification() diff --git a/erpnext/setup/setup_wizard/operations/taxes_setup.py b/erpnext/setup/setup_wizard/operations/taxes_setup.py index c7cc0005186..09b91709642 100644 --- a/erpnext/setup/setup_wizard/operations/taxes_setup.py +++ b/erpnext/setup/setup_wizard/operations/taxes_setup.py @@ -124,7 +124,8 @@ def make_taxes_and_charges_template(company_name, doctype, template): account_data = tax_row.get('account_head') tax_row_defaults = { 'category': 'Total', - 'charge_type': 'On Net Total' + 'charge_type': 'On Net Total', + 'cost_center': frappe.db.get_value('Company', company_name, 'cost_center') } if doctype == 'Purchase Taxes and Charges Template': @@ -144,7 +145,7 @@ def make_taxes_and_charges_template(company_name, doctype, template): doc = frappe.get_doc(template) - # Data in country wise json is already pre validated, hence validations can be ignored + # Data in country wise json is already pre validated, hence validations can be ignored # Ingone validations to make doctypes faster doc.flags.ignore_links = True doc.flags.ignore_validate = True @@ -176,7 +177,7 @@ def make_item_tax_template(company_name, template): doc = frappe.get_doc(template) - # Data in country wise json is already pre validated, hence validations can be ignored + # Data in country wise json is already pre validated, hence validations can be ignored # Ingone validations to make doctypes faster doc.flags.ignore_links = True doc.flags.ignore_validate = True diff --git a/erpnext/setup/utils.py b/erpnext/setup/utils.py index 13269a82823..27237bf2cbe 100644 --- a/erpnext/setup/utils.py +++ b/erpnext/setup/utils.py @@ -93,21 +93,21 @@ def get_exchange_rate(from_currency, to_currency, transaction_date=None, args=No try: cache = frappe.cache() - key = "currency_exchange_rate_{0}:{1}:{2}".format(transaction_date,from_currency, to_currency) + key = "currency_exchange_rate_{0}:{1}:{2}".format(transaction_date, from_currency, to_currency) value = cache.get(key) if not value: import requests - api_url = "https://frankfurter.app/{0}".format(transaction_date) + api_url = "https://api.exchangerate.host/convert" response = requests.get(api_url, params={ - "base": from_currency, - "symbols": to_currency + "date": transaction_date, + "from": from_currency, + "to": to_currency }) # expire in 6 hours response.raise_for_status() - value = response.json()["rates"][to_currency] - - cache.set_value(key, value, expires_in_sec=6 * 60 * 60) + value = response.json()["result"] + cache.setex(name=key, time=21600, value=flt(value)) return flt(value) except: frappe.log_error(title="Get Exchange Rate") diff --git a/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json b/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json index 014f4095c15..6ca3d637da4 100644 --- a/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json +++ b/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json @@ -11,10 +11,11 @@ "hide_custom": 0, "icon": "settings", "idx": 0, + "is_default": 0, "is_standard": 1, "label": "ERPNext Settings", "links": [], - "modified": "2020-12-01 13:38:37.759596", + "modified": "2021-06-12 01:58:11.399566", "modified_by": "Administrator", "module": "Setup", "name": "ERPNext Settings", @@ -109,6 +110,13 @@ "label": "Domain Settings", "link_to": "Domain Settings", "type": "DocType" + }, + { + "doc_view": "", + "icon": "retail", + "label": "Products Settings", + "link_to": "Products Settings", + "type": "DocType" } ] -} \ No newline at end of file +} diff --git a/erpnext/shopping_cart/cart.py b/erpnext/shopping_cart/cart.py index 56afe95efd0..e9f4bd57a6a 100644 --- a/erpnext/shopping_cart/cart.py +++ b/erpnext/shopping_cart/cart.py @@ -308,7 +308,7 @@ def update_party(fullname, company_name=None, mobile_no=None, phone=None): party = get_party() party.customer_name = company_name or fullname - party.customer_type == "Company" if company_name else "Individual" + party.customer_type = "Company" if company_name else "Individual" contact_name = frappe.db.get_value("Contact", {"email_id": frappe.session.user}) contact = frappe.get_doc("Contact", contact_name) diff --git a/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.py b/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.py index c069b90e986..efed1968a14 100644 --- a/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.py +++ b/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.py @@ -6,7 +6,7 @@ from __future__ import unicode_literals import frappe from frappe import _, msgprint -from frappe.utils import comma_and +from frappe.utils import flt from frappe.model.document import Document from frappe.utils import get_datetime, get_datetime_str, now_datetime @@ -18,46 +18,35 @@ class ShoppingCartSettings(Document): def validate(self): if self.enabled: - self.validate_exchange_rates_exist() + self.validate_price_list_exchange_rate() + + def validate_price_list_exchange_rate(self): + "Check if exchange rate exists for Price List currency (to Company's currency)." + from erpnext.setup.utils import get_exchange_rate + + if not self.enabled or not self.company or not self.price_list: + return # this function is also called from hooks, check values again + + company_currency = frappe.get_cached_value("Company", self.company, "default_currency") + price_list_currency = frappe.db.get_value("Price List", self.price_list, "currency") - def validate_exchange_rates_exist(self): - """check if exchange rates exist for all Price List currencies (to company's currency)""" - company_currency = frappe.get_cached_value('Company', self.company, "default_currency") if not company_currency: - msgprint(_("Please specify currency in Company") + ": " + self.company, - raise_exception=ShoppingCartSetupError) + msg = f"Please specify currency in Company {self.company}" + frappe.throw(_(msg), title=_("Missing Currency"), exc=ShoppingCartSetupError) - price_list_currency_map = frappe.db.get_values("Price List", - [self.price_list], "currency") + if not price_list_currency: + msg = f"Please specify currency in Price List {frappe.bold(self.price_list)}" + frappe.throw(_(msg), title=_("Missing Currency"), exc=ShoppingCartSetupError) - price_list_currency_map = dict(price_list_currency_map) - - # check if all price lists have a currency - for price_list, currency in price_list_currency_map.items(): - if not currency: - frappe.throw(_("Currency is required for Price List {0}").format(price_list)) + if price_list_currency != company_currency: + from_currency, to_currency = price_list_currency, company_currency - expected_to_exist = [currency + "-" + company_currency - for currency in price_list_currency_map.values() - if currency != company_currency] + # Get exchange rate checks Currency Exchange Records too + exchange_rate = get_exchange_rate(from_currency, to_currency, args="for_selling") - # manqala 20/09/2016: set up selection parameters for query from tabCurrency Exchange - from_currency = [currency for currency in price_list_currency_map.values() if currency != company_currency] - to_currency = company_currency - # manqala end - - if expected_to_exist: - # manqala 20/09/2016: modify query so that it uses date in the selection from Currency Exchange. - # exchange rates defined with date less than the date on which this document is being saved will be selected - exists = frappe.db.sql_list("""select CONCAT(from_currency,'-',to_currency) from `tabCurrency Exchange` - where from_currency in (%s) and to_currency = "%s" and date <= curdate()""" % (", ".join(["%s"]*len(from_currency)), to_currency), tuple(from_currency)) - # manqala end - - missing = list(set(expected_to_exist).difference(exists)) - - if missing: - msgprint(_("Missing Currency Exchange Rates for {0}").format(comma_and(missing)), - raise_exception=ShoppingCartSetupError) + if not flt(exchange_rate): + msg = f"Missing Currency Exchange Rates for {from_currency}-{to_currency}" + frappe.throw(_(msg), title=_("Missing"), exc=ShoppingCartSetupError) def validate_tax_rule(self): if not frappe.db.get_value("Tax Rule", {"use_for_shopping_cart" : 1}, "name"): @@ -71,7 +60,7 @@ class ShoppingCartSettings(Document): def get_shipping_rules(self, shipping_territory): return self.get_name_from_territory(shipping_territory, "shipping_rules", "shipping_rule") -def validate_cart_settings(doc, method): +def validate_cart_settings(doc=None, method=None): frappe.get_doc("Shopping Cart Settings", "Shopping Cart Settings").run_method("validate") def get_shopping_cart_settings(): diff --git a/erpnext/shopping_cart/doctype/shopping_cart_settings/test_shopping_cart_settings.js b/erpnext/shopping_cart/doctype/shopping_cart_settings/test_shopping_cart_settings.js deleted file mode 100644 index c8485e73fa3..00000000000 --- a/erpnext/shopping_cart/doctype/shopping_cart_settings/test_shopping_cart_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: Shopping Cart Settings", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Shopping Cart Settings - () => frappe.tests.make('Shopping Cart Settings', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/shopping_cart/doctype/shopping_cart_settings/test_shopping_cart_settings.py b/erpnext/shopping_cart/doctype/shopping_cart_settings/test_shopping_cart_settings.py index 75899e121a5..9965e1af672 100644 --- a/erpnext/shopping_cart/doctype/shopping_cart_settings/test_shopping_cart_settings.py +++ b/erpnext/shopping_cart/doctype/shopping_cart_settings/test_shopping_cart_settings.py @@ -16,17 +16,25 @@ class TestShoppingCartSettings(unittest.TestCase): return frappe.get_doc({"doctype": "Shopping Cart Settings", "company": "_Test Company"}) - def test_exchange_rate_exists(self): - frappe.db.sql("""delete from `tabCurrency Exchange`""") + # NOTE: Exchangrate API has all enabled currencies that ERPNext supports. + # We aren't checking just currency exchange record anymore + # while validating price list currency exchange rate to that of company. + # The API is being used to fetch the rate which again almost always + # gives back a valid value (for valid currencies). + # This makes the test obsolete. + # Commenting because im not sure if there's a better test we can write - cart_settings = self.get_cart_settings() - cart_settings.price_list = "_Test Price List Rest of the World" - self.assertRaises(ShoppingCartSetupError, cart_settings.validate_exchange_rates_exist) + # def test_exchange_rate_exists(self): + # frappe.db.sql("""delete from `tabCurrency Exchange`""") - from erpnext.setup.doctype.currency_exchange.test_currency_exchange import test_records as \ - currency_exchange_records - frappe.get_doc(currency_exchange_records[0]).insert() - cart_settings.validate_exchange_rates_exist() + # cart_settings = self.get_cart_settings() + # cart_settings.price_list = "_Test Price List Rest of the World" + # self.assertRaises(ShoppingCartSetupError, cart_settings.validate_price_list_exchange_rate) + + # from erpnext.setup.doctype.currency_exchange.test_currency_exchange import test_records as \ + # currency_exchange_records + # frappe.get_doc(currency_exchange_records[0]).insert() + # cart_settings.validate_price_list_exchange_rate() def test_tax_rule_validation(self): frappe.db.sql("update `tabTax Rule` set use_for_shopping_cart = 0") @@ -36,7 +44,7 @@ class TestShoppingCartSettings(unittest.TestCase): cart_settings.enabled = 1 if not frappe.db.get_value("Tax Rule", {"use_for_shopping_cart": 1}, "name"): self.assertRaises(ShoppingCartSetupError, cart_settings.validate_tax_rule) - + frappe.db.sql("update `tabTax Rule` set use_for_shopping_cart = 1") -test_dependencies = ["Tax Rule"] \ No newline at end of file +test_dependencies = ["Tax Rule"] diff --git a/erpnext/shopping_cart/product_info.py b/erpnext/shopping_cart/product_info.py index 29617a87485..6c9e531a4d1 100644 --- a/erpnext/shopping_cart/product_info.py +++ b/erpnext/shopping_cart/product_info.py @@ -66,4 +66,4 @@ def set_product_info_for_website(item): item["price_sales_uom"] = product_info.get("price").get("formatted_price_sales_uom") else: item["price_stock_uom"] = "" - item["price_sales_uom"] = "" \ No newline at end of file + item["price_sales_uom"] = "" diff --git a/erpnext/shopping_cart/search.py b/erpnext/shopping_cart/search.py index 63e9fe1b31b..9f674dcebf8 100644 --- a/erpnext/shopping_cart/search.py +++ b/erpnext/shopping_cart/search.py @@ -123,4 +123,4 @@ def remove_document_from_index(path): def build_index_for_all_routes(): search = ProductSearch(INDEX_NAME) - return search.build() \ No newline at end of file + return search.build() diff --git a/erpnext/shopping_cart/utils.py b/erpnext/shopping_cart/utils.py index 3241234af5f..0e1466fd1fa 100644 --- a/erpnext/shopping_cart/utils.py +++ b/erpnext/shopping_cart/utils.py @@ -38,4 +38,4 @@ def check_customer_or_supplier(): if link.link_doctype in ('Customer', 'Supplier'): return link.link_doctype, link.link_name - return 'Customer', None \ No newline at end of file + return 'Customer', None diff --git a/erpnext/shopping_cart/web_template/hero_slider/hero_slider.html b/erpnext/shopping_cart/web_template/hero_slider/hero_slider.html index 1b3953435e4..1e3d0d069a1 100644 --- a/erpnext/shopping_cart/web_template/hero_slider/hero_slider.html +++ b/erpnext/shopping_cart/web_template/hero_slider/hero_slider.html @@ -82,4 +82,4 @@ \ No newline at end of file + diff --git a/erpnext/shopping_cart/web_template/item_card_group/item_card_group.html b/erpnext/shopping_cart/web_template/item_card_group/item_card_group.html index 890ae502c82..fe061d5f5f5 100644 --- a/erpnext/shopping_cart/web_template/item_card_group/item_card_group.html +++ b/erpnext/shopping_cart/web_template/item_card_group/item_card_group.html @@ -35,4 +35,4 @@
          \ No newline at end of file + diff --git a/erpnext/startup/filters.py b/erpnext/startup/filters.py index ec07329dedf..98210165dfa 100644 --- a/erpnext/startup/filters.py +++ b/erpnext/startup/filters.py @@ -11,4 +11,4 @@ def get_filters_config(): } } - return filters_config \ No newline at end of file + return filters_config diff --git a/erpnext/startup/leaderboard.py b/erpnext/startup/leaderboard.py index 8819a55c0ab..a89435d4866 100644 --- a/erpnext/startup/leaderboard.py +++ b/erpnext/startup/leaderboard.py @@ -202,4 +202,4 @@ def get_date_condition(date_range, field): date_condition = "and {0} between {1} and {2}".format( field, frappe.db.escape(from_date), frappe.db.escape(to_date) ) - return date_condition \ No newline at end of file + return date_condition diff --git a/erpnext/stock/dashboard/item_dashboard.html b/erpnext/stock/dashboard/item_dashboard.html index 1e18969e63e..99698ba69a9 100644 --- a/erpnext/stock/dashboard/item_dashboard.html +++ b/erpnext/stock/dashboard/item_dashboard.html @@ -4,4 +4,4 @@ -
          \ No newline at end of file + diff --git a/erpnext/stock/dashboard/warehouse_capacity_dashboard.py b/erpnext/stock/dashboard/warehouse_capacity_dashboard.py index ab573e566ac..70b030e48f2 100644 --- a/erpnext/stock/dashboard/warehouse_capacity_dashboard.py +++ b/erpnext/stock/dashboard/warehouse_capacity_dashboard.py @@ -66,4 +66,4 @@ def get_warehouse_capacity_data(filters, start): 'percent_occupied': flt((flt(balance_qty) / flt(entry.stock_capacity)) * 100, 0) }) - return capacity_data \ No newline at end of file + return capacity_data diff --git a/erpnext/stock/dashboard_chart_source/warehouse_wise_stock_value/warehouse_wise_stock_value.js b/erpnext/stock/dashboard_chart_source/warehouse_wise_stock_value/warehouse_wise_stock_value.js index a4137547f7e..2b9d46e4ab0 100644 --- a/erpnext/stock/dashboard_chart_source/warehouse_wise_stock_value/warehouse_wise_stock_value.js +++ b/erpnext/stock/dashboard_chart_source/warehouse_wise_stock_value/warehouse_wise_stock_value.js @@ -11,4 +11,4 @@ frappe.dashboards.chart_sources["Warehouse wise Stock Value"] = { default: frappe.defaults.get_user_default("Company") } ] -}; \ No newline at end of file +}; diff --git a/erpnext/stock/dashboard_chart_source/warehouse_wise_stock_value/warehouse_wise_stock_value.py b/erpnext/stock/dashboard_chart_source/warehouse_wise_stock_value/warehouse_wise_stock_value.py index 374a34ea7ca..2258532c6fe 100644 --- a/erpnext/stock/dashboard_chart_source/warehouse_wise_stock_value/warehouse_wise_stock_value.py +++ b/erpnext/stock/dashboard_chart_source/warehouse_wise_stock_value/warehouse_wise_stock_value.py @@ -45,4 +45,4 @@ def get(chart_name = None, chart = None, no_cache = None, filters = None, from_d "values": datapoints }], "type": "bar" - } \ No newline at end of file + } diff --git a/erpnext/stock/doctype/batch/batch.json b/erpnext/stock/doctype/batch/batch.json index e6d2e1330b5..fc4cf1dbdb8 100644 --- a/erpnext/stock/doctype/batch/batch.json +++ b/erpnext/stock/doctype/batch/batch.json @@ -193,7 +193,7 @@ "image_field": "image", "links": [], "max_attachments": 5, - "modified": "2021-01-07 11:10:09.149170", + "modified": "2021-07-08 16:22:01.343105", "modified_by": "Administrator", "module": "Stock", "name": "Batch", @@ -217,5 +217,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", - "title_field": "batch_id" + "title_field": "batch_id", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index b6eef6ca484..b37ae3f4f61 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -162,19 +162,19 @@ def get_batch_qty(batch_no=None, warehouse=None, item_code=None, posting_date=No out = float(frappe.db.sql("""select sum(actual_qty) from `tabStock Ledger Entry` - where warehouse=%s and batch_no=%s {0}""".format(cond), + where is_cancelled = 0 and warehouse=%s and batch_no=%s {0}""".format(cond), (warehouse, batch_no))[0][0] or 0) if batch_no and not warehouse: out = frappe.db.sql('''select warehouse, sum(actual_qty) as qty from `tabStock Ledger Entry` - where batch_no=%s + where is_cancelled = 0 and batch_no=%s group by warehouse''', batch_no, as_dict=1) if not batch_no and item_code and warehouse: out = frappe.db.sql('''select batch_no, sum(actual_qty) as qty from `tabStock Ledger Entry` - where item_code = %s and warehouse=%s + where is_cancelled = 0 and item_code = %s and warehouse=%s group by batch_no''', (item_code, warehouse), as_dict=1) return out diff --git a/erpnext/stock/doctype/batch/test_batch.js b/erpnext/stock/doctype/batch/test_batch.js index af7f50ff91e..2d2150b8acd 100644 --- a/erpnext/stock/doctype/batch/test_batch.js +++ b/erpnext/stock/doctype/batch/test_batch.js @@ -20,4 +20,3 @@ QUnit.test("test Batch", function(assert) { () => done() ]); }); - diff --git a/erpnext/stock/doctype/batch/test_batch.py b/erpnext/stock/doctype/batch/test_batch.py index cbd272df4b8..a85a0222b55 100644 --- a/erpnext/stock/doctype/batch/test_batch.py +++ b/erpnext/stock/doctype/batch/test_batch.py @@ -269,11 +269,14 @@ class TestBatch(unittest.TestCase): batch2 = create_batch('_Test Batch Price Item', 300, 1) batch3 = create_batch('_Test Batch Price Item', 400, 0) + company = "_Test Company with perpetual inventory" + currency = frappe.get_cached_value("Company", company, "default_currency") + args = frappe._dict({ "item_code": "_Test Batch Price Item", - "company": "_Test Company with perpetual inventory", + "company": company, "price_list": "_Test Price List", - "currency": "_Test Currency", + "currency": currency, "doctype": "Sales Invoice", "conversion_rate": 1, "price_list_currency": "_Test Currency", @@ -333,4 +336,4 @@ def make_new_batch(**args): except frappe.DuplicateEntryError: batch = frappe.get_doc("Batch", args.batch_id) - return batch \ No newline at end of file + return batch diff --git a/erpnext/stock/doctype/customs_tariff_number/test_customs_tariff_number.js b/erpnext/stock/doctype/customs_tariff_number/test_customs_tariff_number.js deleted file mode 100644 index 85812d69739..00000000000 --- a/erpnext/stock/doctype/customs_tariff_number/test_customs_tariff_number.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: Customs Tariff Number", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Customs Tariff Number - () => frappe.tests.make('Customs Tariff Number', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js index 74cb3fcb1f0..8632c9c1085 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note.js @@ -356,3 +356,23 @@ erpnext.stock.delivery_note.set_print_hide = function(doc, cdt, cdn){ dn_fields['taxes'].print_hide = 0; } } + + +frappe.tour['Delivery Note'] = [ + { + fieldname: "customer", + title: __("Customer"), + description: __("This field is used to set the 'Customer'.") + }, + { + fieldname: "items", + title: __("Items"), + description: __("This table is used to set details about the 'Item', 'Qty', 'Basic Rate', etc.") + " " + + __("Different 'Source Warehouse' and 'Target Warehouse' can be set for each row.") + }, + { + fieldname: "set_posting_time", + title: __("Edit Posting Date and Time"), + description: __("This option can be checked to edit the 'Posting Date' and 'Posting Time' fields.") + } +] diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json index f20e76f5bf6..958189614fd 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.json +++ b/erpnext/stock/doctype/delivery_note/delivery_note.json @@ -32,6 +32,8 @@ "contact_info", "shipping_address_name", "shipping_address", + "dispatch_address_name", + "dispatch_address", "contact_person", "contact_display", "contact_mobile", @@ -513,7 +515,8 @@ { "fieldname": "scan_barcode", "fieldtype": "Data", - "label": "Scan Barcode" + "label": "Scan Barcode", + "options": "Barcode" }, { "allow_bulk_edit": 1, @@ -1282,13 +1285,28 @@ "fieldname": "disable_rounded_total", "fieldtype": "Check", "label": "Disable Rounded Total" + }, + { + "fieldname": "dispatch_address_name", + "fieldtype": "Link", + "label": "Dispatch Address Name", + "options": "Address", + "print_hide": 1 + }, + { + "depends_on": "dispatch_address_name", + "fieldname": "dispatch_address", + "fieldtype": "Small Text", + "label": "Dispatch Address", + "print_hide": 1, + "read_only": 1 } ], "icon": "fa fa-truck", "idx": 146, "is_submittable": 1, "links": [], - "modified": "2021-06-11 19:27:30.901112", + "modified": "2021-08-17 20:15:50.574966", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note", diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 4808e948fc6..f99a01b8202 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -503,6 +503,10 @@ def make_sales_invoice(source_name, target_doc=None): } }, target_doc, set_missing_values) + automatically_fetch_payment_terms = cint(frappe.db.get_single_value('Accounts Settings', 'automatically_fetch_payment_terms')) + if automatically_fetch_payment_terms: + doc.set_payment_schedule() + return doc @frappe.whitelist() diff --git a/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py b/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py index 47684d5c6ec..9db5db865f5 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py @@ -30,4 +30,4 @@ def get_data(): 'items': ['Auto Repeat'] }, ] - } \ No newline at end of file + } diff --git a/erpnext/stock/doctype/delivery_note/regional/india.js b/erpnext/stock/doctype/delivery_note/regional/india.js index 5e1ff980009..e853858b60b 100644 --- a/erpnext/stock/doctype/delivery_note/regional/india.js +++ b/erpnext/stock/doctype/delivery_note/regional/india.js @@ -27,4 +27,3 @@ frappe.ui.form.on('Delivery Note', { } } }) - diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.js b/erpnext/stock/doctype/delivery_note/test_delivery_note.js index 3f6e8d15035..76f79894290 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.js +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.js @@ -33,4 +33,3 @@ QUnit.test("test delivery note", function(assert) { () => done() ]); }); - diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index f981aeb13bb..b333a6b57ea 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -17,7 +17,8 @@ from erpnext.stock.doctype.stock_entry.test_stock_entry \ from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos, SerialNoWarehouseError from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation \ import create_stock_reconciliation, set_valuation_method -from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order, create_dn_against_so +from erpnext.selling.doctype.sales_order.test_sales_order \ + import make_sales_order, create_dn_against_so, automatically_fetch_payment_terms, compare_payment_schedules from erpnext.accounts.doctype.account.test_account import get_inventory_account from erpnext.stock.doctype.warehouse.test_warehouse import get_warehouse from erpnext.stock.doctype.item.test_item import make_item @@ -429,12 +430,19 @@ class TestDeliveryNote(unittest.TestCase): }) def test_delivery_of_bundled_items_to_target_warehouse(self): + from erpnext.selling.doctype.customer.test_customer import create_internal_customer + company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company') + customer_name = create_internal_customer( + customer_name="_Test Internal Customer 2", + represents_company="_Test Company with perpetual inventory", + allowed_to_interact_with="_Test Company with perpetual inventory" + ) set_valuation_method("_Test Item", "FIFO") set_valuation_method("_Test Item Home Desktop 100", "FIFO") - target_warehouse=get_warehouse(company=company, abbr="TCP1", + target_warehouse = get_warehouse(company=company, abbr="TCP1", warehouse_name="_Test Customer Warehouse").name for warehouse in ("Stores - TCP1", target_warehouse): @@ -443,10 +451,16 @@ class TestDeliveryNote(unittest.TestCase): create_stock_reconciliation(item_code="_Test Item Home Desktop 100", company = company, expense_account = "Stock Adjustment - TCP1", warehouse=warehouse, qty=500, rate=100) - dn = create_delivery_note(item_code="_Test Product Bundle Item", - company='_Test Company with perpetual inventory', cost_center = 'Main - TCP1', - expense_account = "Cost of Goods Sold - TCP1", do_not_submit=True, qty=5, rate=500, - warehouse="Stores - TCP1", target_warehouse=target_warehouse) + dn = create_delivery_note( + item_code="_Test Product Bundle Item", + company="_Test Company with perpetual inventory", + customer=customer_name, + cost_center = 'Main - TCP1', + expense_account = "Cost of Goods Sold - TCP1", + do_not_submit=True, + qty=5, rate=500, + warehouse="Stores - TCP1", + target_warehouse=target_warehouse) dn.submit() @@ -486,6 +500,9 @@ class TestDeliveryNote(unittest.TestCase): for i, gle in enumerate(gl_entries): self.assertEqual([gle.debit, gle.credit], expected_values.get(gle.account)) + # tear down + frappe.db.rollback() + def test_closed_delivery_note(self): from erpnext.stock.doctype.delivery_note.delivery_note import update_delivery_note_status @@ -759,6 +776,32 @@ class TestDeliveryNote(unittest.TestCase): self.assertTrue("TESTBATCH" in dn.packed_items[0].batch_no, "Batch number not added in packed item") + def test_payment_terms_are_fetched_when_creating_sales_invoice(self): + from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_terms_template + from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice + + automatically_fetch_payment_terms() + + so = make_sales_order(uom="Nos", do_not_save=1) + create_payment_terms_template() + so.payment_terms_template = 'Test Receivable Template' + so.submit() + + dn = create_dn_against_so(so.name, delivered_qty=10) + + si = create_sales_invoice(qty=10, do_not_save=1) + si.items[0].delivery_note= dn.name + si.items[0].dn_detail = dn.items[0].name + si.items[0].sales_order = so.name + si.items[0].so_detail = so.items[0].name + + si.insert() + si.submit() + + self.assertEqual(so.payment_terms_template, si.payment_terms_template) + compare_payment_schedules(self, so, si) + + automatically_fetch_payment_terms(enable=0) def create_delivery_note(**args): dn = frappe.new_doc("Delivery Note") diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note_with_margin.js b/erpnext/stock/doctype/delivery_note/test_delivery_note_with_margin.js index 21eb35ce378..9f1375f563c 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note_with_margin.js +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note_with_margin.js @@ -34,4 +34,3 @@ QUnit.test("test delivery note with margin", function(assert) { () => done() ]); }); - diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.py b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.py index 50305957892..8bd381a2ed3 100644 --- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.py +++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.py @@ -7,4 +7,4 @@ import frappe from frappe.model.document import Document class DeliveryNoteItem(Document): - pass \ No newline at end of file + pass diff --git a/erpnext/stock/doctype/delivery_settings/test_delivery_settings.js b/erpnext/stock/doctype/delivery_settings/test_delivery_settings.js deleted file mode 100644 index 22977c08f75..00000000000 --- a/erpnext/stock/doctype/delivery_settings/test_delivery_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: Delivery Settings", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Delivery Settings - () => frappe.tests.make('Delivery Settings', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/stock/doctype/delivery_trip/delivery_trip.py b/erpnext/stock/doctype/delivery_trip/delivery_trip.py index 9ec28d89814..f76bb87efed 100644 --- a/erpnext/stock/doctype/delivery_trip/delivery_trip.py +++ b/erpnext/stock/doctype/delivery_trip/delivery_trip.py @@ -406,4 +406,4 @@ def make_expense_claim(source_name, target_doc=None): } }}, target_doc) - return doc \ No newline at end of file + return doc diff --git a/erpnext/stock/doctype/delivery_trip/dispatch_notification_template.html b/erpnext/stock/doctype/delivery_trip/dispatch_notification_template.html index 9c062bc34ca..d12334e355a 100644 --- a/erpnext/stock/doctype/delivery_trip/dispatch_notification_template.html +++ b/erpnext/stock/doctype/delivery_trip/dispatch_notification_template.html @@ -47,4 +47,4 @@ {{ vehicle }} - \ No newline at end of file + diff --git a/erpnext/stock/doctype/delivery_trip/test_delivery_trip.js b/erpnext/stock/doctype/delivery_trip/test_delivery_trip.js deleted file mode 100644 index b6d6d1af64f..00000000000 --- a/erpnext/stock/doctype/delivery_trip/test_delivery_trip.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: Delivery Trip", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Delivery Trip - () => frappe.tests.make('Delivery Trip', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index 8aec89381a1..2bf749a8152 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -93,17 +93,18 @@ frappe.ui.form.on("Item", { erpnext.item.edit_prices_button(frm); erpnext.item.toggle_attributes(frm); - + if (!frm.doc.is_fixed_asset) { erpnext.item.make_dashboard(frm); } frm.add_custom_button(__('Duplicate'), function() { var new_item = frappe.model.copy_doc(frm.doc); - if(new_item.item_name===new_item.item_code) { + // Duplicate item could have different name, causing "copy paste" error. + if (new_item.item_name===new_item.item_code) { new_item.item_name = null; } - if(new_item.description===new_item.description) { + if (new_item.item_code===new_item.description || new_item.item_code===new_item.description) { new_item.description = null; } frappe.set_route('Form', 'Item', new_item.name); @@ -137,26 +138,11 @@ frappe.ui.form.on("Item", { frm.toggle_reqd('customer', frm.doc.is_customer_provided_item ? 1:0); }, - gst_hsn_code: function(frm) { - if((!frm.doc.taxes || !frm.doc.taxes.length) && frm.doc.gst_hsn_code) { - frappe.db.get_doc("GST HSN Code", frm.doc.gst_hsn_code).then(hsn_doc => { - $.each(hsn_doc.taxes || [], function(i, tax) { - let a = frappe.model.add_child(cur_frm.doc, 'Item Tax', 'taxes'); - a.item_tax_template = tax.item_tax_template; - a.tax_category = tax.tax_category; - a.valid_from = tax.valid_from; - frm.refresh_field('taxes'); - }); - }); - } - }, - is_fixed_asset: function(frm) { // set serial no to false & toggles its visibility frm.set_value('has_serial_no', 0); + frm.set_value('has_batch_no', 0); frm.toggle_enable(['has_serial_no', 'serial_no_series'], !frm.doc.is_fixed_asset); - frm.toggle_reqd(['asset_category'], frm.doc.is_fixed_asset); - frm.toggle_display(['has_serial_no', 'serial_no_series'], !frm.doc.is_fixed_asset); frm.call({ method: "set_asset_naming_series", @@ -186,8 +172,6 @@ frappe.ui.form.on("Item", { item_code: function(frm) { if(!frm.doc.item_name) frm.set_value("item_name", frm.doc.item_code); - if(!frm.doc.description) - frm.set_value("description", frm.doc.item_code); }, is_stock_item: function(frm) { @@ -275,6 +259,17 @@ $.extend(erpnext.item, { } } + frm.fields_dict["item_defaults"].grid.get_field("default_discount_account").get_query = function(doc, cdt, cdn) { + const row = locals[cdt][cdn]; + return { + filters: { + 'report_type': 'Profit and Loss', + 'company': row.company, + "is_group": 0 + } + }; + }; + frm.fields_dict["item_defaults"].grid.get_field("buying_cost_center").get_query = function(doc, cdt, cdn) { const row = locals[cdt][cdn]; return { @@ -381,7 +376,8 @@ $.extend(erpnext.item, { // Show Stock Levels only if is_stock_item if (frm.doc.is_stock_item) { frappe.require('assets/js/item-dashboard.min.js', function() { - const section = frm.dashboard.add_section('', __("Stock Levels")); + frm.dashboard.parent.find('.stock-levels').remove(); + const section = frm.dashboard.add_section('', __("Stock Levels"), 'stock-levels'); erpnext.item.item_dashboard = new erpnext.stock.ItemDashboard({ parent: section, item_code: frm.doc.name, @@ -795,4 +791,4 @@ frappe.ui.form.on("UOM Conversion Detail", { }); } } -}) +}); diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index 6fed9efa63f..db5caf91645 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -204,6 +204,7 @@ }, { "default": "0", + "depends_on": "eval:!doc.is_fixed_asset", "fieldname": "is_item_from_hub", "fieldtype": "Check", "label": "Is Item from Hub", @@ -238,6 +239,7 @@ { "bold": 1, "default": "1", + "depends_on": "eval:!doc.is_fixed_asset", "fieldname": "is_stock_item", "fieldtype": "Check", "label": "Maintain Stock", @@ -246,6 +248,7 @@ }, { "default": "1", + "depends_on": "eval:!doc.is_fixed_asset", "fieldname": "include_item_in_manufacturing", "fieldtype": "Check", "label": "Include Item In Manufacturing" @@ -282,6 +285,7 @@ "fieldname": "asset_category", "fieldtype": "Link", "label": "Asset Category", + "mandatory_depends_on": "is_fixed_asset", "options": "Asset Category" }, { @@ -434,8 +438,8 @@ }, { "collapsible": 1, - "collapsible_depends_on": "eval:doc.has_batch_no || doc.has_serial_no || doc.is_fixed_asset", - "depends_on": "eval:doc.is_stock_item || doc.is_fixed_asset", + "collapsible_depends_on": "eval:doc.has_batch_no || doc.has_serial_no", + "depends_on": "eval:doc.is_stock_item", "fieldname": "serial_nos_and_batches", "fieldtype": "Section Break", "label": "Serial Nos and Batches" @@ -492,7 +496,7 @@ }, { "default": "0", - "depends_on": "eval:doc.is_stock_item || doc.is_fixed_asset", + "depends_on": "eval:doc.is_stock_item", "fieldname": "has_serial_no", "fieldtype": "Check", "label": "Has Serial No", @@ -510,6 +514,7 @@ { "collapsible": 1, "collapsible_depends_on": "attributes", + "depends_on": "eval:!doc.is_fixed_asset", "fieldname": "variants_section", "fieldtype": "Section Break", "label": "Variants" @@ -540,6 +545,7 @@ "options": "Item Variant Attribute" }, { + "depends_on": "eval:!doc.is_fixed_asset", "fieldname": "defaults", "fieldtype": "Section Break", "label": "Sales, Purchase, Accounting Defaults" @@ -621,6 +627,7 @@ }, { "collapsible": 1, + "depends_on": "eval:!doc.is_fixed_asset", "fieldname": "supplier_details", "fieldtype": "Section Break", "label": "Supplier Details" @@ -668,6 +675,7 @@ }, { "collapsible": 1, + "default": "eval:!doc.is_fixed_asset", "fieldname": "sales_details", "fieldtype": "Section Break", "label": "Sales Details", @@ -761,6 +769,7 @@ }, { "collapsible": 1, + "depends_on": "eval:!doc.is_fixed_asset", "fieldname": "customer_details", "fieldtype": "Section Break", "label": "Customer Details" @@ -791,6 +800,7 @@ }, { "collapsible": 1, + "depends_on": "eval:!doc.is_fixed_asset", "fieldname": "inspection_criteria", "fieldtype": "Section Break", "label": "Inspection Criteria", @@ -861,6 +871,7 @@ }, { "collapsible": 1, + "depends_on": "eval:!doc.is_fixed_asset", "fieldname": "website_section", "fieldtype": "Section Break", "label": "Website", @@ -987,7 +998,7 @@ }, { "collapsible": 1, - "depends_on": "eval:(!doc.is_item_from_hub)", + "depends_on": "eval:(!doc.is_item_from_hub && !doc.is_fixed_asset)", "fieldname": "hub_publishing_sb", "fieldtype": "Section Break", "label": "Hub Publishing Details" @@ -1021,7 +1032,7 @@ "read_only": 1 }, { - "depends_on": "eval:!doc.__islocal", + "depends_on": "eval:!doc.__islocal && !doc.is_fixed_asset", "fieldname": "over_delivery_receipt_allowance", "fieldtype": "Float", "label": "Over Delivery/Receipt Allowance (%)", @@ -1029,7 +1040,7 @@ "oldfieldtype": "Currency" }, { - "depends_on": "eval:!doc.__islocal", + "depends_on": "eval:!doc.__islocal && !doc.is_fixed_asset", "fieldname": "over_billing_allowance", "fieldtype": "Float", "label": "Over Billing Allowance (%)" @@ -1067,7 +1078,7 @@ "index_web_pages_for_search": 1, "links": [], "max_attachments": 1, - "modified": "2021-03-18 14:04:38.575519", + "modified": "2021-08-26 12:23:07.277077", "modified_by": "Administrator", "module": "Stock", "name": "Item", @@ -1138,4 +1149,4 @@ "sort_order": "DESC", "title_field": "item_name", "track_changes": 1 -} +} \ No newline at end of file diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index fbd30cf6c78..ca587bfe82c 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -123,6 +123,7 @@ class Item(WebsiteGenerator): self.cant_change() self.update_show_in_website() self.validate_item_tax_net_rate_range() + set_item_tax_from_hsn_code(self) if not self.is_new(): self.old_item_group = frappe.db.get_value(self.doctype, self.name, "item_group") @@ -1305,3 +1306,7 @@ def update_variants(variants, template, publish_progress=True): def on_doctype_update(): # since route is a Text column, it needs a length for indexing frappe.db.add_index("Item", ["route(500)"]) + +@erpnext.allow_regional +def set_item_tax_from_hsn_code(item): + pass diff --git a/erpnext/stock/doctype/item/regional/india.js b/erpnext/stock/doctype/item/regional/india.js new file mode 100644 index 00000000000..cceb1ec895f --- /dev/null +++ b/erpnext/stock/doctype/item/regional/india.js @@ -0,0 +1,15 @@ +frappe.ui.form.on('Item', { + gst_hsn_code: function(frm) { + if ((!frm.doc.taxes || !frm.doc.taxes.length) && frm.doc.gst_hsn_code) { + frappe.db.get_doc("GST HSN Code", frm.doc.gst_hsn_code).then(hsn_doc => { + $.each(hsn_doc.taxes || [], function(i, tax) { + let a = frappe.model.add_child(cur_frm.doc, 'Item Tax', 'taxes'); + a.item_tax_template = tax.item_tax_template; + a.tax_category = tax.tax_category; + a.valid_from = tax.valid_from; + frm.refresh_field('taxes'); + }); + }); + } + }, +}); diff --git a/erpnext/stock/doctype/item/templates/item.html b/erpnext/stock/doctype/item/templates/item.html index db123090aae..5c42f3b1247 100644 --- a/erpnext/stock/doctype/item/templates/item.html +++ b/erpnext/stock/doctype/item/templates/item.html @@ -4,4 +4,4 @@

          {{ title }}

          {% endblock %} - \ No newline at end of file + diff --git a/erpnext/stock/doctype/item/templates/item_row.html b/erpnext/stock/doctype/item/templates/item_row.html index 2b999819cbb..f81fc1d8743 100644 --- a/erpnext/stock/doctype/item/templates/item_row.html +++ b/erpnext/stock/doctype/item/templates/item_row.html @@ -1,4 +1,4 @@
          {{ title }}
          - \ No newline at end of file + diff --git a/erpnext/stock/doctype/item/test_item.js b/erpnext/stock/doctype/item/test_item.js deleted file mode 100644 index af44278a59a..00000000000 --- a/erpnext/stock/doctype/item/test_item.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", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Item - () => frappe.tests.make('Item', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py index c7467a5a0f5..7a9985d7f07 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -83,14 +83,17 @@ class TestItem(unittest.TestCase): make_test_objects("Item Price") + company = "_Test Company" + currency = frappe.get_cached_value("Company", company, "default_currency") + details = get_item_details({ "item_code": "_Test Item", - "company": "_Test Company", + "company": company, "price_list": "_Test Price List", - "currency": "_Test Currency", + "currency": currency, "doctype": "Sales Order", "conversion_rate": 1, - "price_list_currency": "_Test Currency", + "price_list_currency": currency, "plc_conversion_rate": 1, "order_type": "Sales", "customer": "_Test Customer", @@ -587,8 +590,8 @@ def make_item_variant(): test_records = frappe.get_test_records('Item') def create_item(item_code, is_stock_item=1, valuation_rate=0, warehouse="_Test Warehouse - _TC", - is_customer_provided_item=None, customer=None, is_purchase_item=None, opening_stock=0, - company="_Test Company"): + is_customer_provided_item=None, customer=None, is_purchase_item=None, opening_stock=0, is_fixed_asset=0, + asset_category=None, company="_Test Company"): if not frappe.db.exists("Item", item_code): item = frappe.new_doc("Item") item.item_code = item_code @@ -596,6 +599,8 @@ def create_item(item_code, is_stock_item=1, valuation_rate=0, warehouse="_Test W item.description = item_code item.item_group = "All Item Groups" item.is_stock_item = is_stock_item + item.is_fixed_asset = is_fixed_asset + item.asset_category = asset_category item.opening_stock = opening_stock item.valuation_rate = valuation_rate item.is_purchase_item = is_purchase_item diff --git a/erpnext/stock/doctype/item/tests/test_item.js b/erpnext/stock/doctype/item/tests/test_item.js index 5e3524e5b6d..7f7e72d5c0f 100644 --- a/erpnext/stock/doctype/item/tests/test_item.js +++ b/erpnext/stock/doctype/item/tests/test_item.js @@ -118,4 +118,4 @@ QUnit.test("test: item", function (assert) { ), () => done() ]); -}); \ No newline at end of file +}); diff --git a/erpnext/stock/doctype/item_alternative/test_item_alternative.js b/erpnext/stock/doctype/item_alternative/test_item_alternative.js deleted file mode 100644 index 87318499fe4..00000000000 --- a/erpnext/stock/doctype/item_alternative/test_item_alternative.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 Alternative", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Item Alternative - () => frappe.tests.make('Item Alternative', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/stock/doctype/item_attribute/test_item_attribute.py b/erpnext/stock/doctype/item_attribute/test_item_attribute.py index 61e53d24a46..07af176a944 100644 --- a/erpnext/stock/doctype/item_attribute/test_item_attribute.py +++ b/erpnext/stock/doctype/item_attribute/test_item_attribute.py @@ -28,4 +28,3 @@ class TestItemAttribute(unittest.TestCase): item_attribute.increment = 0.5 item_attribute.save() - diff --git a/erpnext/stock/doctype/item_customer_detail/item_customer_detail.py b/erpnext/stock/doctype/item_customer_detail/item_customer_detail.py index a9183ce5866..3e4e8500467 100644 --- a/erpnext/stock/doctype/item_customer_detail/item_customer_detail.py +++ b/erpnext/stock/doctype/item_customer_detail/item_customer_detail.py @@ -7,4 +7,4 @@ import frappe from frappe.model.document import Document class ItemCustomerDetail(Document): - pass \ No newline at end of file + pass diff --git a/erpnext/stock/doctype/item_default/item_default.json b/erpnext/stock/doctype/item_default/item_default.json index 96b5dfdc8f7..bc171604f43 100644 --- a/erpnext/stock/doctype/item_default/item_default.json +++ b/erpnext/stock/doctype/item_default/item_default.json @@ -1,464 +1,118 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2018-05-03 02:29:24.444341", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "creation": "2018-05-03 02:29:24.444341", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "company", + "default_warehouse", + "column_break_3", + "default_price_list", + "default_discount_account", + "purchase_defaults", + "buying_cost_center", + "default_supplier", + "column_break_8", + "expense_account", + "selling_defaults", + "selling_cost_center", + "column_break_12", + "income_account" + ], "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": 1, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Company", - "length": 0, - "no_copy": 0, - "options": "Company", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "company", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "in_list_view": 1, + "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": "default_warehouse", - "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": "Default Warehouse", - "length": 0, - "no_copy": 0, - "options": "Warehouse", - "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": "default_warehouse", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Default Warehouse", + "options": "Warehouse" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_3", - "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, - "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_3", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "default_price_list", - "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": "Default Price List", - "length": 0, - "no_copy": 0, - "options": "Price List", - "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": "default_price_list", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Default Price List", + "options": "Price List" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "purchase_defaults", - "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": "Purchase Defaults", - "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": "purchase_defaults", + "fieldtype": "Section Break", + "label": "Purchase Defaults" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "buying_cost_center", - "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": "Default Buying Cost Center", - "length": 0, - "no_copy": 0, - "options": "Cost Center", - "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": "buying_cost_center", + "fieldtype": "Link", + "label": "Default Buying Cost Center", + "options": "Cost Center" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "default_supplier", - "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": "Default Supplier", - "length": 0, - "no_copy": 0, - "options": "Supplier", - "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": "default_supplier", + "fieldtype": "Link", + "label": "Default Supplier", + "options": "Supplier" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_8", - "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, - "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_8", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "expense_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": "Default Expense 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": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "expense_account", + "fieldtype": "Link", + "label": "Default Expense Account", + "options": "Account" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "selling_defaults", - "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": "Sales Defaults", - "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": "selling_defaults", + "fieldtype": "Section Break", + "label": "Sales Defaults" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "selling_cost_center", - "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": "Default Selling Cost Center", - "length": 0, - "no_copy": 0, - "options": "Cost Center", - "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": "selling_cost_center", + "fieldtype": "Link", + "label": "Default Selling Cost Center", + "options": "Cost Center" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_12", - "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, - "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_12", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "income_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": "Default Income 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": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldname": "income_account", + "fieldtype": "Link", + "label": "Default Income Account", + "options": "Account" + }, + { + "fieldname": "default_discount_account", + "fieldtype": "Link", + "label": "Default Discount Account", + "options": "Account" } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2018-12-07 11:48:07.638935", - "modified_by": "Administrator", - "module": "Stock", - "name": "Item Default", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "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 + ], + "istable": 1, + "links": [], + "modified": "2021-07-13 01:26:03.860065", + "modified_by": "Administrator", + "module": "Stock", + "name": "Item Default", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/stock/doctype/item_manufacturer/item_manufacturer.py b/erpnext/stock/doctype/item_manufacturer/item_manufacturer.py index c27d1be7892..939abf8d324 100644 --- a/erpnext/stock/doctype/item_manufacturer/item_manufacturer.py +++ b/erpnext/stock/doctype/item_manufacturer/item_manufacturer.py @@ -65,4 +65,4 @@ class ItemManufacturer(Document): @frappe.whitelist() def get_item_manufacturer_part_no(item_code, manufacturer): return frappe.db.get_value("Item Manufacturer", - {'item_code': item_code, 'manufacturer': manufacturer}, 'manufacturer_part_no') \ No newline at end of file + {'item_code': item_code, 'manufacturer': manufacturer}, 'manufacturer_part_no') diff --git a/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json b/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json index 9b1a47eed6c..5de45cbcad9 100644 --- a/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json +++ b/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json @@ -47,7 +47,8 @@ "description": "Simple Python formula applied on Reading fields.
          Numeric eg. 1: reading_1 > 0.2 and reading_1 < 0.5
          \nNumeric eg. 2: mean > 3.5 (mean of populated fields)
          \nValue based eg.: reading_value in (\"A\", \"B\", \"C\")", "fieldname": "acceptance_formula", "fieldtype": "Code", - "label": "Acceptance Criteria Formula" + "label": "Acceptance Criteria Formula", + "options": "PythonExpression" }, { "default": "0", @@ -89,7 +90,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2021-02-04 18:50:02.056173", + "modified": "2021-08-06 15:08:20.911338", "modified_by": "Administrator", "module": "Stock", "name": "Item Quality Inspection Parameter", diff --git a/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.py b/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.py index 92aefc8d9e9..785737b267f 100644 --- a/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.py +++ b/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.py @@ -7,4 +7,4 @@ import frappe from frappe.model.document import Document class ItemQualityInspectionParameter(Document): - pass \ No newline at end of file + pass diff --git a/erpnext/stock/doctype/item_reorder/item_reorder.py b/erpnext/stock/doctype/item_reorder/item_reorder.py index 0f9c593d36a..5cdaa229565 100644 --- a/erpnext/stock/doctype/item_reorder/item_reorder.py +++ b/erpnext/stock/doctype/item_reorder/item_reorder.py @@ -9,4 +9,4 @@ import frappe from frappe.model.document import Document class ItemReorder(Document): - pass \ No newline at end of file + pass diff --git a/erpnext/stock/doctype/item_supplier/item_supplier.py b/erpnext/stock/doctype/item_supplier/item_supplier.py index 1a07f03ec58..5dda535f810 100644 --- a/erpnext/stock/doctype/item_supplier/item_supplier.py +++ b/erpnext/stock/doctype/item_supplier/item_supplier.py @@ -7,4 +7,4 @@ import frappe from frappe.model.document import Document class ItemSupplier(Document): - pass \ No newline at end of file + pass diff --git a/erpnext/stock/doctype/item_tax/item_tax.py b/erpnext/stock/doctype/item_tax/item_tax.py index 1fe2f454681..7c9e8115758 100644 --- a/erpnext/stock/doctype/item_tax/item_tax.py +++ b/erpnext/stock/doctype/item_tax/item_tax.py @@ -7,4 +7,4 @@ import frappe from frappe.model.document import Document class ItemTax(Document): - pass \ No newline at end of file + pass diff --git a/erpnext/stock/doctype/item_variant_settings/test_item_variant_settings.js b/erpnext/stock/doctype/item_variant_settings/test_item_variant_settings.js deleted file mode 100644 index 3b3bf94f375..00000000000 --- a/erpnext/stock/doctype/item_variant_settings/test_item_variant_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: Item Variant Settings", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Item Variant Settings - () => frappe.tests.make('Item Variant Settings', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/stock/doctype/item_website_specification/item_website_specification.py b/erpnext/stock/doctype/item_website_specification/item_website_specification.py index 6d0dbad2a5e..e3041cf3eef 100644 --- a/erpnext/stock/doctype/item_website_specification/item_website_specification.py +++ b/erpnext/stock/doctype/item_website_specification/item_website_specification.py @@ -9,4 +9,4 @@ import frappe from frappe.model.document import Document class ItemWebsiteSpecification(Document): - pass \ No newline at end of file + pass diff --git a/erpnext/stock/doctype/landed_cost_item/landed_cost_item.py b/erpnext/stock/doctype/landed_cost_item/landed_cost_item.py index 0521a7ad1cb..493e8b239a4 100644 --- a/erpnext/stock/doctype/landed_cost_item/landed_cost_item.py +++ b/erpnext/stock/doctype/landed_cost_item/landed_cost_item.py @@ -7,4 +7,4 @@ import frappe from frappe.model.document import Document class LandedCostItem(Document): - pass \ No newline at end of file + pass diff --git a/erpnext/stock/doctype/landed_cost_purchase_receipt/landed_cost_purchase_receipt.py b/erpnext/stock/doctype/landed_cost_purchase_receipt/landed_cost_purchase_receipt.py index f7ccb9b6e29..38f4eafc3aa 100644 --- a/erpnext/stock/doctype/landed_cost_purchase_receipt/landed_cost_purchase_receipt.py +++ b/erpnext/stock/doctype/landed_cost_purchase_receipt/landed_cost_purchase_receipt.py @@ -7,4 +7,4 @@ import frappe from frappe.model.document import Document class LandedCostPurchaseReceipt(Document): - pass \ No newline at end of file + pass diff --git a/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.py b/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.py index e4458207dbb..0dc396aefac 100644 --- a/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.py +++ b/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.py @@ -6,4 +6,4 @@ import frappe from frappe.model.document import Document class LandedCostTaxesandCharges(Document): - pass \ No newline at end of file + pass diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py index 5df4d8743fc..bf969f99f8e 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py @@ -41,7 +41,7 @@ class LandedCostVoucher(Document): def validate(self): self.check_mandatory() - self.validate_purchase_receipts() + self.validate_receipt_documents() init_landed_taxes_and_totals(self) self.set_total_taxes_and_charges() if not self.get("items"): @@ -56,14 +56,23 @@ class LandedCostVoucher(Document): frappe.throw(_("Please enter Receipt Document")) - def validate_purchase_receipts(self): + def validate_receipt_documents(self): receipt_documents = [] for d in self.get("purchase_receipts"): - if frappe.db.get_value(d.receipt_document_type, d.receipt_document, "docstatus") != 1: - frappe.throw(_("Receipt document must be submitted")) - else: - receipt_documents.append(d.receipt_document) + docstatus = frappe.db.get_value(d.receipt_document_type, d.receipt_document, "docstatus") + if docstatus != 1: + msg = f"Row {d.idx}: {d.receipt_document_type} {frappe.bold(d.receipt_document)} must be submitted" + frappe.throw(_(msg), title=_("Invalid Document")) + + if d.receipt_document_type == "Purchase Invoice": + update_stock = frappe.db.get_value(d.receipt_document_type, d.receipt_document, "update_stock") + if not update_stock: + msg = _("Row {0}: Purchase Invoice {1} has no stock impact.").format(d.idx, frappe.bold(d.receipt_document)) + msg += "
          " + _("Please create Landed Cost Vouchers against Invoices that have 'Update Stock' enabled.") + frappe.throw(msg, title=_("Incorrect Invoice")) + + receipt_documents.append(d.receipt_document) for item in self.get("items"): if not item.receipt_document: diff --git a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py index 32b08f60c4a..cb09d933801 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py @@ -11,6 +11,7 @@ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt \ from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.accounts.doctype.account.test_account import get_inventory_account from erpnext.accounts.doctype.account.test_account import create_account +from erpnext.assets.doctype.asset.test_asset import create_asset_category, create_fixed_asset_item class TestLandedCostVoucher(unittest.TestCase): def test_landed_cost_voucher(self): @@ -250,6 +251,39 @@ class TestLandedCostVoucher(unittest.TestCase): self.assertEqual(entry.credit, amounts[0]) self.assertEqual(entry.credit_in_account_currency, amounts[1]) + def test_asset_lcv(self): + "Check if LCV for an Asset updates the Assets Gross Purchase Amount correctly." + frappe.db.set_value("Company", "_Test Company", "capital_work_in_progress_account", "CWIP Account - _TC") + + if not frappe.db.exists("Asset Category", "Computers"): + create_asset_category() + + if not frappe.db.exists("Item", "Macbook Pro"): + create_fixed_asset_item() + + pr = make_purchase_receipt(item_code="Macbook Pro", qty=1, rate=50000) + + # check if draft asset was created + assets = frappe.db.get_all('Asset', filters={'purchase_receipt': pr.name}) + self.assertEqual(len(assets), 1) + + lcv = make_landed_cost_voucher( + company = pr.company, + receipt_document_type = "Purchase Receipt", + receipt_document=pr.name, + charges=80, + expense_account="Expenses Included In Valuation - _TC") + + lcv.save() + lcv.submit() + + # lcv updates amount in draft asset + self.assertEqual(frappe.db.get_value("Asset", assets[0].name, "gross_purchase_amount"), 50080) + + # tear down + lcv.cancel() + pr.cancel() + def make_landed_cost_voucher(** args): args = frappe._dict(args) ref_doc = frappe.get_doc(args.receipt_document_type, args.receipt_document) @@ -268,7 +302,7 @@ def make_landed_cost_voucher(** args): lcv.set("taxes", [{ "description": "Shipping Charges", - "expense_account": "Expenses Included In Valuation - TCP1", + "expense_account": args.expense_account or "Expenses Included In Valuation - TCP1", "amount": args.charges }]) diff --git a/erpnext/stock/doctype/manufacturer/test_manufacturer.js b/erpnext/stock/doctype/manufacturer/test_manufacturer.js deleted file mode 100644 index 0254a367ccb..00000000000 --- a/erpnext/stock/doctype/manufacturer/test_manufacturer.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: Manufacturer", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially('Manufacturer', [ - // insert a new Manufacturer - () => frappe.tests.make([ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js index 6e66f9869c0..087a7883e09 100644 --- a/erpnext/stock/doctype/material_request/material_request.js +++ b/erpnext/stock/doctype/material_request/material_request.js @@ -434,7 +434,7 @@ erpnext.buying.MaterialRequestController = erpnext.buying.BuyingController.exten if (doc.material_request_type == "Customer Provided") { return{ query: "erpnext.controllers.queries.item_query", - filters:{ + filters:{ 'customer': me.frm.doc.customer, 'is_stock_item':1 } diff --git a/erpnext/stock/doctype/material_request/material_request.json b/erpnext/stock/doctype/material_request/material_request.json index 4e2d9e61704..7cdf5b7e8f0 100644 --- a/erpnext/stock/doctype/material_request/material_request.json +++ b/erpnext/stock/doctype/material_request/material_request.json @@ -133,7 +133,8 @@ { "fieldname": "scan_barcode", "fieldtype": "Data", - "label": "Scan Barcode" + "label": "Scan Barcode", + "options": "Barcode" }, { "allow_bulk_edit": 1, @@ -314,7 +315,7 @@ "idx": 70, "is_submittable": 1, "links": [], - "modified": "2021-03-31 23:52:55.392512", + "modified": "2021-08-17 20:16:12.737743", "modified_by": "Administrator", "module": "Stock", "name": "Material Request", diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index 3ad9909ad07..026b85e26d2 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -162,8 +162,15 @@ class MaterialRequest(BuyingController): from `tabStock Entry Detail` where material_request = %s and material_request_item = %s and docstatus = 1""", (self.name, d.name))[0][0]) + mr_qty_allowance = frappe.db.get_single_value('Stock Settings', 'mr_qty_allowance') - if d.ordered_qty and d.ordered_qty > d.stock_qty: + if mr_qty_allowance: + allowed_qty = d.qty + (d.qty * (mr_qty_allowance/100)) + if d.ordered_qty and d.ordered_qty > allowed_qty: + frappe.throw(_("The total Issue / Transfer quantity {0} in Material Request {1} \ + cannot be greater than allowed requested quantity {2} for Item {3}").format(d.ordered_qty, d.parent, allowed_qty, d.item_code)) + + elif d.ordered_qty and d.ordered_qty > d.stock_qty: frappe.throw(_("The total Issue / Transfer quantity {0} in Material Request {1} \ cannot be greater than requested quantity {2} for Item {3}").format(d.ordered_qty, d.parent, d.qty, d.item_code)) diff --git a/erpnext/stock/doctype/material_request/material_request_dashboard.py b/erpnext/stock/doctype/material_request/material_request_dashboard.py index f3e5e5db250..e1e4faf6825 100644 --- a/erpnext/stock/doctype/material_request/material_request_dashboard.py +++ b/erpnext/stock/doctype/material_request/material_request_dashboard.py @@ -20,4 +20,4 @@ def get_data(): 'items': ['Work Order'] } ] - } \ No newline at end of file + } diff --git a/erpnext/stock/doctype/material_request/test_material_request.js b/erpnext/stock/doctype/material_request/test_material_request.js deleted file mode 100644 index 793cad0f3b6..00000000000 --- a/erpnext/stock/doctype/material_request/test_material_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: Material Request", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially('Material Request', [ - // insert a new Material Request - () => frappe.tests.make([ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/stock/doctype/material_request/test_material_request.py b/erpnext/stock/doctype/material_request/test_material_request.py index 72a3a5e67c7..b4776ba2492 100644 --- a/erpnext/stock/doctype/material_request/test_material_request.py +++ b/erpnext/stock/doctype/material_request/test_material_request.py @@ -329,6 +329,58 @@ class TestMaterialRequest(unittest.TestCase): self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1 + 54.0) self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2 + 3.0) + def test_over_transfer_qty_allowance(self): + mr = frappe.new_doc('Material Request') + mr.company = "_Test Company" + mr.scheduled_date = today() + mr.append('items',{ + "item_code": "_Test FG Item", + "item_name": "_Test FG Item", + "qty": 10, + "schedule_date": today(), + "uom": "_Test UOM 1", + "warehouse": "_Test Warehouse - _TC" + }) + + mr.material_request_type = "Material Transfer" + mr.insert() + mr.submit() + + frappe.db.set_value('Stock Settings', None, 'mr_qty_allowance', 20) + + # map a stock entry + + se_doc = make_stock_entry(mr.name) + se_doc.update({ + "posting_date": today(), + "posting_time": "00:00", + }) + se_doc.get("items")[0].update({ + "qty": 13, + "transfer_qty": 12.0, + "s_warehouse": "_Test Warehouse - _TC", + "t_warehouse": "_Test Warehouse 1 - _TC", + "basic_rate": 1.0 + }) + + # make available the qty in _Test Warehouse 1 before transfer + sr = frappe.new_doc("Stock Reconciliation") + sr.company = "_Test Company" + sr.purpose = "Opening Stock" + sr.append('items', { + "item_code": "_Test FG Item", + "warehouse": "_Test Warehouse - _TC", + "qty": 20, + "valuation_rate": 0.01 + }) + sr.insert() + sr.submit() + se = frappe.copy_doc(se_doc) + se.insert() + self.assertRaises(frappe.ValidationError) + se.items[0].qty = 12 + se.submit() + def test_completed_qty_for_over_transfer(self): existing_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC") existing_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC") diff --git a/erpnext/stock/doctype/material_request/tests/test_material_request.js b/erpnext/stock/doctype/material_request/tests/test_material_request.js index bf26cd117f8..a2cd03b6495 100644 --- a/erpnext/stock/doctype/material_request/tests/test_material_request.js +++ b/erpnext/stock/doctype/material_request/tests/test_material_request.js @@ -37,4 +37,3 @@ QUnit.test("test material request", function(assert) { () => done() ]); }); - diff --git a/erpnext/stock/doctype/material_request/tests/test_material_request_from_bom.js b/erpnext/stock/doctype/material_request/tests/test_material_request_from_bom.js index d8b39fe5aaf..6fb55ae02ac 100644 --- a/erpnext/stock/doctype/material_request/tests/test_material_request_from_bom.js +++ b/erpnext/stock/doctype/material_request/tests/test_material_request_from_bom.js @@ -25,4 +25,3 @@ QUnit.test("test material request get items from BOM", function(assert) { () => done() ]); }); - diff --git a/erpnext/stock/doctype/material_request/tests/test_material_request_type_manufacture.js b/erpnext/stock/doctype/material_request/tests/test_material_request_type_manufacture.js index 91b47bac4d9..137079b9838 100644 --- a/erpnext/stock/doctype/material_request/tests/test_material_request_type_manufacture.js +++ b/erpnext/stock/doctype/material_request/tests/test_material_request_type_manufacture.js @@ -27,4 +27,3 @@ QUnit.test("test material request", function(assert) { () => done() ]); }); - diff --git a/erpnext/stock/doctype/material_request/tests/test_material_request_type_material_issue.js b/erpnext/stock/doctype/material_request/tests/test_material_request_type_material_issue.js index 050e0f0d1c9..b03a8543c6f 100644 --- a/erpnext/stock/doctype/material_request/tests/test_material_request_type_material_issue.js +++ b/erpnext/stock/doctype/material_request/tests/test_material_request_type_material_issue.js @@ -27,4 +27,3 @@ QUnit.test("test material request for issue", function(assert) { () => done() ]); }); - diff --git a/erpnext/stock/doctype/material_request/tests/test_material_request_type_material_transfer.js b/erpnext/stock/doctype/material_request/tests/test_material_request_type_material_transfer.js index d6f9b661414..7c62c2e63a1 100644 --- a/erpnext/stock/doctype/material_request/tests/test_material_request_type_material_transfer.js +++ b/erpnext/stock/doctype/material_request/tests/test_material_request_type_material_transfer.js @@ -27,4 +27,3 @@ QUnit.test("test material request for transfer", function(assert) { () => done() ]); }); - diff --git a/erpnext/stock/doctype/material_request_item/material_request_item.py b/erpnext/stock/doctype/material_request_item/material_request_item.py index 16f007f6a20..e0066e65d2c 100644 --- a/erpnext/stock/doctype/material_request_item/material_request_item.py +++ b/erpnext/stock/doctype/material_request_item/material_request_item.py @@ -12,4 +12,4 @@ class MaterialRequestItem(Document): pass def on_doctype_update(): - frappe.db.add_index("Material Request Item", ["item_code", "warehouse"]) \ No newline at end of file + frappe.db.add_index("Material Request Item", ["item_code", "warehouse"]) diff --git a/erpnext/stock/doctype/packing_slip_item/packing_slip_item.py b/erpnext/stock/doctype/packing_slip_item/packing_slip_item.py index 694ab384bf6..b0a855961f9 100644 --- a/erpnext/stock/doctype/packing_slip_item/packing_slip_item.py +++ b/erpnext/stock/doctype/packing_slip_item/packing_slip_item.py @@ -9,4 +9,4 @@ import frappe from frappe.model.document import Document class PackingSlipItem(Document): - pass \ No newline at end of file + pass diff --git a/erpnext/stock/doctype/pick_list/pick_list.js b/erpnext/stock/doctype/pick_list/pick_list.js index ee218f2f685..730fd7a829c 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.js +++ b/erpnext/stock/doctype/pick_list/pick_list.js @@ -201,4 +201,4 @@ function get_item_details(item_code, uom=null) { uom }); } -} \ No newline at end of file +} diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index e795742ea4d..516ae43089b 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -239,6 +239,7 @@ def get_available_item_locations_for_batched_item(item_code, from_warehouses, re and sle.`item_code`=%(item_code)s and sle.`company` = %(company)s and batch.disabled = 0 + and sle.is_cancelled=0 and IFNULL(batch.`expiry_date`, '2200-01-01') > %(today)s {warehouse_condition} GROUP BY diff --git a/erpnext/stock/doctype/pick_list/pick_list_dashboard.py b/erpnext/stock/doctype/pick_list/pick_list_dashboard.py index 6e007df5e6b..7c321c450a6 100644 --- a/erpnext/stock/doctype/pick_list/pick_list_dashboard.py +++ b/erpnext/stock/doctype/pick_list/pick_list_dashboard.py @@ -9,4 +9,4 @@ def get_data(): 'items': ['Stock Entry', 'Delivery Note'] }, ] - } \ No newline at end of file + } diff --git a/erpnext/stock/doctype/price_list/price_list.css b/erpnext/stock/doctype/price_list/price_list.css index 61b069442f8..6832954a811 100644 --- a/erpnext/stock/doctype/price_list/price_list.css +++ b/erpnext/stock/doctype/price_list/price_list.css @@ -4,4 +4,4 @@ .table-grid thead tr { height: 50px; -} \ No newline at end of file +} diff --git a/erpnext/stock/doctype/price_list/price_list.js b/erpnext/stock/doctype/price_list/price_list.js index c362b5a765c..9291498e863 100644 --- a/erpnext/stock/doctype/price_list/price_list.js +++ b/erpnext/stock/doctype/price_list/price_list.js @@ -11,4 +11,4 @@ frappe.ui.form.on("Price List", { frappe.set_route("Report", "Item Price"); }, "fa fa-money"); } -}); \ No newline at end of file +}); diff --git a/erpnext/stock/doctype/price_list/price_list.py b/erpnext/stock/doctype/price_list/price_list.py index 33713faf696..002d3d898eb 100644 --- a/erpnext/stock/doctype/price_list/price_list.py +++ b/erpnext/stock/doctype/price_list/price_list.py @@ -13,6 +13,9 @@ class PriceList(Document): if not cint(self.buying) and not cint(self.selling): throw(_("Price List must be applicable for Buying or Selling")) + if not self.is_new(): + self.check_impact_on_shopping_cart() + def on_update(self): self.set_default_if_missing() self.update_item_price() @@ -32,6 +35,17 @@ class PriceList(Document): buying=%s, selling=%s, modified=NOW() where price_list=%s""", (self.currency, cint(self.buying), cint(self.selling), self.name)) + def check_impact_on_shopping_cart(self): + "Check if Price List currency change impacts Shopping Cart." + from erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings import validate_cart_settings + + doc_before_save = self.get_doc_before_save() + currency_changed = self.currency != doc_before_save.currency + affects_cart = self.name == frappe.get_cached_value("Shopping Cart Settings", None, "price_list") + + if currency_changed and affects_cart: + validate_cart_settings() + def on_trash(self): self.delete_price_list_details_key() @@ -62,4 +76,4 @@ def get_price_list_details(price_list): frappe.cache().hset("price_list_details", price_list, price_list_details) - return price_list_details or {} \ No newline at end of file + return price_list_details or {} diff --git a/erpnext/stock/doctype/price_list/test_price_list.js b/erpnext/stock/doctype/price_list/test_price_list.js deleted file mode 100644 index fe4e07b3bdd..00000000000 --- a/erpnext/stock/doctype/price_list/test_price_list.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: Price List", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Price List - () => frappe.tests.make('Price List', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/stock/doctype/price_list/test_price_list.py b/erpnext/stock/doctype/price_list/test_price_list.py index 5979c861294..2c287c9033c 100644 --- a/erpnext/stock/doctype/price_list/test_price_list.py +++ b/erpnext/stock/doctype/price_list/test_price_list.py @@ -6,4 +6,4 @@ import frappe # test_ignore = ["Item"] -test_records = frappe.get_test_records('Price List') \ No newline at end of file +test_records = frappe.get_test_records('Price List') diff --git a/erpnext/stock/doctype/price_list/test_price_list_uom.js b/erpnext/stock/doctype/price_list/test_price_list_uom.js index 7fbce7d59d2..3896c0e59ea 100644 --- a/erpnext/stock/doctype/price_list/test_price_list_uom.js +++ b/erpnext/stock/doctype/price_list/test_price_list_uom.js @@ -55,4 +55,4 @@ QUnit.test("test price list with uom dependancy", function(assert) { () => done() ]); -}); \ No newline at end of file +}); diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json index 44fb736304f..1a597343c0a 100755 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json @@ -1098,7 +1098,8 @@ { "fieldname": "scan_barcode", "fieldtype": "Data", - "label": "Scan Barcode" + "label": "Scan Barcode", + "options": "Barcode" }, { "fieldname": "billing_address", @@ -1148,7 +1149,7 @@ "idx": 261, "is_submittable": 1, "links": [], - "modified": "2021-05-25 00:15:12.239017", + "modified": "2021-08-17 20:16:40.849885", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt", diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 82c87a83a50..4a4514f2497 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -286,8 +286,16 @@ class PurchaseReceipt(BuyingController): and warehouse_account_name == supplier_warehouse_account: continue - self.add_gl_entry(gl_entries, warehouse_account_name, d.cost_center, stock_value_diff, 0.0, remarks, - stock_rbnb, account_currency=warehouse_account_currency, item=d) + self.add_gl_entry( + gl_entries=gl_entries, + account=warehouse_account_name, + cost_center=d.cost_center, + debit=stock_value_diff, + credit=0.0, + remarks=remarks, + against_account=stock_rbnb, + account_currency=warehouse_account_currency, + item=d) # GL Entry for from warehouse or Stock Received but not billed # Intentionally passed negative debit amount to avoid incorrect GL Entry validation @@ -300,9 +308,17 @@ class PurchaseReceipt(BuyingController): account = warehouse_account[d.from_warehouse]['account'] \ if d.from_warehouse else stock_rbnb - self.add_gl_entry(gl_entries, account, d.cost_center, - -1 * flt(d.base_net_amount, d.precision("base_net_amount")), 0.0, remarks, warehouse_account_name, - debit_in_account_currency=-1 * credit_amount, account_currency=credit_currency, item=d) + self.add_gl_entry( + gl_entries=gl_entries, + account=account, + cost_center=d.cost_center, + debit=-1 * flt(d.base_net_amount, d.precision("base_net_amount")), + credit=0.0, + remarks=remarks, + against_account=warehouse_account_name, + debit_in_account_currency=-1 * credit_amount, + account_currency=credit_currency, + item=d) # Amount added through landed-cos-voucher if d.landed_cost_voucher_amount and landed_cost_entries: @@ -311,14 +327,31 @@ class PurchaseReceipt(BuyingController): credit_amount = (flt(amount["base_amount"]) if (amount["base_amount"] or account_currency!=self.company_currency) else flt(amount["amount"])) - self.add_gl_entry(gl_entries, account, d.cost_center, 0.0, credit_amount, remarks, - warehouse_account_name, credit_in_account_currency=flt(amount["amount"]), - account_currency=account_currency, project=d.project, item=d) + self.add_gl_entry( + gl_entries=gl_entries, + account=account, + cost_center=d.cost_center, + debit=0.0, + credit=credit_amount, + remarks=remarks, + against_account=warehouse_account_name, + credit_in_account_currency=flt(amount["amount"]), + account_currency=account_currency, + project=d.project, + item=d) # sub-contracting warehouse if flt(d.rm_supp_cost) and warehouse_account.get(self.supplier_warehouse): - self.add_gl_entry(gl_entries, supplier_warehouse_account, d.cost_center, 0.0, flt(d.rm_supp_cost), - remarks, warehouse_account_name, account_currency=supplier_warehouse_account_currency, item=d) + self.add_gl_entry( + gl_entries=gl_entries, + account=supplier_warehouse_account, + cost_center=d.cost_center, + debit=0.0, + credit=flt(d.rm_supp_cost), + remarks=remarks, + against_account=warehouse_account_name, + account_currency=supplier_warehouse_account_currency, + item=d) # divisional loss adjustment valuation_amount_as_per_doc = flt(d.base_net_amount, d.precision("base_net_amount")) + \ @@ -335,8 +368,17 @@ class PurchaseReceipt(BuyingController): cost_center = d.cost_center or frappe.get_cached_value("Company", self.company, "cost_center") - self.add_gl_entry(gl_entries, loss_account, cost_center, divisional_loss, 0.0, remarks, - warehouse_account_name, account_currency=credit_currency, project=d.project, item=d) + self.add_gl_entry( + gl_entries=gl_entries, + account=loss_account, + cost_center=cost_center, + debit=divisional_loss, + credit=0.0, + remarks=remarks, + against_account=warehouse_account_name, + account_currency=credit_currency, + project=d.project, + item=d) elif d.warehouse not in warehouse_with_no_account or \ d.rejected_warehouse not in warehouse_with_no_account: @@ -347,12 +389,30 @@ class PurchaseReceipt(BuyingController): debit_currency = get_account_currency(d.expense_account) remarks = self.get("remarks") or _("Accounting Entry for Service") - self.add_gl_entry(gl_entries, service_received_but_not_billed_account, d.cost_center, 0.0, d.amount, - remarks, d.expense_account, account_currency=credit_currency, project=d.project, + self.add_gl_entry( + gl_entries=gl_entries, + account=service_received_but_not_billed_account, + cost_center=d.cost_center, + debit=0.0, + credit=d.amount, + remarks=remarks, + against_account=d.expense_account, + account_currency=credit_currency, + project=d.project, voucher_detail_no=d.name, item=d) - self.add_gl_entry(gl_entries, d.expense_account, d.cost_center, d.amount, 0.0, remarks, service_received_but_not_billed_account, - account_currency = debit_currency, project=d.project, voucher_detail_no=d.name, item=d) + self.add_gl_entry( + gl_entries=gl_entries, + account=d.expense_account, + cost_center=d.cost_center, + debit=d.amount, + credit=0.0, + remarks=remarks, + against_account=service_received_but_not_billed_account, + account_currency = debit_currency, + project=d.project, + voucher_detail_no=d.name, + item=d) if warehouse_with_no_account: frappe.msgprint(_("No accounting entries for the following warehouses") + ": \n" + @@ -402,8 +462,15 @@ class PurchaseReceipt(BuyingController): applicable_amount = negative_expense_to_be_booked * (valuation_tax[tax.name] / total_valuation_amount) amount_including_divisional_loss -= applicable_amount - self.add_gl_entry(gl_entries, account, tax.cost_center, 0.0, applicable_amount, self.remarks or _("Accounting Entry for Stock"), - against_account, item=tax) + self.add_gl_entry( + gl_entries=gl_entries, + account=account, + cost_center=tax.cost_center, + debit=0.0, + credit=applicable_amount, + remarks=self.remarks or _("Accounting Entry for Stock"), + against_account=against_account, + item=tax) i += 1 @@ -415,7 +482,7 @@ class PurchaseReceipt(BuyingController): "cost_center": cost_center, "debit": debit, "credit": credit, - "against_account": against_account, + "against": against_account, "remarks": remarks, } @@ -456,15 +523,31 @@ class PurchaseReceipt(BuyingController): # debit cwip account debit_in_account_currency = (base_asset_amount if cwip_account_currency == self.company_currency else asset_amount) - self.add_gl_entry(gl_entries, cwip_account, item.cost_center, base_asset_amount, 0.0, remarks, - arbnb_account, debit_in_account_currency=debit_in_account_currency, item=item) + self.add_gl_entry( + gl_entries=gl_entries, + account=cwip_account, + cost_center=item.cost_center, + debit=base_asset_amount, + credit=0.0, + remarks=remarks, + against_account=arbnb_account, + debit_in_account_currency=debit_in_account_currency, + item=item) asset_rbnb_currency = get_account_currency(arbnb_account) # credit arbnb account credit_in_account_currency = (base_asset_amount if asset_rbnb_currency == self.company_currency else asset_amount) - self.add_gl_entry(gl_entries, arbnb_account, item.cost_center, 0.0, base_asset_amount, remarks, - cwip_account, credit_in_account_currency=credit_in_account_currency, item=item) + self.add_gl_entry( + gl_entries=gl_entries, + account=arbnb_account, + cost_center=item.cost_center, + debit=0.0, + credit=base_asset_amount, + remarks=remarks, + against_account=cwip_account, + credit_in_account_currency=credit_in_account_currency, + item=item) def add_lcv_gl_entries(self, item, gl_entries): expenses_included_in_asset_valuation = self.get_company_default("expenses_included_in_asset_valuation") @@ -477,11 +560,27 @@ class PurchaseReceipt(BuyingController): remarks = self.get("remarks") or _("Accounting Entry for Stock") - self.add_gl_entry(gl_entries, expenses_included_in_asset_valuation, item.cost_center, 0.0, flt(item.landed_cost_voucher_amount), - remarks, asset_account, project=item.project, item=item) + self.add_gl_entry( + gl_entries=gl_entries, + account=expenses_included_in_asset_valuation, + cost_center=item.cost_center, + debit=0.0, + credit=flt(item.landed_cost_voucher_amount), + remarks=remarks, + against_account=asset_account, + project=item.project, + item=item) - self.add_gl_entry(gl_entries, asset_account, item.cost_center, 0.0, flt(item.landed_cost_voucher_amount), - remarks, expenses_included_in_asset_valuation, project=item.project, item=item) + self.add_gl_entry( + gl_entries=gl_entries, + account=asset_account, + cost_center=item.cost_center, + debit=flt(item.landed_cost_voucher_amount), + credit=0.0, + remarks=remarks, + against_account=expenses_included_in_asset_valuation, + project=item.project, + item=item) def update_assets(self, item, valuation_rate): assets = frappe.db.get_all('Asset', @@ -598,6 +697,7 @@ def make_purchase_invoice(source_name, target_doc=None): doc.run_method("onload") doc.run_method("set_missing_values") doc.run_method("calculate_taxes_and_totals") + doc.set_payment_schedule() def update_item(source_doc, target_doc, source_parent): target_doc.qty, returned_qty = get_pending_qty(source_doc) diff --git a/erpnext/stock/doctype/purchase_receipt/regional/india.js b/erpnext/stock/doctype/purchase_receipt/regional/india.js index b4f1201f36c..2d982cc1bb3 100644 --- a/erpnext/stock/doctype/purchase_receipt/regional/india.js +++ b/erpnext/stock/doctype/purchase_receipt/regional/india.js @@ -1,3 +1,3 @@ {% include "erpnext/regional/india/taxes.js" %} -erpnext.setup_auto_gst_taxation('Purchase Receipt'); \ No newline at end of file +erpnext.setup_auto_gst_taxation('Purchase Receipt'); diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 2eb8bfd5d2f..0210702250e 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -23,9 +23,7 @@ class TestPurchaseReceipt(unittest.TestCase): def test_reverse_purchase_receipt_sle(self): - frappe.db.set_value('UOM', '_Test UOM', 'must_be_whole_number', 0) - - pr = make_purchase_receipt(qty=0.5) + pr = make_purchase_receipt(qty=0.5, item_code="_Test Item Home Desktop 200") sl_entry = frappe.db.get_all("Stock Ledger Entry", {"voucher_type": "Purchase Receipt", "voucher_no": pr.name}, ['actual_qty']) @@ -41,8 +39,6 @@ class TestPurchaseReceipt(unittest.TestCase): self.assertEqual(len(sl_entry_cancelled), 2) self.assertEqual(sl_entry_cancelled[1].actual_qty, -0.5) - frappe.db.set_value('UOM', '_Test UOM', 'must_be_whole_number', 1) - def test_make_purchase_invoice(self): if not frappe.db.exists('Payment Terms Template', '_Test Payment Terms Template For Purchase Invoice'): frappe.get_doc({ @@ -328,18 +324,7 @@ class TestPurchaseReceipt(unittest.TestCase): pr1.submit() self.assertRaises(frappe.ValidationError, pr2.submit) - - pr1.cancel() - se.cancel() - se1.cancel() - se2.cancel() - se3.cancel() - po.reload() - pr2.load_from_db() - pr2.cancel() - - po.load_from_db() - po.cancel() + frappe.db.rollback() def test_serial_no_supplier(self): pr = make_purchase_receipt(item_code="_Test Serialized Item With Series", qty=1) @@ -1044,7 +1029,7 @@ class TestPurchaseReceipt(unittest.TestCase): 'account': srbnb_account, 'voucher_detail_no': pr.items[1].name }, pluck="name") - + # check if the entries are not merged into one # seperate entries should be made since voucher_detail_no is different self.assertEqual(len(item_one_gl_entry), 1) @@ -1052,6 +1037,33 @@ class TestPurchaseReceipt(unittest.TestCase): frappe.db.set_value('Company', company, 'enable_perpetual_inventory_for_non_stock_items', before_test_value) + def test_payment_terms_are_fetched_when_creating_purchase_invoice(self): + from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_terms_template + from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice + from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order, make_pr_against_po + from erpnext.selling.doctype.sales_order.test_sales_order import automatically_fetch_payment_terms, compare_payment_schedules + + automatically_fetch_payment_terms() + + po = create_purchase_order(qty=10, rate=100, do_not_save=1) + create_payment_terms_template() + po.payment_terms_template = 'Test Receivable Template' + po.submit() + + pr = make_pr_against_po(po.name, received_qty=10) + + pi = make_purchase_invoice(qty=10, rate=100, do_not_save=1) + pi.items[0].purchase_receipt = pr.name + pi.items[0].pr_detail = pr.items[0].name + pi.items[0].purchase_order = po.name + pi.items[0].po_detail = po.items[0].name + pi.insert() + + # self.assertEqual(po.payment_terms_template, pi.payment_terms_template) + compare_payment_schedules(self, po, pi) + + automatically_fetch_payment_terms(enable=0) + def get_sl_entries(voucher_type, voucher_no): return frappe.db.sql(""" select actual_qty, warehouse, stock_value_difference from `tabStock Ledger Entry` where voucher_type=%s and voucher_no=%s diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.py b/erpnext/stock/doctype/putaway_rule/putaway_rule.py index 0f50bcd6ea8..315e723fabc 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule.py +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.py @@ -232,4 +232,4 @@ def get_serial_nos_to_allocate(serial_nos, to_allocate): allocated_serial_nos = serial_nos[0: cint(to_allocate)] serial_nos[:] = serial_nos[cint(to_allocate):] # pop out allocated serial nos and modify list return "\n".join(allocated_serial_nos) if allocated_serial_nos else "" - else: return "" \ No newline at end of file + else: return "" diff --git a/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py b/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py index 86f7dc3e084..0590ae1abeb 100644 --- a/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py +++ b/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py @@ -386,4 +386,4 @@ def create_putaway_rule(**args): if not args.do_not_save: putaway.save() - return putaway \ No newline at end of file + return putaway diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.js b/erpnext/stock/doctype/quality_inspection/quality_inspection.js index f7565fd505c..d08dc3e8b76 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.js +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.js @@ -81,4 +81,4 @@ frappe.ui.form.on("Quality Inspection", { }); } }, -}); \ No newline at end of file +}); diff --git a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.js b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.js deleted file mode 100644 index 327484e6cc3..00000000000 --- a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.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: Quality Inspection", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Quality Inspection - () => frappe.tests.make('Quality Inspection', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py index 7f3d701034a..f5d076a077a 100644 --- a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py @@ -14,7 +14,7 @@ from erpnext.controllers.stock_controller import ( ) from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note from erpnext.stock.doctype.item.test_item import create_item -from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry # test_records = frappe.get_test_records('Quality Inspection') @@ -159,6 +159,47 @@ class TestQualityInspection(unittest.TestCase): frappe.delete_doc("Quality Inspection", qi) dn.delete() + def test_rejected_qi_validation(self): + """Test if rejected QI blocks Stock Entry as per Stock Settings.""" + se = make_stock_entry( + item_code="_Test Item with QA", + target="_Test Warehouse - _TC", + qty=1, + basic_rate=100, + inspection_required=True, + do_not_submit=True + ) + + readings = [ + { + "specification": "Iron Content", + "min_value": 0.1, + "max_value": 0.9, + "reading_1": "0.4" + } + ] + + qa = create_quality_inspection( + reference_type="Stock Entry", + reference_name=se.name, + readings=readings, + status="Rejected" + ) + + frappe.db.set_value("Stock Settings", None, "action_if_quality_inspection_is_rejected", "Stop") + se.reload() + self.assertRaises(QualityInspectionRejectedError, se.submit) # when blocked in Stock settings, block rejected QI + + frappe.db.set_value("Stock Settings", None, "action_if_quality_inspection_is_rejected", "Warn") + se.reload() + se.submit() # when allowed in Stock settings, allow rejected QI + + # teardown + qa.reload() + qa.cancel() + se.reload() + se.cancel() + frappe.db.set_value("Stock Settings", None, "action_if_quality_inspection_is_rejected", "Stop") def create_quality_inspection(**args): args = frappe._dict(args) @@ -175,12 +216,11 @@ def create_quality_inspection(**args): if not args.readings: create_quality_inspection_parameter("Size") readings = {"specification": "Size", "min_value": 0, "max_value": 10} + if args.status == "Rejected": + readings["reading_1"] = "12" # status is auto set in child on save else: readings = args.readings - if args.status == "Rejected": - readings["reading_1"] = "12" # status is auto set in child on save - if isinstance(readings, list): for entry in readings: create_quality_inspection_parameter(entry["specification"]) diff --git a/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.py b/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.py index 65188a22c6e..b10fa310d66 100644 --- a/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.py +++ b/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.py @@ -7,4 +7,4 @@ import frappe from frappe.model.document import Document class QualityInspectionReading(Document): - pass \ No newline at end of file + pass diff --git a/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.py b/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.py index 01d2031b3a4..971b3c29825 100644 --- a/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.py +++ b/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.py @@ -16,4 +16,4 @@ def get_template_details(template): fields=["specification", "value", "acceptance_formula", "numeric", "formula_based_criteria", "min_value", "max_value"], filters={'parenttype': 'Quality Inspection Template', 'parent': template}, - order_by="idx") \ No newline at end of file + order_by="idx") diff --git a/erpnext/stock/doctype/quality_inspection_template/test_quality_inspection_template.js b/erpnext/stock/doctype/quality_inspection_template/test_quality_inspection_template.js deleted file mode 100644 index 879c262ed2a..00000000000 --- a/erpnext/stock/doctype/quality_inspection_template/test_quality_inspection_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: Quality Inspection Template", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Quality Inspection Template - () => frappe.tests.make('Quality Inspection Template', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js index b3e4286bccb..4cd40bf38ec 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js @@ -29,13 +29,50 @@ frappe.ui.form.on('Repost Item Valuation', { }; }); } + + frm.trigger('setup_realtime_progress'); }, + + setup_realtime_progress: function(frm) { + frappe.realtime.on('item_reposting_progress', data => { + if (frm.doc.name !== data.name) { + return; + } + + if (frm.doc.status == 'In Progress') { + frm.doc.current_index = data.current_index; + frm.doc.items_to_be_repost = data.items_to_be_repost; + + frm.dashboard.reset(); + frm.trigger('show_reposting_progress'); + } + }); + }, + refresh: function(frm) { if (frm.doc.status == "Failed" && frm.doc.docstatus==1) { frm.add_custom_button(__('Restart'), function () { frm.trigger("restart_reposting"); }).addClass("btn-primary"); } + + frm.trigger('show_reposting_progress'); + }, + + show_reposting_progress: function(frm) { + var bars = []; + + let total_count = frm.doc.items_to_be_repost ? JSON.parse(frm.doc.items_to_be_repost).length : 0; + let progress = flt(cint(frm.doc.current_index) / total_count * 100, 2) || 0.5; + var title = __('Reposting Completed {0}%', [progress]); + + bars.push({ + 'title': title, + 'width': progress + '%', + 'progress_class': 'progress-bar-success' + }); + + frm.dashboard.add_progress(__('Reposting Progress'), bars); }, restart_reposting: function(frm) { diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json index 071fc86d9b3..a800bf87013 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json @@ -21,7 +21,10 @@ "allow_zero_rate", "amended_from", "error_section", - "error_log" + "error_log", + "items_to_be_repost", + "distinct_item_and_warehouse", + "current_index" ], "fields": [ { @@ -142,12 +145,39 @@ "fieldname": "allow_zero_rate", "fieldtype": "Check", "label": "Allow Zero Rate" + }, + { + "fieldname": "items_to_be_repost", + "fieldtype": "Code", + "hidden": 1, + "label": "Items to Be Repost", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "distinct_item_and_warehouse", + "fieldtype": "Code", + "hidden": 1, + "label": "Distinct Item and Warehouse", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "current_index", + "fieldtype": "Int", + "hidden": 1, + "label": "Current Index", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-12-10 07:52:12.476589", + "modified": "2021-07-22 18:59:43.057878", "modified_by": "Administrator", "module": "Stock", "name": "Repost Item Valuation", diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py index 55f2ebb2241..2e454a51596 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -80,7 +80,7 @@ def repost(doc): def repost_sl_entries(doc): if doc.based_on == 'Transaction': - repost_future_sle(voucher_type=doc.voucher_type, voucher_no=doc.voucher_no, + repost_future_sle(doc=doc, voucher_type=doc.voucher_type, voucher_no=doc.voucher_no, allow_negative_stock=doc.allow_negative_stock, via_landed_cost_voucher=doc.via_landed_cost_voucher) else: repost_future_sle(args=[frappe._dict({ @@ -133,6 +133,6 @@ def repost_entries(): def get_repost_item_valuation_entries(): return frappe.db.sql(""" SELECT name from `tabRepost Item Valuation` - WHERE status != 'Completed' and creation <= %s and docstatus = 1 + WHERE status in ('Queued', 'In Progress') and creation <= %s and docstatus = 1 ORDER BY timestamp(posting_date, posting_time) asc, creation asc """, now(), as_dict=1) diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index bad7b608acf..70312bc543b 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -165,8 +165,14 @@ class SerialNo(StockController): ) ORDER BY posting_date desc, posting_time desc, creation desc""", - (self.item_code, self.company, - serial_no, serial_no+'\n%', '%\n'+serial_no, '%\n'+serial_no+'\n%'), as_dict=1): + ( + self.item_code, self.company, + serial_no, + serial_no+'\n%', + '%\n'+serial_no, + '%\n'+serial_no+'\n%' + ), + as_dict=1): if serial_no.upper() in get_serial_nos(sle.serial_no): if cint(sle.actual_qty) > 0: sle_dict.setdefault("incoming", []).append(sle) diff --git a/erpnext/stock/doctype/serial_no/test_serial_no.js b/erpnext/stock/doctype/serial_no/test_serial_no.js deleted file mode 100644 index bf8293257c3..00000000000 --- a/erpnext/stock/doctype/serial_no/test_serial_no.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: Serial No", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Serial No - () => frappe.tests.make('Serial No', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/stock/doctype/serial_no/test_serial_no.py b/erpnext/stock/doctype/serial_no/test_serial_no.py index cde7fe07c63..0eccce3a58e 100644 --- a/erpnext/stock/doctype/serial_no/test_serial_no.py +++ b/erpnext/stock/doctype/serial_no/test_serial_no.py @@ -174,5 +174,23 @@ class TestSerialNo(unittest.TestCase): self.assertEqual(sn_doc.warehouse, "_Test Warehouse - _TC") self.assertEqual(sn_doc.purchase_document_no, se.name) + def test_serial_no_sanitation(self): + "Test if Serial No input is sanitised before entering the DB." + item_code = "_Test Serialized Item" + test_records = frappe.get_test_records('Stock Entry') + + se = frappe.copy_doc(test_records[0]) + se.get("items")[0].item_code = item_code + se.get("items")[0].qty = 3 + se.get("items")[0].serial_no = " _TS1, _TS2 , _TS3 " + se.get("items")[0].transfer_qty = 3 + se.set_stock_entry_type() + se.insert() + se.submit() + + self.assertEqual(se.get("items")[0].serial_no, "_TS1\n_TS2\n_TS3") + + frappe.db.rollback() + def tearDown(self): - frappe.db.rollback() \ No newline at end of file + frappe.db.rollback() diff --git a/erpnext/stock/doctype/shipment/shipment.js b/erpnext/stock/doctype/shipment/shipment.js index ce2906ecbe9..13a17a25913 100644 --- a/erpnext/stock/doctype/shipment/shipment.js +++ b/erpnext/stock/doctype/shipment/shipment.js @@ -150,8 +150,8 @@ frappe.ui.form.on('Shipment', { frm.set_value('pickup_contact_name', ''); frm.set_value('pickup_contact', ''); } - frappe.throw(__("Email or Phone/Mobile of the Contact are mandatory to continue.") - + "
          " + __("Please set Email/Phone for the contact") + frappe.throw(__("Email or Phone/Mobile of the Contact are mandatory to continue.") + + "
          " + __("Please set Email/Phone for the contact") + ` ${contact_name}`); } let contact_display = r.message.contact_display; @@ -244,8 +244,8 @@ frappe.ui.form.on('Shipment', { frm.set_value('pickup_company', ''); frm.set_value('pickup_contact', ''); } - frappe.throw(__("Last Name, Email or Phone/Mobile of the user are mandatory to continue.") + "
          " - + __("Please first set Last Name, Email and Phone for the user") + frappe.throw(__("Last Name, Email or Phone/Mobile of the user are mandatory to continue.") + "
          " + + __("Please first set Last Name, Email and Phone for the user") + ` ${frappe.session.user}`); } let contact_display = r.full_name; diff --git a/erpnext/stock/doctype/shipment/shipment_list.js b/erpnext/stock/doctype/shipment/shipment_list.js index 52b052c81f3..ae6a3c154e8 100644 --- a/erpnext/stock/doctype/shipment/shipment_list.js +++ b/erpnext/stock/doctype/shipment/shipment_list.js @@ -5,4 +5,4 @@ frappe.listview_settings['Shipment'] = { return [__("Booked"), "green"]; } } -}; \ No newline at end of file +}; diff --git a/erpnext/stock/doctype/shipment/test_shipment.py b/erpnext/stock/doctype/shipment/test_shipment.py index 9c3e22f0231..db2f1161743 100644 --- a/erpnext/stock/doctype/shipment/test_shipment.py +++ b/erpnext/stock/doctype/shipment/test_shipment.py @@ -24,7 +24,7 @@ def create_test_delivery_note(): customer = get_shipment_customer() item = get_shipment_item(company.name) posting_date = date.today() + timedelta(days=1) - + create_material_receipt(item, company.name) delivery_note = frappe.new_doc("Delivery Note") delivery_note.company = company.name @@ -73,7 +73,7 @@ def create_test_shipment(delivery_notes = None): shipment.pickup_to = '17:00' shipment.description_of_content = 'unit test entry' for delivery_note in delivery_notes: - shipment.append('shipment_delivery_note', + shipment.append('shipment_delivery_note', { "delivery_note": delivery_note.name } @@ -222,7 +222,7 @@ def create_material_receipt(item, company): ) stock.insert() stock.submit() - + def create_shipment_item(item_name, company_name): item = frappe.new_doc("Item") diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 67083930272..efbc12ce841 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -1101,3 +1101,4 @@ function check_should_not_attach_bom_items(bom_no) { } $.extend(cur_frm.cscript, new erpnext.stock.StockEntry({frm: cur_frm})); + diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.json b/erpnext/stock/doctype/stock_entry/stock_entry.json index 523d332b8f4..2f377788961 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.json +++ b/erpnext/stock/doctype/stock_entry/stock_entry.json @@ -84,8 +84,6 @@ "oldfieldtype": "Section Break" }, { - "allow_on_submit": 1, - "default": "{purpose}", "fieldname": "title", "fieldtype": "Data", "hidden": 1, @@ -355,6 +353,7 @@ }, { "fieldname": "scan_barcode", + "options": "Barcode", "fieldtype": "Data", "label": "Scan Barcode" }, @@ -629,7 +628,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-05-26 17:07:58.015737", + "modified": "2021-08-20 19:19:31.514846", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry", diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 8f27ef4356c..ba7c6d11337 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -58,6 +58,7 @@ class StockEntry(StockController): self.validate_posting_time() self.validate_purpose() + self.set_title() self.validate_item() self.validate_customer_provided_item() self.validate_qty() @@ -76,6 +77,7 @@ class StockEntry(StockController): self.validate_difference_account() self.set_job_card_data() self.set_purpose_for_stock_entry() + self.clean_serial_nos() self.validate_duplicate_serial_no() if not self.from_bom: @@ -270,7 +272,7 @@ class StockEntry(StockController): item_wise_qty = {} if self.purpose == "Manufacture" and self.work_order: for d in self.items: - if d.is_finished_item: + if d.is_finished_item or d.is_process_loss: item_wise_qty.setdefault(d.item_code, []).append(d.qty) for item_code, qty_list in iteritems(item_wise_qty): @@ -316,9 +318,6 @@ class StockEntry(StockController): d.s_warehouse = self.from_warehouse d.t_warehouse = self.to_warehouse - if not (d.s_warehouse or d.t_warehouse): - frappe.throw(_("Atleast one warehouse is mandatory")) - if self.purpose in source_mandatory and not d.s_warehouse: if self.from_warehouse: d.s_warehouse = self.from_warehouse @@ -331,9 +330,10 @@ class StockEntry(StockController): else: frappe.throw(_("Target warehouse is mandatory for row {0}").format(d.idx)) + if self.purpose == "Manufacture": if validate_for_manufacture: - if d.is_finished_item or d.is_scrap_item: + if d.is_finished_item or d.is_scrap_item or d.is_process_loss: d.s_warehouse = None if not d.t_warehouse: frappe.throw(_("Target warehouse is mandatory for row {0}").format(d.idx)) @@ -345,6 +345,9 @@ class StockEntry(StockController): if cstr(d.s_warehouse) == cstr(d.t_warehouse) and not self.purpose == "Material Transfer for Manufacture": frappe.throw(_("Source and target warehouse cannot be same for row {0}").format(d.idx)) + if not (d.s_warehouse or d.t_warehouse): + frappe.throw(_("Atleast one warehouse is mandatory")) + def validate_work_order(self): if self.purpose in ("Manufacture", "Material Transfer for Manufacture", "Material Consumption for Manufacture"): # check if work order is entered @@ -462,7 +465,7 @@ class StockEntry(StockController): """ # Set rate for outgoing items outgoing_items_cost = self.set_rate_for_outgoing_items(reset_outgoing_rate, raise_error_if_no_rate) - finished_item_qty = sum(d.transfer_qty for d in self.items if d.is_finished_item) + finished_item_qty = sum(d.transfer_qty for d in self.items if d.is_finished_item or d.is_process_loss) # Set basic rate for incoming items for d in self.get('items'): @@ -483,6 +486,8 @@ class StockEntry(StockController): raise_error_if_no_rate=raise_error_if_no_rate) d.basic_rate = flt(d.basic_rate, d.precision("basic_rate")) + if d.is_process_loss: + d.basic_rate = flt(0.) d.basic_amount = flt(flt(d.transfer_qty) * flt(d.basic_rate), d.precision("basic_amount")) def set_rate_for_outgoing_items(self, reset_outgoing_rate=True, raise_error_if_no_rate=True): @@ -529,7 +534,7 @@ class StockEntry(StockController): scrap_items_cost = sum([flt(d.basic_amount) for d in self.get("items") if d.is_scrap_item]) # Get raw materials cost from BOM if multiple material consumption entries - if frappe.db.get_single_value("Manufacturing Settings", "material_consumption"): + if frappe.db.get_single_value("Manufacturing Settings", "material_consumption", cache=True): bom_items = self.get_bom_raw_materials(finished_item_qty) outgoing_items_cost = sum([flt(row.qty)*flt(row.rate) for row in bom_items.values()]) @@ -719,6 +724,10 @@ class StockEntry(StockController): frappe.throw(_("Multiple items cannot be marked as finished item")) if self.purpose == "Manufacture": + if not finished_items: + frappe.throw(_('Finished Good has not set in the stock entry {0}') + .format(self.name)) + allowance_percentage = flt(frappe.db.get_single_value("Manufacturing Settings", "overproduction_percentage_for_work_order")) @@ -1036,6 +1045,7 @@ class StockEntry(StockController): self.set_scrap_items() self.set_actual_qty() + self.update_items_for_process_loss() self.validate_customer_provided_item() self.calculate_rate_and_amount() @@ -1090,13 +1100,13 @@ class StockEntry(StockController): "is_finished_item": 1 } - if self.work_order and self.pro_doc.has_batch_no: + if self.work_order and self.pro_doc.has_batch_no and cint(frappe.db.get_single_value('Manufacturing Settings', + 'make_serial_no_batch_from_work_order', cache=True)): self.set_batchwise_finished_goods(args, item) else: - self.add_finisged_goods(args, item) + self.add_finished_goods(args, item) def set_batchwise_finished_goods(self, args, item): - qty = flt(self.fg_completed_qty) filters = { "reference_name": self.pro_doc.name, "reference_doctype": self.pro_doc.doctype, @@ -1105,7 +1115,17 @@ class StockEntry(StockController): fields = ["qty_to_produce as qty", "produced_qty", "name"] - for row in frappe.get_all("Batch", filters = filters, fields = fields, order_by="creation asc"): + data = frappe.get_all("Batch", filters = filters, fields = fields, order_by="creation asc") + + if not data: + self.add_finished_goods(args, item) + else: + self.add_batchwise_finished_good(data, args, item) + + def add_batchwise_finished_good(self, data, args, item): + qty = flt(self.fg_completed_qty) + + for row in data: batch_qty = flt(row.qty) - flt(row.produced_qty) if not batch_qty: continue @@ -1121,9 +1141,9 @@ class StockEntry(StockController): args["qty"] = fg_qty args["batch_no"] = row.name - self.add_finisged_goods(args, item) + self.add_finished_goods(args, item) - def add_finisged_goods(self, args, item): + def add_finished_goods(self, args, item): self.add_to_stock_entry_detail({ item.name: args }, bom_no = self.bom_no) @@ -1170,7 +1190,7 @@ class StockEntry(StockController): wo = frappe.get_doc("Work Order", self.work_order) wo_items = frappe.get_all('Work Order Item', filters={'parent': self.work_order}, - fields=["item_code", "required_qty", "consumed_qty", "transferred_qty"] + fields=["item_code", "source_warehouse", "required_qty", "consumed_qty", "transferred_qty"] ) work_order_qty = wo.material_transferred_for_manufacturing or wo.qty @@ -1190,7 +1210,7 @@ class StockEntry(StockController): if qty > 0: self.add_to_stock_entry_detail({ item.item_code: { - "from_warehouse": wo.wip_warehouse, + "from_warehouse": wo.wip_warehouse or item.source_warehouse, "to_warehouse": "", "qty": qty, "item_name": item.item_name, @@ -1383,6 +1403,7 @@ class StockEntry(StockController): get_default_cost_center(item_dict[d], company = self.company)) se_child.is_finished_item = item_dict[d].get("is_finished_item", 0) se_child.is_scrap_item = item_dict[d].get("is_scrap_item", 0) + se_child.is_process_loss = item_dict[d].get("is_process_loss", 0) for field in ["idx", "po_detail", "original_item", "expense_account", "description", "item_name", "serial_no", "batch_no"]: @@ -1561,6 +1582,30 @@ class StockEntry(StockController): if material_request and material_request not in material_requests: material_requests.append(material_request) frappe.db.set_value('Material Request', material_request, 'transfer_status', status) + + def update_items_for_process_loss(self): + process_loss_dict = {} + for d in self.get("items"): + if not d.is_process_loss: + continue + + scrap_warehouse = frappe.db.get_single_value("Manufacturing Settings", "default_scrap_warehouse") + if scrap_warehouse is not None: + d.t_warehouse = scrap_warehouse + d.is_scrap_item = 0 + + if d.item_code not in process_loss_dict: + process_loss_dict[d.item_code] = [flt(0), flt(0)] + process_loss_dict[d.item_code][0] += flt(d.transfer_qty) + process_loss_dict[d.item_code][1] += flt(d.qty) + + for d in self.get("items"): + if not d.is_finished_item or d.item_code not in process_loss_dict: + continue + # Assumption: 1 finished item has 1 row. + d.transfer_qty -= process_loss_dict[d.item_code][0] + d.qty -= process_loss_dict[d.item_code][1] + def set_serial_no_batch_for_finished_good(self): args = {} @@ -1592,6 +1637,14 @@ class StockEntry(StockController): return sorted(list(set(get_serial_nos(self.pro_doc.serial_no)) - set(used_serial_nos))) + def set_title(self): + if frappe.flags.in_import and self.title: + # Allow updating title during data import/update + return + + self.title = self.purpose + + @frappe.whitelist() def move_sample_to_retention_warehouse(company, items): if isinstance(items, string_types): @@ -1775,7 +1828,7 @@ def get_expired_batch_items(): from `tabBatch` b, `tabStock Ledger Entry` sle where b.expiry_date <= %s and b.expiry_date is not NULL - and b.batch_id = sle.batch_no + and b.batch_id = sle.batch_no and sle.is_cancelled = 0 group by sle.warehouse, sle.item_code, sle.batch_no""",(nowdate()), as_dict=1) @frappe.whitelist() @@ -1842,4 +1895,4 @@ def get_supplied_items(purchase_order): supplied_item.total_supplied_qty = flt(supplied_item.supplied_qty) - flt(supplied_item.returned_qty) - return supplied_item_details \ No newline at end of file + return supplied_item_details diff --git a/erpnext/stock/doctype/stock_entry/stock_entry_utils.py b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py index b12a8547fea..563fcb03973 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry_utils.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py @@ -45,6 +45,8 @@ def make_stock_entry(**args): s.posting_date = args.posting_date if args.posting_time: s.posting_time = args.posting_time + if args.inspection_required: + s.inspection_required = args.inspection_required # map names if args.from_warehouse: diff --git a/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_manufacture.js b/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_manufacture.js new file mode 100644 index 00000000000..285ae4f59e8 --- /dev/null +++ b/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_manufacture.js @@ -0,0 +1,27 @@ +QUnit.module('Stock'); + +QUnit.test("test manufacture from bom", function(assert) { + assert.expect(2); + let done = assert.async(); + frappe.run_serially([ + () => { + return frappe.tests.make("Stock Entry", [ + { purpose: "Manufacture" }, + { from_bom: 1 }, + { bom_no: "BOM-_Test Item - Non Whole UOM-001" }, + { fg_completed_qty: 2 } + ]); + }, + () => cur_frm.save(), + () => frappe.click_button("Update Rate and Availability"), + () => { + assert.ok(cur_frm.doc.items[1] === 0.75, " Finished Item Qty correct"); + assert.ok(cur_frm.doc.items[2] === 0.25, " Process Loss Item Qty correct"); + }, + () => frappe.tests.click_button('Submit'), + () => frappe.tests.click_button('Yes'), + () => frappe.timeout(0.3), + () => done() + ]); +}); + diff --git a/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_material_issue.js b/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_material_issue.js index 3cf4861ccb8..a87a7fb7fd8 100644 --- a/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_material_issue.js +++ b/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_material_issue.js @@ -28,4 +28,3 @@ QUnit.test("test material request", function(assert) { () => done() ]); }); - diff --git a/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_material_issue_with_serialize_item.js b/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_material_issue_with_serialize_item.js index aac09c30cd5..cae318d8f2c 100644 --- a/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_material_issue_with_serialize_item.js +++ b/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_material_issue_with_serialize_item.js @@ -32,4 +32,3 @@ QUnit.test("test material issue", function(assert) { () => done() ]); }); - diff --git a/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_material_receipt.js b/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_material_receipt.js index 828738eb6ca..ef0286fe1b9 100644 --- a/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_material_receipt.js +++ b/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_material_receipt.js @@ -29,4 +29,3 @@ QUnit.test("test material request", function(assert) { () => done() ]); }); - diff --git a/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_material_receipt_for_serialize_item.js b/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_material_receipt_for_serialize_item.js index ffd06642bf0..54e1ac81211 100644 --- a/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_material_receipt_for_serialize_item.js +++ b/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_material_receipt_for_serialize_item.js @@ -32,4 +32,3 @@ QUnit.test("test material receipt", function(assert) { () => done() ]); }); - diff --git a/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_material_transfer.js b/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_material_transfer.js index cdeb4ab04a7..fac0b4b8922 100644 --- a/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_material_transfer.js +++ b/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_material_transfer.js @@ -31,4 +31,3 @@ QUnit.test("test material request", function(assert) { () => done() ]); }); - diff --git a/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_material_transfer_for_manufacture.js b/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_material_transfer_for_manufacture.js index e8b2973c457..9f853072709 100644 --- a/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_material_transfer_for_manufacture.js +++ b/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_material_transfer_for_manufacture.js @@ -31,4 +31,3 @@ QUnit.test("test material Transfer to manufacture", function(assert) { () => done() ]); }); - diff --git a/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_repack.js b/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_repack.js index 699634df6d2..20f119ad617 100644 --- a/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_repack.js +++ b/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_repack.js @@ -39,4 +39,3 @@ QUnit.test("test repack", function(assert) { () => done() ]); }); - diff --git a/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_subcontract.js b/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_subcontract.js index 770f886d043..8243426032d 100644 --- a/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_subcontract.js +++ b/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_subcontract.js @@ -31,4 +31,3 @@ QUnit.test("test material Transfer to manufacture", function(assert) { () => done() ]); }); - diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json index a1782839048..2282b6aa167 100644 --- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json +++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json @@ -18,6 +18,7 @@ "col_break2", "is_finished_item", "is_scrap_item", + "is_process_loss", "quality_inspection", "subcontracted_item", "section_break_8", @@ -307,6 +308,7 @@ "fieldname": "quality_inspection", "fieldtype": "Link", "label": "Quality Inspection", + "no_copy": 1, "options": "Quality Inspection" }, { @@ -542,13 +544,19 @@ "no_copy": 1, "print_hide": 1, "read_only": 1 + }, + { + "default": "0", + "fieldname": "is_process_loss", + "fieldtype": "Check", + "label": "Is Process Loss" } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-04-22 20:08:23.799715", + "modified": "2021-06-22 16:47:11.268975", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry Detail", diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.py b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.py index f9e062f8516..a5623fded23 100644 --- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.py +++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.py @@ -7,4 +7,4 @@ import frappe from frappe.model.document import Document class StockEntryDetail(Document): - pass \ No newline at end of file + pass diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py index 0febcb68910..be1f00e37fa 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py @@ -55,12 +55,12 @@ class StockLedgerEntry(Document): "sum(actual_qty)") or 0 frappe.db.set_value("Batch", self.batch_no, "batch_qty", batch_qty) - #check for item quantity available in stock def actual_amt_check(self): + """Validate that qty at warehouse for selected batch is >=0""" if self.batch_no and not self.get("allow_negative_stock"): batch_bal_after_transaction = flt(frappe.db.sql("""select sum(actual_qty) from `tabStock Ledger Entry` - where warehouse=%s and item_code=%s and batch_no=%s""", + where is_cancelled =0 and warehouse=%s and item_code=%s and batch_no=%s""", (self.warehouse, self.item_code, self.batch_no))[0][0]) if batch_bal_after_transaction < 0: @@ -89,17 +89,16 @@ class StockLedgerEntry(Document): if item_det.is_stock_item != 1: frappe.throw(_("Item {0} must be a stock Item").format(self.item_code)) - # check if batch number is required - if self.voucher_type != 'Stock Reconciliation': - if item_det.has_batch_no == 1: - batch_item = self.item_code if self.item_code == item_det.item_name else self.item_code + ":" + item_det.item_name - if not self.batch_no: - frappe.throw(_("Batch number is mandatory for Item {0}").format(batch_item)) - elif not frappe.db.get_value("Batch",{"item": self.item_code, "name": self.batch_no}): - frappe.throw(_("{0} is not a valid Batch Number for Item {1}").format(self.batch_no, batch_item)) + # check if batch number is valid + if item_det.has_batch_no == 1: + batch_item = self.item_code if self.item_code == item_det.item_name else self.item_code + ":" + item_det.item_name + if not self.batch_no: + frappe.throw(_("Batch number is mandatory for Item {0}").format(batch_item)) + elif not frappe.db.get_value("Batch",{"item": self.item_code, "name": self.batch_no}): + frappe.throw(_("{0} is not a valid Batch Number for Item {1}").format(self.batch_no, batch_item)) - elif item_det.has_batch_no == 0 and self.batch_no and self.is_cancelled == 0: - frappe.throw(_("The Item {0} cannot have Batch").format(self.item_code)) + elif item_det.has_batch_no == 0 and self.batch_no and self.is_cancelled == 0: + frappe.throw(_("The Item {0} cannot have Batch").format(self.item_code)) if item_det.has_variants: frappe.throw(_("Stock cannot exist for Item {0} since has variants").format(self.item_code), @@ -108,7 +107,7 @@ class StockLedgerEntry(Document): self.stock_uom = item_det.stock_uom def check_stock_frozen_date(self): - stock_settings = frappe.get_doc('Stock Settings', 'Stock Settings') + stock_settings = frappe.get_cached_doc('Stock Settings') if stock_settings.stock_frozen_upto: if (getdate(self.posting_date) <= getdate(stock_settings.stock_frozen_upto) @@ -153,7 +152,7 @@ class StockLedgerEntry(Document): last_transaction_time = frappe.db.sql(""" select MAX(timestamp(posting_date, posting_time)) as posting_time from `tabStock Ledger Entry` - where docstatus = 1 and item_code = %s + where docstatus = 1 and is_cancelled = 0 and item_code = %s and warehouse = %s""", (self.item_code, self.warehouse))[0][0] cur_doc_posting_datetime = "%s %s" % (self.posting_date, self.get("posting_time") or "00:00:00") @@ -178,3 +177,4 @@ def on_doctype_update(): frappe.db.add_index("Stock Ledger Entry", ["voucher_no", "voucher_type"]) frappe.db.add_index("Stock Ledger Entry", ["batch_no", "item_code", "warehouse"]) + frappe.db.add_index("Stock Ledger Entry", ["voucher_detail_no"]) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js index a01db80da4a..99694690bbc 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js @@ -17,6 +17,14 @@ frappe.ui.form.on("Stock Reconciliation", { } } }); + frm.set_query("batch_no", "items", function(doc, cdt, cdn) { + var item = locals[cdt][cdn]; + return { + filters: { + 'item': item.item_code + } + }; + }); if (frm.doc.company) { erpnext.queries.setup_queries(frm, "Warehouse", function() { @@ -280,3 +288,4 @@ erpnext.stock.StockReconciliation = erpnext.stock.StockController.extend({ }); cur_frm.cscript = new erpnext.stock.StockReconciliation({frm: cur_frm}); + diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 2e092865075..4531652a13c 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -31,6 +31,7 @@ class StockReconciliation(StockController): self.validate_expense_account() self.validate_customer_provided_item() self.set_zero_value_for_customer_provided_items() + self.clean_serial_nos() self.set_total_qty_and_amount() self.validate_putaway_capacity() @@ -389,7 +390,7 @@ class StockReconciliation(StockController): sl_entries = self.merge_similar_item_serial_nos(sl_entries) sl_entries.reverse() - allow_negative_stock = frappe.db.get_value("Stock Settings", None, "allow_negative_stock") + allow_negative_stock = cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock")) self.make_sl_entries(sl_entries, allow_negative_stock=allow_negative_stock) diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.js index 80001d63fd4..666d2c7144f 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.js +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.js @@ -29,4 +29,3 @@ QUnit.test("test Stock Reconciliation", function(assert) { () => done() ]); }); - diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 84cdc491282..e4381271ed2 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -15,6 +15,8 @@ from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.utils import get_incoming_rate, get_stock_value_on, get_valuation_method from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt +from erpnext.tests.utils import change_settings + class TestStockReconciliation(unittest.TestCase): @classmethod @@ -309,6 +311,7 @@ class TestStockReconciliation(unittest.TestCase): pr2.cancel() pr1.cancel() + @change_settings("Stock Settings", {"allow_negative_stock": 0}) def test_backdated_stock_reco_future_negative_stock(self): """ Test if a backdated stock reco causes future negative stock and is blocked. @@ -326,8 +329,6 @@ class TestStockReconciliation(unittest.TestCase): warehouse = "_Test Warehouse - _TC" create_item(item_code) - negative_stock_setting = frappe.db.get_single_value("Stock Settings", "allow_negative_stock") - frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 0) pr1 = make_purchase_receipt(item_code=item_code, warehouse=warehouse, qty=10, rate=100, posting_date=add_days(nowdate(), -2)) @@ -347,11 +348,70 @@ class TestStockReconciliation(unittest.TestCase): self.assertRaises(NegativeStockError, sr3.submit) # teardown - frappe.db.set_value("Stock Settings", None, "allow_negative_stock", negative_stock_setting) sr3.cancel() dn2.cancel() pr1.cancel() + + @change_settings("Stock Settings", {"allow_negative_stock": 0}) + def test_backdated_stock_reco_cancellation_future_negative_stock(self): + """ + Test if a backdated stock reco cancellation that causes future negative stock is blocked. + ------------------------------------------- + Var | Doc | Qty | Balance + ------------------------------------------- + SR | Reco | 100 | 100 (posting date: today-1) (shouldn't be cancelled after DN) + DN | DN | 100 | 0 (posting date: today) + """ + from erpnext.stock.stock_ledger import NegativeStockError + from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note + frappe.db.commit() + + item_code = "Backdated-Reco-Cancellation-Item" + warehouse = "_Test Warehouse - _TC" + create_item(item_code) + + + sr = create_stock_reconciliation(item_code=item_code, warehouse=warehouse, qty=100, rate=100, + posting_date=add_days(nowdate(), -1)) + + dn = create_delivery_note(item_code=item_code, warehouse=warehouse, qty=100, rate=120, + posting_date=nowdate()) + + dn_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": dn.name, "is_cancelled": 0}, + "qty_after_transaction") + self.assertEqual(dn_balance, 0) + + # check if cancellation of stock reco is blocked + self.assertRaises(NegativeStockError, sr.cancel) + + repost_exists = bool(frappe.db.exists("Repost Item Valuation", {"voucher_no": sr.name})) + self.assertFalse(repost_exists, msg="Negative stock validation not working on reco cancellation") + + # teardown + frappe.db.rollback() + + + def test_valid_batch(self): + create_batch_item_with_batch("Testing Batch Item 1", "001") + create_batch_item_with_batch("Testing Batch Item 2", "002") + sr = create_stock_reconciliation(item_code="Testing Batch Item 1", qty=1, rate=100, batch_no="002" + , do_not_submit=True) + self.assertRaises(frappe.ValidationError, sr.submit) + +def create_batch_item_with_batch(item_name, batch_id): + batch_item_doc = create_item(item_name, is_stock_item=1) + if not batch_item_doc.has_batch_no: + batch_item_doc.has_batch_no = 1 + batch_item_doc.create_new_batch = 1 + batch_item_doc.save(ignore_permissions=True) + + if not frappe.db.exists('Batch', batch_id): + b = frappe.new_doc('Batch') + b.item = item_name + b.batch_id = batch_id + b.save() + def insert_existing_sle(warehouse): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry @@ -437,4 +497,3 @@ def set_valuation_method(item_code, valuation_method): }, allow_negative_stock=1) test_dependencies = ["Item", "Warehouse"] - diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.js b/erpnext/stock/doctype/stock_settings/stock_settings.js index 48624e0f25e..6167becdaac 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.js +++ b/erpnext/stock/doctype/stock_settings/stock_settings.js @@ -16,36 +16,3 @@ frappe.ui.form.on('Stock Settings', { } }); -frappe.tour['Stock Settings'] = [ - { - fieldname: "item_naming_by", - title: __("Item Naming By"), - description: __("By default, the Item Name is set as per the Item Code entered. If you want Items to be named by a ") + "Naming Series" + __(" choose the 'Naming Series' option."), - }, - { - fieldname: "default_warehouse", - title: __("Default Warehouse"), - description: __("Set a Default Warehouse for Inventory Transactions. This will be fetched into the Default Warehouse in the Item master.") - }, - { - fieldname: "allow_negative_stock", - title: __("Allow Negative Stock"), - description: __("This will allow stock items to be displayed in negative values. Using this option depends on your use case. With this option unchecked, the system warns before obstructing a transaction that is causing negative stock.") - - }, - { - fieldname: "valuation_method", - title: __("Valuation Method"), - description: __("Choose between FIFO and Moving Average Valuation Methods. Click ") + "here" + __(" to know more about them.") - }, - { - fieldname: "show_barcode_field", - title: __("Show Barcode Field"), - description: __("Show 'Scan Barcode' field above every child table to insert Items with ease.") - }, - { - fieldname: "automatically_set_serial_nos_based_on_fifo", - title: __("Automatically Set Serial Nos based on FIFO"), - description: __("Serial numbers for stock will be set automatically based on the Items entered based on first in first out in transactions like Purchase/Sales Invoices, Delivery Notes, etc.") - } -]; diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json index cf5d98d0923..f75cb561385 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.json +++ b/erpnext/stock/doctype/stock_settings/stock_settings.json @@ -18,12 +18,16 @@ "section_break_9", "over_delivery_receipt_allowance", "role_allowed_to_over_deliver_receive", + "mr_qty_allowance", "column_break_12", "auto_insert_price_list_rate_if_missing", "allow_negative_stock", "show_barcode_field", "clean_description_html", + "quality_inspection_settings_section", "action_if_quality_inspection_is_not_submitted", + "column_break_21", + "action_if_quality_inspection_is_rejected", "section_break_7", "automatically_set_serial_nos_based_on_fifo", "set_qty_in_transactions_based_on_serial_no_input", @@ -264,6 +268,28 @@ { "fieldname": "column_break_31", "fieldtype": "Column Break" + }, + { + "fieldname": "quality_inspection_settings_section", + "fieldtype": "Section Break", + "label": "Quality Inspection Settings" + }, + { + "fieldname": "column_break_21", + "fieldtype": "Column Break" + }, + { + "default": "Stop", + "fieldname": "action_if_quality_inspection_is_rejected", + "fieldtype": "Select", + "label": "Action If Quality Inspection Is Rejected", + "options": "Stop\nWarn" + }, + { + "description": "The percentage you are allowed to transfer more against the quantity ordered. For example, if you have ordered 100 units, and your Allowance is 10%, then you are allowed transfer 110 units.", + "fieldname": "mr_qty_allowance", + "fieldtype": "Float", + "label": "Over Transfer Allowance" } ], "icon": "icon-cog", @@ -271,7 +297,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2021-04-30 17:27:42.709231", + "modified": "2021-06-28 17:02:26.683002", "modified_by": "Administrator", "module": "Stock", "name": "Stock Settings", @@ -291,4 +317,4 @@ "sort_field": "modified", "sort_order": "ASC", "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/stock/doctype/stock_settings/test_stock_settings.js b/erpnext/stock/doctype/stock_settings/test_stock_settings.js deleted file mode 100644 index 57d9fc61aab..00000000000 --- a/erpnext/stock/doctype/stock_settings/test_stock_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: Stock Settings", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Stock Settings - () => frappe.tests.make('Stock Settings', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/stock/doctype/uom_category/test_uom_category.js b/erpnext/stock/doctype/uom_category/test_uom_category.js deleted file mode 100644 index 4b5972ea716..00000000000 --- a/erpnext/stock/doctype/uom_category/test_uom_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: UOM Category", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new UOM Category - () => frappe.tests.make('UOM Category', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/stock/doctype/uom_conversion_detail/uom_conversion_detail.py b/erpnext/stock/doctype/uom_conversion_detail/uom_conversion_detail.py index 67fe20bd379..fdead205670 100644 --- a/erpnext/stock/doctype/uom_conversion_detail/uom_conversion_detail.py +++ b/erpnext/stock/doctype/uom_conversion_detail/uom_conversion_detail.py @@ -7,4 +7,4 @@ import frappe from frappe.model.document import Document class UOMConversionDetail(Document): - pass \ No newline at end of file + pass diff --git a/erpnext/stock/doctype/variant_field/test_variant_field.js b/erpnext/stock/doctype/variant_field/test_variant_field.js deleted file mode 100644 index 2600a10fe0d..00000000000 --- a/erpnext/stock/doctype/variant_field/test_variant_field.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: Variant Field", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Variant Field - () => frappe.tests.make('Variant Field', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/stock/doctype/warehouse/test_warehouse.js b/erpnext/stock/doctype/warehouse/test_warehouse.js index 8ea280cc59e..850da1ee45f 100644 --- a/erpnext/stock/doctype/warehouse/test_warehouse.js +++ b/erpnext/stock/doctype/warehouse/test_warehouse.js @@ -16,4 +16,4 @@ QUnit.test("test: warehouse", function (assert) { () => done() ]); -}); \ No newline at end of file +}); diff --git a/erpnext/stock/doctype/warehouse/test_warehouse.py b/erpnext/stock/doctype/warehouse/test_warehouse.py index e3981c913e1..6e429a22552 100644 --- a/erpnext/stock/doctype/warehouse/test_warehouse.py +++ b/erpnext/stock/doctype/warehouse/test_warehouse.py @@ -180,4 +180,4 @@ def get_group_stock_account(company, company_abbr=None): if not company_abbr: company_abbr = frappe.get_cached_value("Company", company, 'abbr') group_stock_account = "Current Assets - " + company_abbr - return group_stock_account \ No newline at end of file + return group_stock_account diff --git a/erpnext/stock/doctype/warehouse/warehouse.js b/erpnext/stock/doctype/warehouse/warehouse.js index 1f172504a7f..4e1679c4116 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.js +++ b/erpnext/stock/doctype/warehouse/warehouse.js @@ -48,11 +48,11 @@ frappe.ui.form.on("Warehouse", { frm.add_custom_button(__('Non-Group to Group'), function() { convert_to_group_or_ledger(frm); }, 'fa fa-retweet', 'btn-default') } - + frm.toggle_enable(['is_group', 'company'], false); frappe.dynamic_link = {doc: frm.doc, fieldname: 'name', doctype: 'Warehouse'}; - + frm.fields_dict['parent_warehouse'].get_query = function(doc) { return { filters: { @@ -83,6 +83,7 @@ function convert_to_group_or_ledger(frm){ callback: function(){ frm.refresh(); } - + }) -} \ No newline at end of file +} + diff --git a/erpnext/stock/doctype/warehouse/warehouse_tree.js b/erpnext/stock/doctype/warehouse/warehouse_tree.js index 407d7d1ccd5..e9e14c72466 100644 --- a/erpnext/stock/doctype/warehouse/warehouse_tree.js +++ b/erpnext/stock/doctype/warehouse/warehouse_tree.js @@ -24,4 +24,4 @@ frappe.treeview_settings['Warehouse'] = { + '').insertBefore(node.$ul); } } -} \ No newline at end of file +} diff --git a/erpnext/stock/form_tour/stock_entry/stock_entry.json b/erpnext/stock/form_tour/stock_entry/stock_entry.json new file mode 100644 index 00000000000..6363c6ad4dd --- /dev/null +++ b/erpnext/stock/form_tour/stock_entry/stock_entry.json @@ -0,0 +1,56 @@ +{ + "creation": "2021-08-24 14:44:22.292652", + "docstatus": 0, + "doctype": "Form Tour", + "idx": 0, + "is_standard": 1, + "modified": "2021-08-25 16:31:31.441194", + "modified_by": "Administrator", + "module": "Stock", + "name": "Stock Entry", + "owner": "Administrator", + "reference_doctype": "Stock Entry", + "save_on_complete": 1, + "steps": [ + { + "description": "Select the type of Stock Entry to be made. For now, to receive stock into a warehouses select Material Receipt.", + "field": "", + "fieldname": "stock_entry_type", + "fieldtype": "Link", + "has_next_condition": 1, + "is_table_field": 0, + "label": "Stock Entry Type", + "next_step_condition": "eval: doc.stock_entry_type === \"Material Receipt\"", + "parent_field": "", + "position": "Top", + "title": "Stock Entry Type" + }, + { + "description": "Select a target warehouse where the stock will be received.", + "field": "", + "fieldname": "to_warehouse", + "fieldtype": "Link", + "has_next_condition": 1, + "is_table_field": 0, + "label": "Default Target Warehouse", + "next_step_condition": "eval: doc.to_warehouse", + "parent_field": "", + "position": "Top", + "title": "Default Target Warehouse" + }, + { + "description": "Select an item and entry quantity to be delivered.", + "field": "", + "fieldname": "items", + "fieldtype": "Table", + "has_next_condition": 1, + "is_table_field": 0, + "label": "Items", + "next_step_condition": "eval: doc.items[0]?.item_code", + "parent_field": "", + "position": "Top", + "title": "Items" + } + ], + "title": "Stock Entry" +} \ No newline at end of file diff --git a/erpnext/stock/form_tour/stock_reconciliation/stock_reconciliation.json b/erpnext/stock/form_tour/stock_reconciliation/stock_reconciliation.json new file mode 100644 index 00000000000..5b7fd72c082 --- /dev/null +++ b/erpnext/stock/form_tour/stock_reconciliation/stock_reconciliation.json @@ -0,0 +1,55 @@ +{ + "creation": "2021-08-24 14:44:46.770952", + "docstatus": 0, + "doctype": "Form Tour", + "idx": 0, + "is_standard": 1, + "modified": "2021-08-25 16:26:11.718664", + "modified_by": "Administrator", + "module": "Stock", + "name": "Stock Reconciliation", + "owner": "Administrator", + "reference_doctype": "Stock Reconciliation", + "save_on_complete": 1, + "steps": [ + { + "description": "Set Purpose to Opening Stock to set the stock opening balance.", + "field": "", + "fieldname": "purpose", + "fieldtype": "Select", + "has_next_condition": 1, + "is_table_field": 0, + "label": "Purpose", + "next_step_condition": "eval: doc.purpose === \"Opening Stock\"", + "parent_field": "", + "position": "Top", + "title": "Purpose" + }, + { + "description": "Select the items for which the opening stock has to be set.", + "field": "", + "fieldname": "items", + "fieldtype": "Table", + "has_next_condition": 1, + "is_table_field": 0, + "label": "Items", + "next_step_condition": "eval: doc.items[0]?.item_code", + "parent_field": "", + "position": "Top", + "title": "Items" + }, + { + "description": "Edit the Posting Date by clicking on the Edit Posting Date and Time checkbox below.", + "field": "", + "fieldname": "posting_date", + "fieldtype": "Date", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Posting Date", + "parent_field": "", + "position": "Bottom", + "title": "Posting Date" + } + ], + "title": "Stock Reconciliation" +} \ No newline at end of file diff --git a/erpnext/stock/form_tour/stock_settings/stock_settings.json b/erpnext/stock/form_tour/stock_settings/stock_settings.json new file mode 100644 index 00000000000..3d164e33b3b --- /dev/null +++ b/erpnext/stock/form_tour/stock_settings/stock_settings.json @@ -0,0 +1,89 @@ +{ + "creation": "2021-08-20 15:20:59.336585", + "docstatus": 0, + "doctype": "Form Tour", + "idx": 0, + "is_standard": 1, + "modified": "2021-08-25 16:19:37.699528", + "modified_by": "Administrator", + "module": "Stock", + "name": "Stock Settings", + "owner": "Administrator", + "reference_doctype": "Stock Settings", + "save_on_complete": 1, + "steps": [ + { + "description": "By default, the Item Name is set as per the Item Code entered. If you want Items to be named by a Naming Series choose the 'Naming Series' option.", + "field": "", + "fieldname": "item_naming_by", + "fieldtype": "Select", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Item Naming By", + "parent_field": "", + "position": "Bottom", + "title": "Item Naming By" + }, + { + "description": "Set a Default Warehouse for Inventory Transactions. This will be fetched into the Default Warehouse in the Item master.", + "field": "", + "fieldname": "default_warehouse", + "fieldtype": "Link", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Default Warehouse", + "parent_field": "", + "position": "Bottom", + "title": "Default Warehouse" + }, + { + "description": "Quality inspection is performed on the inward and outward movement of goods. Receipt and delivery transactions will be stopped or the user will be warned if the quality inspection is not performed.", + "field": "", + "fieldname": "action_if_quality_inspection_is_not_submitted", + "fieldtype": "Select", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Action If Quality Inspection Is Not Submitted", + "parent_field": "", + "position": "Bottom", + "title": "Action if Quality Inspection Is Not Submitted" + }, + { + "description": "Serial numbers for stock will be set automatically based on the Items entered based on first in first out in transactions like Purchase/Sales Invoices, Delivery Notes, etc.", + "field": "", + "fieldname": "automatically_set_serial_nos_based_on_fifo", + "fieldtype": "Check", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Automatically Set Serial Nos Based on FIFO", + "parent_field": "", + "position": "Bottom", + "title": "Automatically Set Serial Nos based on FIFO" + }, + { + "description": "Show 'Scan Barcode' field above every child table to insert Items with ease.", + "field": "", + "fieldname": "show_barcode_field", + "fieldtype": "Check", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Show Barcode Field in Stock Transactions", + "parent_field": "", + "position": "Bottom", + "title": "Show Barcode Field" + }, + { + "description": "Choose between FIFO and Moving Average Valuation Methods. Click here to know more about them.", + "field": "", + "fieldname": "valuation_method", + "fieldtype": "Select", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Default Valuation Method", + "parent_field": "", + "position": "Bottom", + "title": "Default Valuation Method" + } + ], + "title": "Stock Settings" +} \ No newline at end of file diff --git a/erpnext/stock/form_tour/warehouse/warehouse.json b/erpnext/stock/form_tour/warehouse/warehouse.json new file mode 100644 index 00000000000..23ff2aebbaa --- /dev/null +++ b/erpnext/stock/form_tour/warehouse/warehouse.json @@ -0,0 +1,54 @@ +{ + "creation": "2021-08-24 14:43:44.465237", + "docstatus": 0, + "doctype": "Form Tour", + "idx": 0, + "is_standard": 1, + "modified": "2021-08-24 14:50:31.988256", + "modified_by": "Administrator", + "module": "Stock", + "name": "Warehouse", + "owner": "Administrator", + "reference_doctype": "Warehouse", + "save_on_complete": 1, + "steps": [ + { + "description": "Select a name for the warehouse. This should reflect its location or purpose.", + "field": "", + "fieldname": "warehouse_name", + "fieldtype": "Data", + "has_next_condition": 1, + "is_table_field": 0, + "label": "Warehouse Name", + "next_step_condition": "eval: doc.warehouse_name", + "parent_field": "", + "position": "Bottom", + "title": "Warehouse Name" + }, + { + "description": "Select a warehouse type to categorize the warehouse into a sub-group.", + "field": "", + "fieldname": "warehouse_type", + "fieldtype": "Link", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Warehouse Type", + "parent_field": "", + "position": "Top", + "title": "Warehouse Type" + }, + { + "description": "Select an account to set a default account for all transactions with this warehouse.", + "field": "", + "fieldname": "account", + "fieldtype": "Link", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Account", + "parent_field": "", + "position": "Top", + "title": "Account" + } + ], + "title": "Warehouse" +} \ No newline at end of file diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index ca174a3f63c..c72073c6143 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -74,9 +74,7 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru update_party_blanket_order(args, out) - if not doc or cint(doc.get('is_return')) == 0: - # get price list rate only if the invoice is not a credit or debit note - get_price_list_rate(args, item, out) + out.update(get_price_list_rate(args, item)) if args.customer and cint(args.is_pos): out.update(get_pos_profile_item_details(args.company, args, update_data=True)) @@ -280,6 +278,10 @@ def get_basic_details(args, item, overwrite_warehouse=True): else: args.uom = item.stock_uom + if (args.get("batch_no") and + item.name != frappe.get_cached_value('Batch', args.get("batch_no"), 'item')): + args['batch_no'] = '' + out = frappe._dict({ "item_code": item.name, "item_name": item.item_name, @@ -288,6 +290,7 @@ def get_basic_details(args, item, overwrite_warehouse=True): "warehouse": warehouse, "income_account": get_default_income_account(args, item_defaults, item_group_defaults, brand_defaults), "expense_account": expense_account or get_default_expense_account(args, item_defaults, item_group_defaults, brand_defaults) , + "discount_account": None or get_default_discount_account(args, item_defaults), "cost_center": get_default_cost_center(args, item_defaults, item_group_defaults, brand_defaults), 'has_serial_no': item.has_serial_no, 'has_batch_no': item.has_batch_no, @@ -313,8 +316,8 @@ def get_basic_details(args, item, overwrite_warehouse=True): "transaction_date": args.get("transaction_date"), "against_blanket_order": args.get("against_blanket_order"), "bom_no": item.get("default_bom"), - "weight_per_unit": args.get("weight_per_unit") or item.get("weight_per_unit"), - "weight_uom": args.get("weight_uom") or item.get("weight_uom") + "weight_per_unit": item.get("weight_per_unit"), + "weight_uom": item.get("weight_uom") }) if item.get("enable_deferred_revenue") or item.get("enable_deferred_expense"): @@ -441,7 +444,7 @@ def get_item_tax_info(company, tax_category, item_codes, item_rates=None, item_t if item_tax_templates is None: item_tax_templates = {} - + if item_rates is None: item_rates = {} @@ -590,6 +593,10 @@ def get_default_expense_account(args, item, item_group, brand): or brand.get("expense_account") or args.expense_account) +def get_default_discount_account(args, item): + return (item.get("default_discount_account") + or args.discount_account) + def get_default_deferred_account(args, item, fieldname=None): if item.get("enable_deferred_revenue") or item.get("enable_deferred_expense"): return (item.get(fieldname) @@ -639,7 +646,10 @@ def get_default_supplier(args, item, item_group, brand): or item_group.get("default_supplier") or brand.get("default_supplier")) -def get_price_list_rate(args, item_doc, out): +def get_price_list_rate(args, item_doc, out=None): + if out is None: + out = frappe._dict() + meta = frappe.get_meta(args.parenttype or args.doctype) if meta.get_field("currency") or args.get('currency'): @@ -652,17 +662,17 @@ def get_price_list_rate(args, item_doc, out): if meta.get_field("currency"): validate_conversion_rate(args, meta) - price_list_rate = get_price_list_rate_for(args, item_doc.name) or 0 + price_list_rate = get_price_list_rate_for(args, item_doc.name) # variant - if not price_list_rate and item_doc.variant_of: + if price_list_rate is None and item_doc.variant_of: price_list_rate = get_price_list_rate_for(args, item_doc.variant_of) # insert in database - if not price_list_rate: + if price_list_rate is None: if args.price_list and args.rate: insert_item_price(args) - return {} + return out out.price_list_rate = flt(price_list_rate) * flt(args.plc_conversion_rate) \ / flt(args.conversion_rate) @@ -672,6 +682,8 @@ def get_price_list_rate(args, item_doc, out): out.update(get_last_purchase_details(item_doc.name, args.name, args.conversion_rate)) + return out + def insert_item_price(args): """Insert Item Price if Price List and Price List Rate are specified and currency is the same""" if frappe.db.get_value("Price List", args.price_list, "currency", cache=True) == args.currency \ @@ -807,10 +819,14 @@ def check_packing_list(price_list_rate_name, desired_qty, item_code): def validate_conversion_rate(args, meta): from erpnext.controllers.accounts_controller import validate_conversion_rate - if (not args.conversion_rate - and args.currency==frappe.get_cached_value('Company', args.company, "default_currency")): + company_currency = frappe.get_cached_value('Company', args.company, "default_currency") + if (not args.conversion_rate and args.currency==company_currency): args.conversion_rate = 1.0 + if (not args.ignore_conversion_rate and args.conversion_rate == 1 and args.currency!=company_currency): + args.conversion_rate = get_exchange_rate(args.currency, + company_currency, args.transaction_date, "for_buying") or 1.0 + # validate currency conversion rate validate_conversion_rate(args.currency, args.conversion_rate, meta.get_label("conversion_rate"), args.company) @@ -1070,9 +1086,8 @@ def apply_price_list(args, as_doc=False): } def apply_price_list_on_item(args): - item_details = frappe._dict() item_doc = frappe.get_doc("Item", args.item_code) - get_price_list_rate(args, item_doc, item_details) + item_details = get_price_list_rate(args, item_doc) item_details.update(get_pricing_rule_for_item(args, item_details.price_list_rate)) diff --git a/erpnext/stock/landed_taxes_and_charges_common.js b/erpnext/stock/landed_taxes_and_charges_common.js index f3f61963a88..ff8a69fb033 100644 --- a/erpnext/stock/landed_taxes_and_charges_common.js +++ b/erpnext/stock/landed_taxes_and_charges_common.js @@ -59,4 +59,3 @@ document_list.forEach((doctype) => { } }); }); - diff --git a/erpnext/stock/module_onboarding/stock/stock.json b/erpnext/stock/module_onboarding/stock/stock.json index 847464822b4..c246747a5b3 100644 --- a/erpnext/stock/module_onboarding/stock/stock.json +++ b/erpnext/stock/module_onboarding/stock/stock.json @@ -19,32 +19,26 @@ "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/stock", "idx": 0, "is_complete": 0, - "modified": "2020-10-14 14:54:42.741971", + "modified": "2021-08-20 14:38:55.570067", "modified_by": "Administrator", "module": "Stock", "name": "Stock", "owner": "Administrator", "steps": [ { - "step": "Setup your Warehouse" + "step": "Stock Settings" }, { - "step": "Create a Product" - }, - { - "step": "Create a Supplier" - }, - { - "step": "Introduction to Stock Entry" + "step": "Create a Warehouse" }, { "step": "Create a Stock Entry" }, { - "step": "Create a Purchase Receipt" + "step": "Stock Opening Balance" }, { - "step": "Stock Settings" + "step": "View Stock Projected Qty" } ], "subtitle": "Inventory, Warehouses, Analysis, and more.", diff --git a/erpnext/stock/onboarding_step/create_a_purchase_receipt/create_a_purchase_receipt.json b/erpnext/stock/onboarding_step/create_a_purchase_receipt/create_a_purchase_receipt.json deleted file mode 100644 index 9012493f57e..00000000000 --- a/erpnext/stock/onboarding_step/create_a_purchase_receipt/create_a_purchase_receipt.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "action": "Create Entry", - "creation": "2020-05-19 18:59:13.266713", - "docstatus": 0, - "doctype": "Onboarding Step", - "idx": 0, - "is_complete": 0, - "is_mandatory": 0, - "is_single": 0, - "is_skipped": 0, - "modified": "2020-10-14 14:53:25.618434", - "modified_by": "Administrator", - "name": "Create a Purchase Receipt", - "owner": "Administrator", - "reference_document": "Purchase Receipt", - "show_full_form": 1, - "title": "Create a Purchase Receipt", - "validate_action": 1 -} \ No newline at end of file diff --git a/erpnext/stock/onboarding_step/create_a_stock_entry/create_a_stock_entry.json b/erpnext/stock/onboarding_step/create_a_stock_entry/create_a_stock_entry.json index 09902b8844e..3cb522c893d 100644 --- a/erpnext/stock/onboarding_step/create_a_stock_entry/create_a_stock_entry.json +++ b/erpnext/stock/onboarding_step/create_a_stock_entry/create_a_stock_entry.json @@ -1,19 +1,21 @@ { "action": "Create Entry", + "action_label": "Create a Material Transfer Entry", "creation": "2020-05-15 03:20:16.277043", + "description": "# Manage Stock Movements\nStock entry allows you to register the movement of stock for various purposes like transfer, received, issues, repacked, etc. To address issues related to theft and pilferages, you can always ensure that the movement of goods happens against a document reference Stock Entry in ERPNext.\n\nLet\u2019s get a quick walk-through on the various scenarios covered in Stock Entry by watching [*this video*](https://www.youtube.com/watch?v=Njt107hlY3I).", "docstatus": 0, "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, - "is_mandatory": 0, "is_single": 0, "is_skipped": 0, - "modified": "2020-10-14 14:53:00.105905", + "modified": "2021-06-18 13:57:11.434063", "modified_by": "Administrator", "name": "Create a Stock Entry", "owner": "Administrator", "reference_document": "Stock Entry", + "show_form_tour": 1, "show_full_form": 1, - "title": "Create a Stock Entry", + "title": "Manage Stock Movements", "validate_action": 1 } \ No newline at end of file diff --git a/erpnext/stock/onboarding_step/create_a_supplier/create_a_supplier.json b/erpnext/stock/onboarding_step/create_a_supplier/create_a_supplier.json index ef61fa3b2e2..49efe578a29 100644 --- a/erpnext/stock/onboarding_step/create_a_supplier/create_a_supplier.json +++ b/erpnext/stock/onboarding_step/create_a_supplier/create_a_supplier.json @@ -1,18 +1,19 @@ { - "action": "Create Entry", + "action": "Show Form Tour", "creation": "2020-05-14 22:09:10.043554", + "description": "# Create a Supplier\nIn this step we will create a **Supplier**. If you have already created a **Supplier** you can skip this step.", "docstatus": 0, "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, - "is_mandatory": 0, "is_single": 0, "is_skipped": 0, - "modified": "2020-10-14 14:53:00.120455", + "modified": "2021-05-17 16:37:37.697077", "modified_by": "Administrator", "name": "Create a Supplier", "owner": "Administrator", "reference_document": "Supplier", + "show_form_tour": 0, "show_full_form": 0, "title": "Create a Supplier", "validate_action": 1 diff --git a/erpnext/stock/onboarding_step/create_a_warehouse/create_a_warehouse.json b/erpnext/stock/onboarding_step/create_a_warehouse/create_a_warehouse.json new file mode 100644 index 00000000000..22c88bf10ea --- /dev/null +++ b/erpnext/stock/onboarding_step/create_a_warehouse/create_a_warehouse.json @@ -0,0 +1,21 @@ +{ + "action": "Create Entry", + "action_label": "Let\u2019s create your first warehouse ", + "creation": "2021-05-17 16:13:19.297789", + "description": "# Setup a Warehouse\nThe warehouse can be your location/godown/store where you maintain the item's inventory, and receive/deliver them to various parties.\n\nIn ERPNext, you can maintain a Warehouse in the tree structure, so that location and sub-location of an item can be tracked. Also, you can link a Warehouse to a specific Accounting ledger, where the real-time stock value of that warehouse\u2019s item will be reflected.", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-08-18 12:23:36.675572", + "modified_by": "Administrator", + "name": "Create a Warehouse", + "owner": "Administrator", + "reference_document": "Warehouse", + "show_form_tour": 1, + "show_full_form": 1, + "title": "Setup a Warehouse", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/stock/onboarding_step/create_an_item/create_an_item.json b/erpnext/stock/onboarding_step/create_an_item/create_an_item.json new file mode 100644 index 00000000000..016cbd566d5 --- /dev/null +++ b/erpnext/stock/onboarding_step/create_an_item/create_an_item.json @@ -0,0 +1,22 @@ +{ + "action": "Create Entry", + "action_label": "", + "creation": "2021-05-17 13:47:18.515052", + "description": "# Create an Item\nThe Stock module deals with the movement of items.\n\nIn this step we will create an [**Item**](https://docs.erpnext.com/docs/user/manual/en/stock/item).", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "intro_video_url": "", + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-05-18 16:15:20.695028", + "modified_by": "Administrator", + "name": "Create an Item", + "owner": "Administrator", + "reference_document": "Item", + "show_form_tour": 1, + "show_full_form": 1, + "title": "Create an Item", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/stock/onboarding_step/introduction_to_stock_entry/introduction_to_stock_entry.json b/erpnext/stock/onboarding_step/introduction_to_stock_entry/introduction_to_stock_entry.json index 212e5055eda..384950e8b99 100644 --- a/erpnext/stock/onboarding_step/introduction_to_stock_entry/introduction_to_stock_entry.json +++ b/erpnext/stock/onboarding_step/introduction_to_stock_entry/introduction_to_stock_entry.json @@ -1,17 +1,18 @@ { "action": "Watch Video", "creation": "2020-05-15 02:47:17.958806", + "description": "# Introduction to Stock Entry\nThis video will give a quick introduction to [**Stock Entry**](https://docs.erpnext.com/docs/user/manual/en/stock/stock-entry).", "docstatus": 0, "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, - "is_mandatory": 0, "is_single": 0, "is_skipped": 0, - "modified": "2020-10-14 14:53:00.075177", + "modified": "2021-05-18 15:13:43.306064", "modified_by": "Administrator", "name": "Introduction to Stock Entry", "owner": "Administrator", + "show_form_tour": 0, "show_full_form": 0, "title": "Introduction to Stock Entry", "validate_action": 1, diff --git a/erpnext/stock/onboarding_step/setup_your_warehouse/setup_your_warehouse.json b/erpnext/stock/onboarding_step/setup_your_warehouse/setup_your_warehouse.json index 75940ed2a6c..5d33a649100 100644 --- a/erpnext/stock/onboarding_step/setup_your_warehouse/setup_your_warehouse.json +++ b/erpnext/stock/onboarding_step/setup_your_warehouse/setup_your_warehouse.json @@ -5,15 +5,15 @@ "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, - "is_mandatory": 0, "is_single": 0, "is_skipped": 0, - "modified": "2020-10-14 14:53:25.538900", + "modified": "2021-05-17 13:53:06.936579", "modified_by": "Administrator", "name": "Setup your Warehouse", "owner": "Administrator", "path": "Tree/Warehouse", "reference_document": "Warehouse", + "show_form_tour": 0, "show_full_form": 0, "title": "Set up your Warehouse", "validate_action": 1 diff --git a/erpnext/stock/onboarding_step/stock_opening_balance/stock_opening_balance.json b/erpnext/stock/onboarding_step/stock_opening_balance/stock_opening_balance.json new file mode 100644 index 00000000000..48fd1fddee0 --- /dev/null +++ b/erpnext/stock/onboarding_step/stock_opening_balance/stock_opening_balance.json @@ -0,0 +1,22 @@ +{ + "action": "Create Entry", + "action_label": "Let\u2019s create a stock opening entry", + "creation": "2021-05-17 16:13:47.511883", + "description": "# Update Stock Opening Balance\nIt\u2019s an entry to update the stock balance of an item, in a warehouse, on a date and time you are going live on ERPNext.\n\nOnce opening stocks are updated, you can create transactions like manufacturing and stock deliveries, where this opening stock will be consumed.", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-06-18 13:59:36.021097", + "modified_by": "Administrator", + "name": "Stock Opening Balance", + "owner": "Administrator", + "reference_document": "Stock Reconciliation", + "show_form_tour": 1, + "show_full_form": 1, + "title": "Update Stock Opening Balance", + "validate_action": 1, + "video_url": "https://www.youtube.com/watch?v=nlHX0ZZ84Lw" +} \ No newline at end of file diff --git a/erpnext/stock/onboarding_step/stock_settings/stock_settings.json b/erpnext/stock/onboarding_step/stock_settings/stock_settings.json index ae34afa695f..2cf90e806cd 100644 --- a/erpnext/stock/onboarding_step/stock_settings/stock_settings.json +++ b/erpnext/stock/onboarding_step/stock_settings/stock_settings.json @@ -1,19 +1,21 @@ { "action": "Show Form Tour", + "action_label": "Take a walk through Stock Settings", "creation": "2020-05-15 02:53:57.209967", + "description": "# Review Stock Settings\n\nIn ERPNext, the Stock module\u2019s features are configurable as per your business needs. Stock Settings is the place where you can set your preferences for:\n- Default values for Item and Pricing\n- Default valuation method for inventory valuation\n- Set preference for serialization and batching of item\n- Set tolerance for over-receipt and delivery of items", "docstatus": 0, "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, - "is_mandatory": 0, "is_single": 1, "is_skipped": 0, - "modified": "2020-10-14 14:53:00.092504", + "modified": "2021-08-18 12:06:51.139387", "modified_by": "Administrator", "name": "Stock Settings", "owner": "Administrator", "reference_document": "Stock Settings", + "show_form_tour": 0, "show_full_form": 0, - "title": "Explore Stock Settings", + "title": "Review Stock Settings", "validate_action": 1 } \ No newline at end of file diff --git a/erpnext/stock/onboarding_step/view_stock_projected_qty/view_stock_projected_qty.json b/erpnext/stock/onboarding_step/view_stock_projected_qty/view_stock_projected_qty.json new file mode 100644 index 00000000000..e684780751f --- /dev/null +++ b/erpnext/stock/onboarding_step/view_stock_projected_qty/view_stock_projected_qty.json @@ -0,0 +1,24 @@ +{ + "action": "View Report", + "action_label": "Check Stock Projected Qty", + "creation": "2021-08-20 14:38:41.649103", + "description": "# Check Stock Reports\nBased on the various stock transactions, you can get a host of one-click Stock Reports in ERPNext like Stock Ledger, Stock Balance, Projected Quantity, and Ageing analysis.", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-08-20 14:38:41.649103", + "modified_by": "Administrator", + "name": "View Stock Projected Qty", + "owner": "Administrator", + "reference_report": "Stock Projected Qty", + "report_description": "You can set the filters to narrow the results, then click on Generate New Report to see the updated report.", + "report_reference_doctype": "Item", + "report_type": "Script Report", + "show_form_tour": 0, + "show_full_form": 0, + "title": "Check Stock Projected Qty", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/stock/onboarding_step/view_warehouses/view_warehouses.json b/erpnext/stock/onboarding_step/view_warehouses/view_warehouses.json new file mode 100644 index 00000000000..c46c4bdab86 --- /dev/null +++ b/erpnext/stock/onboarding_step/view_warehouses/view_warehouses.json @@ -0,0 +1,20 @@ +{ + "action": "Go to Page", + "creation": "2021-05-17 16:12:43.427579", + "description": "# View Warehouse\nIn ERPNext the term 'warehouse' can be thought of as a storage location.\n\nWarehouses are arranged in ERPNext in a tree like structure, where multiple sub-warehouses can be grouped under a single warehouse.\n\nIn this step we will view the [**Warehouse Tree**](https://docs.erpnext.com/docs/user/manual/en/stock/warehouse#21-tree-view) to view the [**Warehouses**](https://docs.erpnext.com/docs/user/manual/en/stock/warehouse) that are set by default.", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-05-18 15:04:41.198413", + "modified_by": "Administrator", + "name": "View Warehouses", + "owner": "Administrator", + "path": "Tree/Warehouse", + "show_form_tour": 0, + "show_full_form": 0, + "title": "View Warehouses", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.html b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.html index 90112c78a83..de7e38e7d3e 100644 --- a/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.html +++ b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.html @@ -37,4 +37,4 @@ {% endif %} -{% endfor %} \ No newline at end of file +{% endfor %} diff --git a/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.js b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.js index b610e7dd587..47ae86b9e21 100644 --- a/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.js +++ b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.js @@ -117,4 +117,4 @@ frappe.pages['warehouse-capacity-summary'].on_page_load = function(wrapper) { setup_click('Item'); setup_click('Warehouse'); }); -}; \ No newline at end of file +}; diff --git a/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary_header.html b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary_header.html index acaf180a903..7ac5e640302 100644 --- a/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary_header.html +++ b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary_header.html @@ -16,4 +16,4 @@ % Occupied - \ No newline at end of file + diff --git a/erpnext/stock/report/batch_item_expiry_status/batch_item_expiry_status.py b/erpnext/stock/report/batch_item_expiry_status/batch_item_expiry_status.py index 7354eee4130..29689b1a912 100644 --- a/erpnext/stock/report/batch_item_expiry_status/batch_item_expiry_status.py +++ b/erpnext/stock/report/batch_item_expiry_status/batch_item_expiry_status.py @@ -24,7 +24,7 @@ def execute(filters=None): data.append([item, item_map[item]["item_name"], item_map[item]["description"], wh, batch, frappe.db.get_value('Batch', batch, 'expiry_date'), qty_dict.expiry_status ]) - + return columns, data @@ -70,7 +70,7 @@ def get_item_warehouse_batch_map(filters, float_precision): "expires_on": None, "expiry_status": None})) qty_dict = iwb_map[d.item_code][d.warehouse][d.batch_no] - + expiry_date_unicode = frappe.db.get_value('Batch', d.batch_no, 'expiry_date') qty_dict.expires_on = expiry_date_unicode diff --git a/erpnext/stock/report/cogs_by_item_group/__init__.py b/erpnext/stock/report/cogs_by_item_group/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/stock/report/cogs_by_item_group/cogs_by_item_group.js b/erpnext/stock/report/cogs_by_item_group/cogs_by_item_group.js new file mode 100644 index 00000000000..d7c50a66979 --- /dev/null +++ b/erpnext/stock/report/cogs_by_item_group/cogs_by_item_group.js @@ -0,0 +1,31 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + + +frappe.query_reports["COGS By Item Group"] = { + filters: [ + { + label: __("Company"), + fieldname: "company", + fieldtype: "Link", + options: "Company", + mandatory: true, + default: frappe.defaults.get_user_default("Company"), + }, + { + label: __("From Date"), + fieldname: "from_date", + fieldtype: "Date", + mandatory: true, + default: frappe.datetime.year_start(), + }, + { + label: __("To Date"), + fieldname: "to_date", + fieldtype: "Date", + mandatory: true, + default: frappe.datetime.get_today(), + }, + ] +}; diff --git a/erpnext/stock/report/cogs_by_item_group/cogs_by_item_group.json b/erpnext/stock/report/cogs_by_item_group/cogs_by_item_group.json new file mode 100644 index 00000000000..a14adf8a453 --- /dev/null +++ b/erpnext/stock/report/cogs_by_item_group/cogs_by_item_group.json @@ -0,0 +1,32 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2021-06-02 18:59:19.830928", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "modified": "2021-06-02 18:59:55.470621", + "modified_by": "Administrator", + "module": "Stock", + "name": "COGS By Item Group", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "GL Entry", + "report_name": "COGS By Item Group", + "report_type": "Script Report", + "roles": [ + { + "role": "Accounts User" + }, + { + "role": "Accounts Manager" + }, + { + "role": "Auditor" + } + ] +} \ No newline at end of file diff --git a/erpnext/stock/report/cogs_by_item_group/cogs_by_item_group.py b/erpnext/stock/report/cogs_by_item_group/cogs_by_item_group.py new file mode 100644 index 00000000000..da593a40d68 --- /dev/null +++ b/erpnext/stock/report/cogs_by_item_group/cogs_by_item_group.py @@ -0,0 +1,188 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from collections import OrderedDict +import datetime +from typing import Dict, List, Tuple, Union + +import frappe +from frappe import _ +from frappe.utils import date_diff + +from erpnext.accounts.report.general_ledger.general_ledger import get_gl_entries + + +Filters = frappe._dict +Row = frappe._dict +Data = List[Row] +Columns = List[Dict[str, str]] +DateTime = Union[datetime.date, datetime.datetime] +FilteredEntries = List[Dict[str, Union[str, float, DateTime, None]]] +ItemGroupsDict = Dict[Tuple[int, int], Dict[str, Union[str, int]]] +SVDList = List[frappe._dict] + + +def execute(filters: Filters) -> Tuple[Columns, Data]: + update_filters_with_account(filters) + validate_filters(filters) + columns = get_columns() + data = get_data(filters) + return columns, data + + +def update_filters_with_account(filters: Filters) -> None: + account = frappe.get_value("Company", filters.get("company"), "default_expense_account") + filters.update(dict(account=account)) + + +def validate_filters(filters: Filters) -> None: + if filters.from_date > filters.to_date: + frappe.throw(_("From Date must be before To Date")) + + +def get_columns() -> Columns: + return [ + { + 'label': 'Item Group', + 'fieldname': 'item_group', + 'fieldtype': 'Data', + 'width': '200' + }, + { + 'label': 'COGS Debit', + 'fieldname': 'cogs_debit', + 'fieldtype': 'Currency', + 'width': '200' + } + ] + + +def get_data(filters: Filters) -> Data: + filtered_entries = get_filtered_entries(filters) + svd_list = get_stock_value_difference_list(filtered_entries) + leveled_dict = get_leveled_dict() + + assign_self_values(leveled_dict, svd_list) + assign_agg_values(leveled_dict) + + data = [] + for item in leveled_dict.items(): + i = item[1] + if i['agg_value'] == 0: + continue + data.append(get_row(i['name'], i['agg_value'], i['is_group'], i['level'])) + if i['self_value'] < i['agg_value'] and i['self_value'] > 0: + data.append(get_row(i['name'], i['self_value'], 0, i['level'] + 1)) + return data + + +def get_filtered_entries(filters: Filters) -> FilteredEntries: + gl_entries = get_gl_entries(filters, []) + filtered_entries = [] + for entry in gl_entries: + posting_date = entry.get('posting_date') + from_date = filters.get('from_date') + if date_diff(from_date, posting_date) > 0: + continue + filtered_entries.append(entry) + return filtered_entries + + +def get_stock_value_difference_list(filtered_entries: FilteredEntries) -> SVDList: + voucher_nos = [fe.get('voucher_no') for fe in filtered_entries] + svd_list = frappe.get_list( + 'Stock Ledger Entry', fields=['item_code','stock_value_difference'], + filters=[('voucher_no', 'in', voucher_nos)] + ) + assign_item_groups_to_svd_list(svd_list) + return svd_list + + +def get_leveled_dict() -> OrderedDict: + item_groups_dict = get_item_groups_dict() + lr_list = sorted(item_groups_dict, key=lambda x : int(x[0])) + leveled_dict = OrderedDict() + current_level = 0 + nesting_r = [] + for l, r in lr_list: + while current_level > 0 and nesting_r[-1] < l: + nesting_r.pop() + current_level -= 1 + + leveled_dict[(l,r)] = { + 'level' : current_level, + 'name' : item_groups_dict[(l,r)]['name'], + 'is_group' : item_groups_dict[(l,r)]['is_group'] + } + + if int(r) - int(l) > 1: + current_level += 1 + nesting_r.append(r) + + update_leveled_dict(leveled_dict) + return leveled_dict + + +def assign_self_values(leveled_dict: OrderedDict, svd_list: SVDList) -> None: + key_dict = {v['name']:k for k, v in leveled_dict.items()} + for item in svd_list: + key = key_dict[item.get("item_group")] + leveled_dict[key]['self_value'] += -item.get("stock_value_difference") + + +def assign_agg_values(leveled_dict: OrderedDict) -> None: + keys = list(leveled_dict.keys())[::-1] + prev_level = leveled_dict[keys[-1]]['level'] + accu = [0] + for k in keys[:-1]: + curr_level = leveled_dict[k]['level'] + if curr_level == prev_level: + accu[-1] += leveled_dict[k]['self_value'] + leveled_dict[k]['agg_value'] = leveled_dict[k]['self_value'] + + elif curr_level > prev_level: + accu.append(leveled_dict[k]['self_value']) + leveled_dict[k]['agg_value'] = accu[-1] + + elif curr_level < prev_level: + accu[-1] += leveled_dict[k]['self_value'] + leveled_dict[k]['agg_value'] = accu[-1] + + prev_level = curr_level + + # root node + rk = keys[-1] + leveled_dict[rk]['agg_value'] = sum(accu) + leveled_dict[rk]['self_value'] + + +def get_row(name:str, value:float, is_bold:int, indent:int) -> Row: + item_group = name + if is_bold: + item_group = frappe.bold(item_group) + return frappe._dict(item_group=item_group, cogs_debit=value, indent=indent) + + +def assign_item_groups_to_svd_list(svd_list: SVDList) -> None: + ig_map = get_item_groups_map(svd_list) + for item in svd_list: + item.item_group = ig_map[item.get("item_code")] + + +def get_item_groups_map(svd_list: SVDList) -> Dict[str, str]: + item_codes = set(i['item_code'] for i in svd_list) + ig_list = frappe.get_list( + 'Item', fields=['item_code','item_group'], + filters=[('item_code', 'in', item_codes)] + ) + return {i['item_code']:i['item_group'] for i in ig_list} + + +def get_item_groups_dict() -> ItemGroupsDict: + item_groups_list = frappe.get_all("Item Group", fields=("name", "is_group", "lft", "rgt")) + return {(i['lft'],i['rgt']):{'name':i['name'], 'is_group':i['is_group']} + for i in item_groups_list} + + +def update_leveled_dict(leveled_dict: OrderedDict) -> None: + for k in leveled_dict: + leveled_dict[k].update({'self_value':0, 'agg_value':0}) diff --git a/erpnext/stock/report/delayed_item_report/delayed_item_report.py b/erpnext/stock/report/delayed_item_report/delayed_item_report.py index 4fc4027200d..61306662c0d 100644 --- a/erpnext/stock/report/delayed_item_report/delayed_item_report.py +++ b/erpnext/stock/report/delayed_item_report/delayed_item_report.py @@ -174,4 +174,4 @@ class DelayedItemReport(object): "fieldname": "po_no", "fieldtype": "Data", "width": 100 - }] \ No newline at end of file + }] diff --git a/erpnext/stock/report/delayed_order_report/delayed_order_report.py b/erpnext/stock/report/delayed_order_report/delayed_order_report.py index 79dc5d88219..d9151606884 100644 --- a/erpnext/stock/report/delayed_order_report/delayed_order_report.py +++ b/erpnext/stock/report/delayed_order_report/delayed_order_report.py @@ -87,4 +87,4 @@ class DelayedOrderReport(DelayedItemReport): "fieldname": "po_no", "fieldtype": "Data", "width": 110 - }] \ No newline at end of file + }] diff --git a/erpnext/stock/report/delivery_note_trends/delivery_note_trends.js b/erpnext/stock/report/delivery_note_trends/delivery_note_trends.js index ade004cde42..8a04565c197 100644 --- a/erpnext/stock/report/delivery_note_trends/delivery_note_trends.js +++ b/erpnext/stock/report/delivery_note_trends/delivery_note_trends.js @@ -6,4 +6,3 @@ frappe.require("assets/erpnext/js/sales_trends_filters.js", function() { filters: erpnext.get_sales_trends_filters() } }); - diff --git a/erpnext/stock/report/delivery_note_trends/delivery_note_trends.py b/erpnext/stock/report/delivery_note_trends/delivery_note_trends.py index 446d3049b71..77fd2ff2447 100644 --- a/erpnext/stock/report/delivery_note_trends/delivery_note_trends.py +++ b/erpnext/stock/report/delivery_note_trends/delivery_note_trends.py @@ -47,4 +47,4 @@ def get_chart_data(data, filters): ] }, "type" : "bar" - } \ No newline at end of file + } diff --git a/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.py b/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.py index cf174c93682..00125e71a9a 100644 --- a/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.py +++ b/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.py @@ -108,4 +108,4 @@ def get_columns(): 'fieldtype': 'Float', 'fieldname': 'differnce', 'width': 110 - }] \ No newline at end of file + }] diff --git a/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.py b/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.py index e54cf4c66c7..b3b7594ffd7 100644 --- a/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.py +++ b/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.py @@ -145,4 +145,4 @@ def get_columns(): 'fieldtype': 'Currency', 'fieldname': 'valuation_rate', 'width': 110 - }] \ No newline at end of file + }] diff --git a/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py index a7243878eb8..c8f60a15d64 100644 --- a/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py +++ b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py @@ -138,4 +138,4 @@ def get_columns(filters): "fieldtype": "Currency", "width": "150" } - ] \ No newline at end of file + ] diff --git a/erpnext/stock/report/item_price_stock/item_price_stock.js b/erpnext/stock/report/item_price_stock/item_price_stock.js index 0bbc61b9dbf..7af1dab6a0b 100644 --- a/erpnext/stock/report/item_price_stock/item_price_stock.js +++ b/erpnext/stock/report/item_price_stock/item_price_stock.js @@ -11,4 +11,4 @@ frappe.query_reports["Item Price Stock"] = { "options": "Item" } ] -} \ No newline at end of file +} diff --git a/erpnext/stock/report/item_shortage_report/item_shortage_report.py b/erpnext/stock/report/item_shortage_report/item_shortage_report.py index 086d833bbc4..c67eed7e926 100644 --- a/erpnext/stock/report/item_shortage_report/item_shortage_report.py +++ b/erpnext/stock/report/item_shortage_report/item_shortage_report.py @@ -158,5 +158,3 @@ def get_columns(): ] return columns - - diff --git a/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.js b/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.js index c0535bf0efa..173aad6d5a9 100644 --- a/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.js +++ b/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.js @@ -29,4 +29,4 @@ frappe.query_reports["Itemwise Recommended Reorder Level"] = { "options": "Brand" } ] -} \ No newline at end of file +} diff --git a/erpnext/stock/report/process_loss_report/__init__.py b/erpnext/stock/report/process_loss_report/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/stock/report/process_loss_report/process_loss_report.js b/erpnext/stock/report/process_loss_report/process_loss_report.js new file mode 100644 index 00000000000..b0c2b94a254 --- /dev/null +++ b/erpnext/stock/report/process_loss_report/process_loss_report.js @@ -0,0 +1,44 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Process Loss Report"] = { + filters: [ + { + label: __("Company"), + fieldname: "company", + fieldtype: "Link", + options: "Company", + mandatory: true, + default: frappe.defaults.get_user_default("Company"), + }, + { + label: __("Item"), + fieldname: "item", + fieldtype: "Link", + options: "Item", + mandatory: false, + }, + { + label: __("Work Order"), + fieldname: "work_order", + fieldtype: "Link", + options: "Work Order", + mandatory: false, + }, + { + label: __("From Date"), + fieldname: "from_date", + fieldtype: "Date", + mandatory: true, + default: frappe.datetime.year_start(), + }, + { + label: __("To Date"), + fieldname: "to_date", + fieldtype: "Date", + mandatory: true, + default: frappe.datetime.get_today(), + }, + ] +}; diff --git a/erpnext/stock/report/process_loss_report/process_loss_report.json b/erpnext/stock/report/process_loss_report/process_loss_report.json new file mode 100644 index 00000000000..afe4aff7f1c --- /dev/null +++ b/erpnext/stock/report/process_loss_report/process_loss_report.json @@ -0,0 +1,29 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2021-08-24 16:38:15.233395", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "modified": "2021-08-24 16:38:15.233395", + "modified_by": "Administrator", + "module": "Stock", + "name": "Process Loss Report", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Work Order", + "report_name": "Process Loss Report", + "report_type": "Script Report", + "roles": [ + { + "role": "Manufacturing User" + }, + { + "role": "Stock User" + } + ] +} \ No newline at end of file diff --git a/erpnext/stock/report/process_loss_report/process_loss_report.py b/erpnext/stock/report/process_loss_report/process_loss_report.py new file mode 100644 index 00000000000..3d48ebde1a5 --- /dev/null +++ b/erpnext/stock/report/process_loss_report/process_loss_report.py @@ -0,0 +1,133 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ +from typing import Dict, List, Tuple + +Filters = frappe._dict +Row = frappe._dict +Data = List[Row] +Columns = List[Dict[str, str]] +QueryArgs = Dict[str, str] + +def execute(filters: Filters) -> Tuple[Columns, Data]: + columns = get_columns() + data = get_data(filters) + return columns, data + +def get_data(filters: Filters) -> Data: + query_args = get_query_args(filters) + data = run_query(query_args) + update_data_with_total_pl_value(data) + return data + +def get_columns() -> Columns: + return [ + { + 'label': _('Work Order'), + 'fieldname': 'name', + 'fieldtype': 'Link', + 'options': 'Work Order', + 'width': '200' + }, + { + 'label': _('Item'), + 'fieldname': 'production_item', + 'fieldtype': 'Link', + 'options': 'Item', + 'width': '100' + }, + { + 'label': _('Status'), + 'fieldname': 'status', + 'fieldtype': 'Data', + 'width': '100' + }, + { + 'label': _('Manufactured Qty'), + 'fieldname': 'produced_qty', + 'fieldtype': 'Float', + 'width': '150' + }, + { + 'label': _('Loss Qty'), + 'fieldname': 'process_loss_qty', + 'fieldtype': 'Float', + 'width': '150' + }, + { + 'label': _('Actual Manufactured Qty'), + 'fieldname': 'actual_produced_qty', + 'fieldtype': 'Float', + 'width': '150' + }, + { + 'label': _('Loss Value'), + 'fieldname': 'total_pl_value', + 'fieldtype': 'Float', + 'width': '150' + }, + { + 'label': _('FG Value'), + 'fieldname': 'total_fg_value', + 'fieldtype': 'Float', + 'width': '150' + }, + { + 'label': _('Raw Material Value'), + 'fieldname': 'total_rm_value', + 'fieldtype': 'Float', + 'width': '150' + } + ] + +def get_query_args(filters: Filters) -> QueryArgs: + query_args = {} + query_args.update(filters) + query_args.update( + get_filter_conditions(filters) + ) + return query_args + +def run_query(query_args: QueryArgs) -> Data: + return frappe.db.sql(""" + SELECT + wo.name, wo.status, wo.production_item, wo.qty, + wo.produced_qty, wo.process_loss_qty, + (wo.produced_qty - wo.process_loss_qty) as actual_produced_qty, + sum(se.total_incoming_value) as total_fg_value, + sum(se.total_outgoing_value) as total_rm_value + FROM + `tabWork Order` wo INNER JOIN `tabStock Entry` se + ON wo.name=se.work_order + WHERE + process_loss_qty > 0 + AND wo.company = %(company)s + AND se.docstatus = 1 + AND se.posting_date BETWEEN %(from_date)s AND %(to_date)s + {item_filter} + {work_order_filter} + GROUP BY + se.work_order + """.format(**query_args), query_args, as_dict=1, debug=1) + +def update_data_with_total_pl_value(data: Data) -> None: + for row in data: + value_per_unit_fg = row['total_fg_value'] / row['actual_produced_qty'] + row['total_pl_value'] = row['process_loss_qty'] * value_per_unit_fg + +def get_filter_conditions(filters: Filters) -> QueryArgs: + filter_conditions = dict(item_filter="", work_order_filter="") + if "item" in filters: + production_item = filters.get("item") + filter_conditions.update( + {"item_filter": f"AND wo.production_item='{production_item}'"} + ) + if "work_order" in filters: + work_order_name = filters.get("work_order") + filter_conditions.update( + {"work_order_filter": f"AND wo.name='{work_order_name}'"} + ) + return filter_conditions + diff --git a/erpnext/stock/report/purchase_receipt_trends/purchase_receipt_trends.js b/erpnext/stock/report/purchase_receipt_trends/purchase_receipt_trends.js index d16485e8cc6..695efacb694 100644 --- a/erpnext/stock/report/purchase_receipt_trends/purchase_receipt_trends.js +++ b/erpnext/stock/report/purchase_receipt_trends/purchase_receipt_trends.js @@ -6,4 +6,3 @@ frappe.require("assets/erpnext/js/purchase_trends_filters.js", function() { filters: erpnext.get_purchase_trends_filters() } }); - diff --git a/erpnext/stock/report/purchase_receipt_trends/purchase_receipt_trends.py b/erpnext/stock/report/purchase_receipt_trends/purchase_receipt_trends.py index 8227f1548c1..0d96ea6aa8e 100644 --- a/erpnext/stock/report/purchase_receipt_trends/purchase_receipt_trends.py +++ b/erpnext/stock/report/purchase_receipt_trends/purchase_receipt_trends.py @@ -48,4 +48,4 @@ def get_chart_data(data, filters): }, "type" : "bar", "colors":["#5e64ff"] - } \ No newline at end of file + } diff --git a/erpnext/stock/report/serial_no_ledger/serial_no_ledger.py b/erpnext/stock/report/serial_no_ledger/serial_no_ledger.py index c3339fd341e..cc3aa3522d8 100644 --- a/erpnext/stock/report/serial_no_ledger/serial_no_ledger.py +++ b/erpnext/stock/report/serial_no_ledger/serial_no_ledger.py @@ -50,4 +50,3 @@ def get_columns(filters): def get_data(filters): return get_stock_ledger_entries(filters, '<=', order="asc") or [] - diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.js b/erpnext/stock/report/stock_ageing/stock_ageing.js index 8495142ba5b..b22788f7a29 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing.js +++ b/erpnext/stock/report/stock_ageing/stock_ageing.js @@ -64,4 +64,4 @@ frappe.query_reports["Stock Ageing"] = { "default": 0 } ] -} \ No newline at end of file +} diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py index 623dc2ffd97..8a9f0a5e583 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/stock_ageing.py @@ -26,7 +26,7 @@ def execute(filters=None): average_age = get_average_age(fifo_queue, to_date) earliest_age = date_diff(to_date, fifo_queue[0][1]) latest_age = date_diff(to_date, fifo_queue[-1][1]) - range1, range2, range3, above_range3 = get_range_age(filters, fifo_queue, to_date) + range1, range2, range3, above_range3 = get_range_age(filters, fifo_queue, to_date, item_dict) row = [details.name, details.item_name, details.description, details.item_group, details.brand] @@ -58,19 +58,21 @@ def get_average_age(fifo_queue, to_date): return flt(age_qty / total_qty, 2) if total_qty else 0.0 -def get_range_age(filters, fifo_queue, to_date): +def get_range_age(filters, fifo_queue, to_date, item_dict): range1 = range2 = range3 = above_range3 = 0.0 + for item in fifo_queue: age = date_diff(to_date, item[1]) + qty = flt(item[0]) if not item_dict["has_serial_no"] else 1.0 if age <= filters.range1: - range1 += flt(item[0]) + range1 += qty elif age <= filters.range2: - range2 += flt(item[0]) + range2 += qty elif age <= filters.range3: - range3 += flt(item[0]) + range3 += qty else: - above_range3 += flt(item[0]) + above_range3 += qty return range1, range2, range3, above_range3 @@ -197,9 +199,7 @@ def get_fifo_queue(filters, sle=None): fifo_queue.append([d.actual_qty, d.posting_date]) else: if serial_no_list: - for serial_no in fifo_queue: - if serial_no[0] in serial_no_list: - fifo_queue.remove(serial_no) + fifo_queue[:] = [serial_no for serial_no in fifo_queue if serial_no[0] not in serial_no_list] else: qty_to_pop = abs(d.actual_qty) while qty_to_pop: @@ -222,14 +222,16 @@ def get_fifo_queue(filters, sle=None): else: item_details[key]["total_qty"] += d.actual_qty + item_details[key]["has_serial_no"] = d.has_serial_no + return item_details def get_stock_ledger_entries(filters): return frappe.db.sql("""select - item.name, item.item_name, item_group, brand, description, item.stock_uom, + item.name, item.item_name, item_group, brand, description, item.stock_uom, item.has_serial_no, actual_qty, posting_date, voucher_type, voucher_no, serial_no, batch_no, qty_after_transaction, warehouse from `tabStock Ledger Entry` sle, - (select name, item_name, description, stock_uom, brand, item_group + (select name, item_name, description, stock_uom, brand, item_group, has_serial_no from `tabItem` {item_conditions}) item where item_code = item.name and company = %(company)s and diff --git a/erpnext/stock/report/stock_analytics/stock_analytics.js b/erpnext/stock/report/stock_analytics/stock_analytics.js index 6b384e28611..78afe6d2642 100644 --- a/erpnext/stock/report/stock_analytics/stock_analytics.js +++ b/erpnext/stock/report/stock_analytics/stock_analytics.js @@ -36,12 +36,26 @@ frappe.query_reports["Stock Analytics"] = { options:"Brand", default: "", }, + { + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + reqd: 1, + }, { fieldname: "warehouse", label: __("Warehouse"), fieldtype: "Link", - options:"Warehouse", + options: "Warehouse", default: "", + get_query: function() { + const company = frappe.query_report.get_filter_value('company'); + return { + filters: { 'company': company } + } + } }, { fieldname: "from_date", diff --git a/erpnext/stock/report/stock_analytics/stock_analytics.py b/erpnext/stock/report/stock_analytics/stock_analytics.py index 0cc8ca48aac..a1e1e7fce7c 100644 --- a/erpnext/stock/report/stock_analytics/stock_analytics.py +++ b/erpnext/stock/report/stock_analytics/stock_analytics.py @@ -1,14 +1,15 @@ # Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt +import datetime -from __future__ import unicode_literals import frappe from frappe import _, scrub -from frappe.utils import getdate, flt +from frappe.utils import getdate, get_quarter_start, get_first_day_of_week +from frappe.utils import get_first_day as get_first_day_of_month + from erpnext.stock.report.stock_balance.stock_balance import (get_items, get_stock_ledger_entries, get_item_details) from erpnext.accounts.utils import get_fiscal_year from erpnext.stock.utils import is_reposting_item_valuation_in_progress -from six import iteritems def execute(filters=None): is_reposting_item_valuation_in_progress() @@ -71,7 +72,8 @@ def get_columns(filters): def get_period_date_ranges(filters): from dateutil.relativedelta import relativedelta - from_date, to_date = getdate(filters.from_date), getdate(filters.to_date) + from_date = round_down_to_nearest_frequency(filters.from_date, filters.range) + to_date = getdate(filters.to_date) increment = { "Monthly": 1, @@ -97,6 +99,31 @@ def get_period_date_ranges(filters): return periodic_daterange + +def round_down_to_nearest_frequency(date: str, frequency: str) -> datetime.datetime: + """Rounds down the date to nearest frequency unit. + example: + + >>> round_down_to_nearest_frequency("2021-02-21", "Monthly") + datetime.datetime(2021, 2, 1) + + >>> round_down_to_nearest_frequency("2021-08-21", "Yearly") + datetime.datetime(2021, 1, 1) + """ + + def _get_first_day_of_fiscal_year(date): + fiscal_year = get_fiscal_year(date) + return fiscal_year and fiscal_year[1] or date + + round_down_function = { + "Monthly": get_first_day_of_month, + "Quarterly": get_quarter_start, + "Weekly": get_first_day_of_week, + "Yearly": _get_first_day_of_fiscal_year, + }.get(frequency, getdate) + return round_down_function(date) + + def get_period(posting_date, filters): months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] @@ -114,14 +141,42 @@ def get_period(posting_date, filters): def get_periodic_data(entry, filters): + """Structured as: + Item 1 + - Balance (updated and carried forward): + - Warehouse A : bal_qty/value + - Warehouse B : bal_qty/value + - Jun 2021 (sum of warehouse quantities used in report) + - Warehouse A : bal_qty/value + - Warehouse B : bal_qty/value + - Jul 2021 (sum of warehouse quantities used in report) + - Warehouse A : bal_qty/value + - Warehouse B : bal_qty/value + Item 2 + - Balance (updated and carried forward): + - Warehouse A : bal_qty/value + - Warehouse B : bal_qty/value + - Jun 2021 (sum of warehouse quantities used in report) + - Warehouse A : bal_qty/value + - Warehouse B : bal_qty/value + - Jul 2021 (sum of warehouse quantities used in report) + - Warehouse A : bal_qty/value + - Warehouse B : bal_qty/value + """ periodic_data = {} for d in entry: period = get_period(d.posting_date, filters) bal_qty = 0 + # if period against item does not exist yet, instantiate it + # insert existing balance dict against period, and add/subtract to it + if periodic_data.get(d.item_code) and not periodic_data.get(d.item_code).get(period): + previous_balance = periodic_data[d.item_code]['balance'].copy() + periodic_data[d.item_code][period] = previous_balance + if d.voucher_type == "Stock Reconciliation": - if periodic_data.get(d.item_code): - bal_qty = periodic_data[d.item_code]["balance"] + if periodic_data.get(d.item_code) and periodic_data.get(d.item_code).get('balance').get(d.warehouse): + bal_qty = periodic_data[d.item_code]['balance'][d.warehouse] qty_diff = d.qty_after_transaction - bal_qty else: @@ -132,12 +187,12 @@ def get_periodic_data(entry, filters): else: value = d.stock_value_difference - periodic_data.setdefault(d.item_code, {}).setdefault(period, 0.0) - periodic_data.setdefault(d.item_code, {}).setdefault("balance", 0.0) - - periodic_data[d.item_code]["balance"] += value - periodic_data[d.item_code][period] = periodic_data[d.item_code]["balance"] + # period-warehouse wise balance + periodic_data.setdefault(d.item_code, {}).setdefault('balance', {}).setdefault(d.warehouse, 0.0) + periodic_data.setdefault(d.item_code, {}).setdefault(period, {}).setdefault(d.warehouse, 0.0) + periodic_data[d.item_code]['balance'][d.warehouse] += value + periodic_data[d.item_code][period][d.warehouse] = periodic_data[d.item_code]['balance'][d.warehouse] return periodic_data @@ -149,7 +204,7 @@ def get_data(filters): periodic_data = get_periodic_data(sle, filters) ranges = get_period_date_ranges(filters) - for dummy, item_data in iteritems(item_details): + for dummy, item_data in item_details.items(): row = { "name": item_data.name, "item_name": item_data.item_name, @@ -160,7 +215,8 @@ def get_data(filters): total = 0 for dummy, end_date in ranges: period = get_period(end_date, filters) - amount = flt(periodic_data.get(item_data.name, {}).get(period)) + period_data = periodic_data.get(item_data.name, {}).get(period) + amount = sum(period_data.values()) if period_data else 0 row[scrub(period)] = amount total += amount row["total"] = total @@ -179,7 +235,3 @@ def get_chart_data(columns): chart["type"] = "line" return chart - - - - diff --git a/erpnext/stock/report/stock_analytics/test_stock_analytics.py b/erpnext/stock/report/stock_analytics/test_stock_analytics.py new file mode 100644 index 00000000000..00e268b4e0e --- /dev/null +++ b/erpnext/stock/report/stock_analytics/test_stock_analytics.py @@ -0,0 +1,35 @@ +import datetime +import unittest + +from frappe import _dict +from erpnext.accounts.utils import get_fiscal_year + +from erpnext.stock.report.stock_analytics.stock_analytics import get_period_date_ranges + + +class TestStockAnalyticsReport(unittest.TestCase): + def test_get_period_date_ranges(self): + + filters = _dict(range="Monthly", from_date="2020-12-28", to_date="2021-02-06") + + ranges = get_period_date_ranges(filters) + + expected_ranges = [ + [datetime.date(2020, 12, 1), datetime.date(2020, 12, 31)], + [datetime.date(2021, 1, 1), datetime.date(2021, 1, 31)], + [datetime.date(2021, 2, 1), datetime.date(2021, 2, 6)], + ] + + self.assertEqual(ranges, expected_ranges) + + def test_get_period_date_ranges_yearly(self): + + filters = _dict(range="Yearly", from_date="2021-01-28", to_date="2021-02-06") + + ranges = get_period_date_ranges(filters) + first_date = get_fiscal_year("2021-01-28")[1] + expected_ranges = [ + [first_date, datetime.date(2021, 2, 6)], + ] + + self.assertEqual(ranges, expected_ranges) diff --git a/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py b/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py index 14d543b1740..7e0c0e8ab35 100644 --- a/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py +++ b/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py @@ -22,6 +22,7 @@ def get_data(report_filters): data = [] filters = { + "is_cancelled": 0, "company": report_filters.company, "posting_date": ("<=", report_filters.as_on_date) } @@ -34,7 +35,7 @@ def get_data(report_filters): key = (d.voucher_type, d.voucher_no) gl_data = voucher_wise_gl_data.get(key) or {} d.account_value = gl_data.get("account_value", 0) - d.difference_value = (d.stock_value - d.account_value) + d.difference_value = abs(d.stock_value - d.account_value) if abs(d.difference_value) > 0.1: data.append(d) @@ -127,4 +128,4 @@ def get_columns(filters): "fieldtype": "Currency", "width": "120" } - ] \ No newline at end of file + ] diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index b6a80631892..fc3d719a780 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -16,8 +16,6 @@ def execute(filters=None): is_reposting_item_valuation_in_progress() if not filters: filters = {} - validate_filters(filters) - from_date = filters.get('from_date') to_date = filters.get('to_date') @@ -237,12 +235,15 @@ def filter_items_with_no_transactions(iwb_map, float_precision): return iwb_map def get_items(filters): + "Get items based on item code, item group or brand." conditions = [] if filters.get("item_code"): conditions.append("item.name=%(item_code)s") else: if filters.get("item_group"): conditions.append(get_item_group_condition(filters.get("item_group"))) + if filters.get("brand"): # used in stock analytics report + conditions.append("item.brand=%(brand)s") items = [] if conditions: @@ -295,12 +296,6 @@ def get_item_reorder_details(items): return dict((d.parent + d.warehouse, d) for d in item_reorder_details) -def validate_filters(filters): - if not (filters.get("item_code") or filters.get("warehouse")): - sle_count = flt(frappe.db.sql("""select count(name) from `tabStock Ledger Entry`""")[0][0]) - if sle_count > 500000: - frappe.throw(_("Please set filter based on Item or Warehouse due to a large amount of entries.")) - def get_variants_attributes(): '''Return all item variant attributes.''' return [i.name for i in frappe.get_all('Item Attribute')] diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py index 8909f217f49..b6923e97c4f 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.py +++ b/erpnext/stock/report/stock_ledger/stock_ledger.py @@ -23,6 +23,7 @@ def execute(filters=None): conversion_factors = [] if opening_row: data.append(opening_row) + conversion_factors.append(0) actual_qty = stock_value = 0 diff --git a/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py b/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py index 808d2791709..7956f2e8648 100644 --- a/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py +++ b/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py @@ -32,7 +32,7 @@ def execute(filters=None): if filters.brand and filters.brand != item.brand: continue - + elif filters.item_group and filters.item_group != item.item_group: continue diff --git a/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py b/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py index 78e95df9898..fa19eeba58b 100644 --- a/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py +++ b/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py @@ -58,14 +58,14 @@ def get_data(warehouse): serial_item_list = frappe.get_all("Item", filters={ 'has_serial_no': True, }, fields=['item_code', 'item_name']) - + status_list = ['Active', 'Expired'] data = [] for item in serial_item_list: - total_serial_no = frappe.db.count("Serial No", + total_serial_no = frappe.db.count("Serial No", filters={"item_code": item.item_code, "status": ("in", status_list), "warehouse": warehouse}) - actual_qty = frappe.db.get_value('Bin', fieldname=['actual_qty'], + actual_qty = frappe.db.get_value('Bin', fieldname=['actual_qty'], filters={"warehouse": warehouse, "item_code": item.item_code}) # frappe.db.get_value returns null if no record exist. @@ -84,4 +84,4 @@ def get_data(warehouse): data.append(row) - return data \ No newline at end of file + return data diff --git a/erpnext/stock/report/supplier_wise_sales_analytics/supplier_wise_sales_analytics.js b/erpnext/stock/report/supplier_wise_sales_analytics/supplier_wise_sales_analytics.js index cdc9895917c..5b006470756 100644 --- a/erpnext/stock/report/supplier_wise_sales_analytics/supplier_wise_sales_analytics.js +++ b/erpnext/stock/report/supplier_wise_sales_analytics/supplier_wise_sales_analytics.js @@ -25,4 +25,4 @@ frappe.query_reports["Supplier-Wise Sales Analytics"] = { "default": frappe.datetime.month_end() }, ] -} \ No newline at end of file +} diff --git a/erpnext/stock/report/supplier_wise_sales_analytics/supplier_wise_sales_analytics.py b/erpnext/stock/report/supplier_wise_sales_analytics/supplier_wise_sales_analytics.py index 5873a7a3008..4108a575542 100644 --- a/erpnext/stock/report/supplier_wise_sales_analytics/supplier_wise_sales_analytics.py +++ b/erpnext/stock/report/supplier_wise_sales_analytics/supplier_wise_sales_analytics.py @@ -69,7 +69,7 @@ def get_consumed_details(filters): i.stock_uom, sle.actual_qty, sle.stock_value_difference, sle.voucher_no, sle.voucher_type from `tabStock Ledger Entry` sle, `tabItem` i - where sle.item_code=i.name and sle.actual_qty < 0 %s""" % conditions, values, as_dict=1): + where sle.is_cancelled = 0 and sle.item_code=i.name and sle.actual_qty < 0 %s""" % conditions, values, as_dict=1): consumed_details.setdefault(d.item_code, []).append(d) return consumed_details diff --git a/erpnext/stock/report/total_stock_summary/total_stock_summary.js b/erpnext/stock/report/total_stock_summary/total_stock_summary.js index 264642856da..90648f1b249 100644 --- a/erpnext/stock/report/total_stock_summary/total_stock_summary.js +++ b/erpnext/stock/report/total_stock_summary/total_stock_summary.js @@ -38,4 +38,4 @@ frappe.query_reports["Total Stock Summary"] = { "reqd": 1 }, ] -} \ No newline at end of file +} diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 4e9c7689ae4..e98df737cb7 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -6,13 +6,14 @@ import frappe import erpnext import copy from frappe import _ -from frappe.utils import cint, flt, cstr, now, get_link_to_form +from frappe.utils import cint, flt, cstr, now, get_link_to_form, getdate from frappe.model.meta import get_field_precision from erpnext.stock.utils import get_valuation_method, get_incoming_outgoing_rate_for_cancel from erpnext.stock.utils import get_bin import json from six import iteritems + # future reposting class NegativeStockError(frappe.ValidationError): pass class SerialNoExistsInFutureTransaction(frappe.ValidationError): @@ -126,29 +127,61 @@ def make_entry(args, allow_negative_stock=False, via_landed_cost_voucher=False): sle.submit() return sle -def repost_future_sle(args=None, voucher_type=None, voucher_no=None, allow_negative_stock=None, via_landed_cost_voucher=False): +def repost_future_sle(args=None, doc=None, voucher_type=None, voucher_no=None, allow_negative_stock=None, via_landed_cost_voucher=False): if not args and voucher_type and voucher_no: - args = get_args_for_voucher(voucher_type, voucher_no) + args = get_items_to_be_repost(voucher_type, voucher_no, doc) - distinct_item_warehouses = [(d.item_code, d.warehouse) for d in args] + distinct_item_warehouses = get_distinct_item_warehouse(args, doc) - i = 0 + i = get_current_index(doc) or 0 while i < len(args): obj = update_entries_after({ - "item_code": args[i].item_code, - "warehouse": args[i].warehouse, - "posting_date": args[i].posting_date, - "posting_time": args[i].posting_time, - "creation": args[i].get("creation") + "item_code": args[i].get('item_code'), + "warehouse": args[i].get('warehouse'), + "posting_date": args[i].get('posting_date'), + "posting_time": args[i].get('posting_time'), + "creation": args[i].get("creation"), + "distinct_item_warehouses": distinct_item_warehouses }, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher) - for item_wh, new_sle in iteritems(obj.new_items): - if item_wh not in distinct_item_warehouses: - args.append(new_sle) + distinct_item_warehouses[(args[i].get('item_code'), args[i].get('warehouse'))].reposting_status = True + if obj.new_items_found: + for item_wh, data in iteritems(distinct_item_warehouses): + if ('args_idx' not in data and not data.reposting_status) or (data.sle_changed and data.reposting_status): + data.args_idx = len(args) + args.append(data.sle) + elif data.sle_changed and not data.reposting_status: + args[data.args_idx] = data.sle + + data.sle_changed = False i += 1 -def get_args_for_voucher(voucher_type, voucher_no): + if doc and i % 2 == 0: + update_args_in_repost_item_valuation(doc, i, args, distinct_item_warehouses) + + if doc and args: + update_args_in_repost_item_valuation(doc, i, args, distinct_item_warehouses) + +def update_args_in_repost_item_valuation(doc, index, args, distinct_item_warehouses): + frappe.db.set_value(doc.doctype, doc.name, { + 'items_to_be_repost': json.dumps(args, default=str), + 'distinct_item_and_warehouse': json.dumps({str(k): v for k,v in distinct_item_warehouses.items()}, default=str), + 'current_index': index + }) + + frappe.db.commit() + + frappe.publish_realtime('item_reposting_progress', { + 'name': doc.name, + 'items_to_be_repost': json.dumps(args, default=str), + 'current_index': index + }) + +def get_items_to_be_repost(voucher_type, voucher_no, doc=None): + if doc and doc.items_to_be_repost: + return json.loads(doc.items_to_be_repost) or [] + return frappe.db.get_all("Stock Ledger Entry", filters={"voucher_type": voucher_type, "voucher_no": voucher_no}, fields=["item_code", "warehouse", "posting_date", "posting_time", "creation"], @@ -156,6 +189,25 @@ def get_args_for_voucher(voucher_type, voucher_no): group_by="item_code, warehouse" ) +def get_distinct_item_warehouse(args=None, doc=None): + distinct_item_warehouses = {} + if doc and doc.distinct_item_and_warehouse: + distinct_item_warehouses = json.loads(doc.distinct_item_and_warehouse) + distinct_item_warehouses = {frappe.safe_eval(k): frappe._dict(v) for k, v in distinct_item_warehouses.items()} + else: + for i, d in enumerate(args): + distinct_item_warehouses.setdefault((d.item_code, d.warehouse), frappe._dict({ + "reposting_status": False, + "sle": d, + "args_idx": i + })) + + return distinct_item_warehouses + +def get_current_index(doc=None): + if doc and doc.current_index: + return doc.current_index + class update_entries_after(object): """ update valution rate and qty after transaction @@ -186,11 +238,12 @@ class update_entries_after(object): self.company = frappe.get_cached_value("Warehouse", self.args.warehouse, "company") self.get_precision() self.valuation_method = get_valuation_method(self.item_code) - self.new_items = {} + + self.new_items_found = False + self.distinct_item_warehouses = args.get("distinct_item_warehouses", frappe._dict()) self.data = frappe._dict() self.initialize_previous_data(self.args) - self.build() def get_precision(self): @@ -273,6 +326,7 @@ class update_entries_after(object): where item_code = %(item_code)s and warehouse = %(warehouse)s + and is_cancelled = 0 and timestamp(posting_date, time_format(posting_time, %(time_format)s)) = timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s)) order by @@ -296,11 +350,29 @@ class update_entries_after(object): elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse == self.args.warehouse: return entries_to_fix elif dependant_sle.item_code != self.item_code: - if (dependant_sle.item_code, dependant_sle.warehouse) not in self.new_items: - self.new_items[(dependant_sle.item_code, dependant_sle.warehouse)] = dependant_sle + self.update_distinct_item_warehouses(dependant_sle) return entries_to_fix elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse in self.data: return entries_to_fix + else: + return self.append_future_sle_for_dependant(dependant_sle, entries_to_fix) + + def update_distinct_item_warehouses(self, dependant_sle): + key = (dependant_sle.item_code, dependant_sle.warehouse) + val = frappe._dict({ + "sle": dependant_sle + }) + if key not in self.distinct_item_warehouses: + self.distinct_item_warehouses[key] = val + self.new_items_found = True + else: + existing_sle_posting_date = self.distinct_item_warehouses[key].get("sle", {}).get("posting_date") + if getdate(dependant_sle.posting_date) < getdate(existing_sle_posting_date): + val.sle_changed = True + self.distinct_item_warehouses[key] = val + self.new_items_found = True + + def append_future_sle_for_dependant(self, dependant_sle, entries_to_fix): self.initialize_previous_data(dependant_sle) args = self.data[dependant_sle.warehouse].previous_sle \ @@ -393,6 +465,7 @@ class update_entries_after(object): rate = 0 # Material Transfer, Repack, Manufacturing if sle.voucher_type == "Stock Entry": + self.recalculate_amounts_in_stock_entry(sle.voucher_no) rate = frappe.db.get_value("Stock Entry Detail", sle.voucher_detail_no, "valuation_rate") # Sales and Purchase Return elif sle.voucher_type in ("Purchase Receipt", "Purchase Invoice", "Delivery Note", "Sales Invoice"): @@ -442,7 +515,11 @@ class update_entries_after(object): frappe.db.set_value("Stock Entry Detail", sle.voucher_detail_no, "basic_rate", outgoing_rate) # Update outgoing item's rate, recalculate FG Item's rate and total incoming/outgoing amount - stock_entry = frappe.get_doc("Stock Entry", sle.voucher_no, for_update=True) + if not sle.dependant_sle_voucher_detail_no: + self.recalculate_amounts_in_stock_entry(sle.voucher_no) + + def recalculate_amounts_in_stock_entry(self, voucher_no): + stock_entry = frappe.get_doc("Stock Entry", voucher_no, for_update=True) stock_entry.calculate_rate_and_amount(reset_outgoing_rate=False, raise_error_if_no_rate=False) stock_entry.db_update() for d in stock_entry.items: @@ -872,7 +949,7 @@ def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no, return valuation_rate -def update_qty_in_future_sle(args, allow_negative_stock=None): +def update_qty_in_future_sle(args, allow_negative_stock=False): """Recalculate Qty after Transaction in future SLEs based on current SLE.""" datetime_limit_condition = "" qty_shift = args.actual_qty @@ -961,8 +1038,8 @@ def get_datetime_limit_condition(detail): ) )""" -def validate_negative_qty_in_future_sle(args, allow_negative_stock=None): - allow_negative_stock = allow_negative_stock \ +def validate_negative_qty_in_future_sle(args, allow_negative_stock=False): + allow_negative_stock = cint(allow_negative_stock) \ or cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock")) if (args.actual_qty < 0 or args.voucher_type == "Stock Reconciliation") and not allow_negative_stock: diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index 8a6a3a3e4a0..9f6d0a8addd 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -224,7 +224,7 @@ def get_avg_purchase_rate(serial_nos): def get_valuation_method(item_code): """get valuation method from item or default""" - val_method = frappe.db.get_value('Item', item_code, 'valuation_method') + val_method = frappe.db.get_value('Item', item_code, 'valuation_method', cache=True) if not val_method: val_method = frappe.db.get_value("Stock Settings", None, "valuation_method") or "FIFO" return val_method @@ -275,17 +275,17 @@ def get_valid_serial_nos(sr_nos, qty=0, item_code=''): return valid_serial_nos def validate_warehouse_company(warehouse, company): - warehouse_company = frappe.db.get_value("Warehouse", warehouse, "company") + warehouse_company = frappe.db.get_value("Warehouse", warehouse, "company", cache=True) if warehouse_company and warehouse_company != company: frappe.throw(_("Warehouse {0} does not belong to company {1}").format(warehouse, company), InvalidWarehouseCompany) def is_group_warehouse(warehouse): - if frappe.db.get_value("Warehouse", warehouse, "is_group"): + if frappe.db.get_value("Warehouse", warehouse, "is_group", cache=True): frappe.throw(_("Group node warehouse is not allowed to select for transactions")) def validate_disabled_warehouse(warehouse): - if frappe.db.get_value("Warehouse", warehouse, "disabled"): + if frappe.db.get_value("Warehouse", warehouse, "disabled", cache=True): frappe.throw(_("Disabled Warehouse {0} cannot be used for this transaction.").format(get_link_to_form('Warehouse', warehouse))) def update_included_uom_in_report(columns, result, include_uom, conversion_factors): @@ -314,13 +314,16 @@ def update_included_uom_in_report(columns, result, include_uom, conversion_facto for row_idx, row in enumerate(result): data = row.items() if is_dict_obj else enumerate(row) for key, value in data: - if key not in convertible_columns or not conversion_factors[row_idx-1]: + if key not in convertible_columns: continue + # If no conversion factor for the UOM, defaults to 1 + if not conversion_factors[row_idx]: + conversion_factors[row_idx] = 1 if convertible_columns.get(key) == 'rate': - new_value = flt(value) * conversion_factors[row_idx-1] + new_value = flt(value) * conversion_factors[row_idx] else: - new_value = flt(value) / conversion_factors[row_idx-1] + new_value = flt(value) / conversion_factors[row_idx] if not is_dict_obj: row.insert(key+1, new_value) @@ -386,4 +389,4 @@ def is_reposting_item_valuation_in_progress(): reposting_in_progress = frappe.db.exists("Repost Item Valuation", {'docstatus': 1, 'status': ['in', ['Queued','In Progress']]}) if reposting_in_progress: - frappe.msgprint(_("Item valuation reposting in progress. Report might show incorrect item valuation."), alert=1) \ No newline at end of file + frappe.msgprint(_("Item valuation reposting in progress. Report might show incorrect item valuation."), alert=1) diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py index 9c69deb6a48..074f1aca0e2 100644 --- a/erpnext/support/doctype/issue/issue.py +++ b/erpnext/support/doctype/issue/issue.py @@ -223,6 +223,10 @@ class Issue(Document): return replicated_issue.name + def reset_issue_metrics(self): + self.db_set("resolution_time", None) + self.db_set("user_resolution_time", None) + def before_insert(self): if frappe.db.get_single_value("Support Settings", "track_service_level_agreement"): if frappe.flags.in_test: @@ -231,8 +235,7 @@ class Issue(Document): self.set_response_and_resolution_time() def set_response_and_resolution_time(self, priority=None, service_level_agreement=None): - service_level_agreement = get_active_service_level_agreement_for(priority=priority, - customer=self.customer, service_level_agreement=service_level_agreement) + service_level_agreement = get_active_service_level_agreement_for(self) if not service_level_agreement: if frappe.db.get_value("Issue", self.name, "service_level_agreement"): @@ -243,7 +246,8 @@ class Issue(Document): frappe.throw(_("This Service Level Agreement is specific to Customer {0}").format(service_level_agreement.customer)) self.service_level_agreement = service_level_agreement.name - self.priority = service_level_agreement.default_priority if not priority else priority + if not self.priority: + self.priority = service_level_agreement.default_priority priority = get_priority(self) diff --git a/erpnext/support/doctype/issue_priority/issue_priority.py b/erpnext/support/doctype/issue_priority/issue_priority.py index 7c8925ebc30..514b6cc26ba 100644 --- a/erpnext/support/doctype/issue_priority/issue_priority.py +++ b/erpnext/support/doctype/issue_priority/issue_priority.py @@ -8,4 +8,4 @@ from frappe import _ from frappe.model.document import Document class IssuePriority(Document): - pass \ No newline at end of file + pass diff --git a/erpnext/support/doctype/issue_priority/test_issue_priority.py b/erpnext/support/doctype/issue_priority/test_issue_priority.py index a7b55f8a74c..618c93ea9d8 100644 --- a/erpnext/support/doctype/issue_priority/test_issue_priority.py +++ b/erpnext/support/doctype/issue_priority/test_issue_priority.py @@ -25,4 +25,4 @@ def insert_priority(name): frappe.get_doc({ "doctype": "Issue Priority", "name": name - }).insert(ignore_permissions=True) \ No newline at end of file + }).insert(ignore_permissions=True) diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.json b/erpnext/support/doctype/service_level_agreement/service_level_agreement.json index 939c1999828..1678f04def8 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.json +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.json @@ -18,6 +18,10 @@ "entity_type", "column_break_10", "entity", + "filters_section", + "condition", + "column_break_15", + "condition_description", "agreement_details_section", "start_date", "active", @@ -171,10 +175,30 @@ "fieldtype": "Table", "label": "Pause SLA On", "options": "Pause SLA On Status" + }, + { + "fieldname": "filters_section", + "fieldtype": "Section Break", + "label": "Assignment Condition" + }, + { + "fieldname": "column_break_15", + "fieldtype": "Column Break" + }, + { + "fieldname": "condition", + "fieldtype": "Code", + "label": "Condition", + "options": "Python" + }, + { + "fieldname": "condition_description", + "fieldtype": "HTML", + "options": "

          Condition Examples:

          \n
          doc.status==\"Open\"
          doc.due_date==nowdate()
          doc.total > 40000\n
          " } ], "links": [], - "modified": "2020-06-10 12:30:15.050785", + "modified": "2021-07-27 11:16:45.596579", "modified_by": "Administrator", "module": "Support", "name": "Service Level Agreement", @@ -208,4 +232,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py index 70c469663b7..11812426d67 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -3,10 +3,12 @@ # For license information, please see license.txt from __future__ import unicode_literals + import frappe from frappe.model.document import Document from frappe import _ -from frappe.utils import getdate, get_weekdays, get_link_to_form +from frappe.utils import getdate, get_weekdays, get_link_to_form, nowdate +from frappe.utils.safe_exec import get_safe_globals class ServiceLevelAgreement(Document): @@ -14,6 +16,7 @@ class ServiceLevelAgreement(Document): self.validate_doc() self.check_priorities() self.check_support_and_resolution() + self.validate_condition() def check_priorities(self): default_priority = [] @@ -92,6 +95,14 @@ class ServiceLevelAgreement(Document): if frappe.db.exists("Service Level Agreement", {"entity_type": self.entity_type, "entity": self.entity, "name": ["!=", self.name]}): frappe.throw(_("Service Level Agreement with Entity Type {0} and Entity {1} already exists.").format(self.entity_type, self.entity)) + def validate_condition(self): + temp_doc = frappe.new_doc('Issue') + if self.condition: + try: + frappe.safe_eval(self.condition, None, get_context(temp_doc)) + except Exception: + frappe.throw(_("The Condition '{0}' is invalid").format(self.condition)) + def get_service_level_agreement_priority(self, priority): priority = frappe.get_doc("Service Level Priority", {"priority": priority, "parent": self.name}) @@ -112,7 +123,7 @@ def check_agreement_status(): if doc.end_date and getdate(doc.end_date) < getdate(frappe.utils.getdate()): frappe.db.set_value("Service Level Agreement", service_level_agreement.name, "active", 0) -def get_active_service_level_agreement_for(priority, customer=None, service_level_agreement=None): +def get_active_service_level_agreement_for(doc): if not frappe.db.get_single_value("Support Settings", "track_service_level_agreement"): return @@ -121,23 +132,42 @@ def get_active_service_level_agreement_for(priority, customer=None, service_leve ["Service Level Agreement", "enable", "=", 1] ] - if priority: - filters.append(["Service Level Priority", "priority", "=", priority]) + if doc.get('priority'): + filters.append(["Service Level Priority", "priority", "=", doc.get('priority')]) + customer = doc.get('customer') or_filters = [ ["Service Level Agreement", "entity", "in", [customer, get_customer_group(customer), get_customer_territory(customer)]] ] + + service_level_agreement = doc.get('service_level_agreement') if service_level_agreement: or_filters = [ - ["Service Level Agreement", "name", "=", service_level_agreement], + ["Service Level Agreement", "name", "=", doc.get('service_level_agreement')], ] - or_filters.append(["Service Level Agreement", "default_service_level_agreement", "=", 1]) + default_sla_filter = filters + [["Service Level Agreement", "default_service_level_agreement", "=", 1]] + default_sla = frappe.get_all("Service Level Agreement", filters=default_sla_filter, + fields=["name", "default_priority", "condition"]) - agreement = frappe.get_list("Service Level Agreement", filters=filters, or_filters=or_filters, - fields=["name", "default_priority"]) + filters += [["Service Level Agreement", "default_service_level_agreement", "=", 0]] + agreements = frappe.get_all("Service Level Agreement", filters=filters, or_filters=or_filters, + fields=["name", "default_priority", "condition"]) - return agreement[0] if agreement else None + # check if the current document on which SLA is to be applied fulfills all the conditions + filtered_agreements = [] + for agreement in agreements: + condition = agreement.get('condition') + if not condition or (condition and frappe.safe_eval(condition, None, get_context(doc))): + filtered_agreements.append(agreement) + + # if any default sla + filtered_agreements += default_sla + + return filtered_agreements[0] if filtered_agreements else None + +def get_context(doc): + return {"doc": doc.as_dict(), "nowdate": nowdate, "frappe": frappe._dict(utils=get_safe_globals().get("frappe").get("utils"))} def get_customer_group(customer): if customer: diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement_dashboard.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement_dashboard.py index f2bd6813965..7e7a405d6e7 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement_dashboard.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement_dashboard.py @@ -9,4 +9,4 @@ def get_data(): 'items': ['Issue'] } ] - } \ No newline at end of file + } diff --git a/erpnext/support/doctype/support_settings/test_support_settings.js b/erpnext/support/doctype/support_settings/test_support_settings.js deleted file mode 100644 index 0787306c3ea..00000000000 --- a/erpnext/support/doctype/support_settings/test_support_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: Support Settings", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Support Settings - () => frappe.tests.make('Support Settings', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/support/doctype/warranty_claim/warranty_claim.js b/erpnext/support/doctype/warranty_claim/warranty_claim.js index 79f46758d12..d2ee52ad5cc 100644 --- a/erpnext/support/doctype/warranty_claim/warranty_claim.js +++ b/erpnext/support/doctype/warranty_claim/warranty_claim.js @@ -93,4 +93,4 @@ cur_frm.fields_dict['item_code'].get_query = function(doc, cdt, cdn) { ] } } -}; \ No newline at end of file +}; diff --git a/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.py b/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.py index 922da2b33de..69bf2730d35 100644 --- a/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.py +++ b/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.py @@ -32,4 +32,4 @@ def execute(filters=None): ORDER BY creation_date desc ''', (filters.from_date, filters.to_date)) - return columns, data \ No newline at end of file + return columns, data diff --git a/erpnext/support/report/issue_analytics/issue_analytics.py b/erpnext/support/report/issue_analytics/issue_analytics.py index 3fdb10ddf38..54fce0b3592 100644 --- a/erpnext/support/report/issue_analytics/issue_analytics.py +++ b/erpnext/support/report/issue_analytics/issue_analytics.py @@ -218,4 +218,4 @@ class IssueAnalytics(object): 'datasets': [] }, 'type': 'line' - } \ No newline at end of file + } diff --git a/erpnext/support/report/issue_analytics/test_issue_analytics.py b/erpnext/support/report/issue_analytics/test_issue_analytics.py index 77483198ecc..a9d961a4592 100644 --- a/erpnext/support/report/issue_analytics/test_issue_analytics.py +++ b/erpnext/support/report/issue_analytics/test_issue_analytics.py @@ -22,7 +22,7 @@ class TestIssueAnalytics(unittest.TestCase): if current_month_date.year != last_month_date.year: self.current_month += '_' + str(current_month_date.year) self.last_month += '_' + str(last_month_date.year) - + def test_issue_analytics(self): create_service_level_agreements_for_issues() create_issue_types() @@ -211,4 +211,4 @@ def create_records(): "assign_to": ["test@example.com", "test1@example.com"], "doctype": "Issue", "name": issue.name - }) \ No newline at end of file + }) diff --git a/erpnext/support/report/issue_summary/issue_summary.py b/erpnext/support/report/issue_summary/issue_summary.py index bba25b8bed6..7c4af39f104 100644 --- a/erpnext/support/report/issue_summary/issue_summary.py +++ b/erpnext/support/report/issue_summary/issue_summary.py @@ -362,4 +362,3 @@ class IssueSummary(object): 'datatype': 'Int', } ] - diff --git a/erpnext/support/web_form/issues/issues.js b/erpnext/support/web_form/issues/issues.js index 699703c5792..ffc5e984253 100644 --- a/erpnext/support/web_form/issues/issues.js +++ b/erpnext/support/web_form/issues/issues.js @@ -1,3 +1,3 @@ frappe.ready(function() { // bind events here -}) \ No newline at end of file +}) diff --git a/erpnext/telephony/doctype/call_log/call_log.py b/erpnext/telephony/doctype/call_log/call_log.py index 4d553df08b8..6f8e4116956 100644 --- a/erpnext/telephony/doctype/call_log/call_log.py +++ b/erpnext/telephony/doctype/call_log/call_log.py @@ -142,7 +142,7 @@ def link_existing_conversations(doc, state): for log in logs: call_log = frappe.get_doc('Call Log', log) call_log.add_link(link_type=doc.doctype, link_name=doc.name) - call_log.save() + call_log.save(ignore_permissions=True) frappe.db.commit() except Exception: frappe.log_error(title=_('Error during caller information update')) @@ -173,4 +173,3 @@ def get_linked_call_logs(doctype, docname): }) return timeline_contents - diff --git a/erpnext/telephony/doctype/incoming_call_settings/incoming_call_settings.js b/erpnext/telephony/doctype/incoming_call_settings/incoming_call_settings.js index 1bcc8461323..b80acdb3760 100644 --- a/erpnext/telephony/doctype/incoming_call_settings/incoming_call_settings.js +++ b/erpnext/telephony/doctype/incoming_call_settings/incoming_call_settings.js @@ -99,4 +99,3 @@ frappe.ui.form.on('Incoming Call Settings', { validate_call_schedule(frm.doc.call_handling_schedule); } }); - diff --git a/erpnext/templates/emails/anniversary_reminder.html b/erpnext/templates/emails/anniversary_reminder.html new file mode 100644 index 00000000000..ac9f7e4993a --- /dev/null +++ b/erpnext/templates/emails/anniversary_reminder.html @@ -0,0 +1,25 @@ +
          +
          + {% for person in anniversary_persons %} + {% if person.image %} + + + {% else %} + + {{ frappe.utils.get_abbr(person.name) }} + + {% endif %} + {% endfor %} +
          +
          + {{ reminder_text }} +

          {{ message }}

          +
          +
          \ No newline at end of file diff --git a/erpnext/templates/emails/birthday_reminder.html b/erpnext/templates/emails/birthday_reminder.html index 12cdf1ec600..1f57b4969c0 100644 --- a/erpnext/templates/emails/birthday_reminder.html +++ b/erpnext/templates/emails/birthday_reminder.html @@ -22,4 +22,4 @@ {{ reminder_text }}

          {{ message }}

          - \ No newline at end of file + diff --git a/erpnext/templates/emails/daily_project_summary.html b/erpnext/templates/emails/daily_project_summary.html index 8b60830db62..5ccc6101665 100644 --- a/erpnext/templates/emails/daily_project_summary.html +++ b/erpnext/templates/emails/daily_project_summary.html @@ -43,4 +43,4 @@
          -{% endfor %} \ No newline at end of file +{% endfor %} diff --git a/erpnext/templates/emails/daily_work_summary.html b/erpnext/templates/emails/daily_work_summary.html index a22e09cb8de..1764e8f7038 100644 --- a/erpnext/templates/emails/daily_work_summary.html +++ b/erpnext/templates/emails/daily_work_summary.html @@ -52,4 +52,4 @@ -{% endif %} \ No newline at end of file +{% endif %} diff --git a/erpnext/templates/emails/holiday_reminder.html b/erpnext/templates/emails/holiday_reminder.html new file mode 100644 index 00000000000..e38d27bf8bc --- /dev/null +++ b/erpnext/templates/emails/holiday_reminder.html @@ -0,0 +1,16 @@ +
          + {{ reminder_text }} +

          {{ message }}

          +
          + +{% if advance_holiday_reminder %} + {% if holidays | len > 0 %} +
            + {% for holiday in holidays %} +
          1. {{ frappe.format(holiday.holiday_date, 'Date') }} - {{ holiday.description }}
          2. + {% endfor %} +
          + {% else %} +

          You don't have no upcoming holidays this {{ frequency }}.

          + {% endif %} +{% endif %} diff --git a/erpnext/templates/emails/request_for_quotation.html b/erpnext/templates/emails/request_for_quotation.html index 812939a5538..3283987fab0 100644 --- a/erpnext/templates/emails/request_for_quotation.html +++ b/erpnext/templates/emails/request_for_quotation.html @@ -21,4 +21,4 @@

          -{% endif %} \ No newline at end of file +{% endif %} diff --git a/erpnext/templates/emails/training_event.html b/erpnext/templates/emails/training_event.html index 51c232d8e87..8a2414a3c92 100644 --- a/erpnext/templates/emails/training_event.html +++ b/erpnext/templates/emails/training_event.html @@ -11,7 +11,7 @@

          {{_("Update Response")}}

          {% if not self_study %}

          {{_("Please update your status for this training event")}}:

          -
          +
          {% else %}

          {{_("Please confirm once you have completed your training")}}:

          diff --git a/erpnext/templates/generators/item/item.html b/erpnext/templates/generators/item/item.html index 135982d7090..663ea79f4ec 100644 --- a/erpnext/templates/generators/item/item.html +++ b/erpnext/templates/generators/item/item.html @@ -33,4 +33,4 @@ -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/erpnext/templates/generators/item/item_inquiry.js b/erpnext/templates/generators/item/item_inquiry.js index e7db3a368df..4724b681196 100644 --- a/erpnext/templates/generators/item/item_inquiry.js +++ b/erpnext/templates/generators/item/item_inquiry.js @@ -74,4 +74,4 @@ frappe.ready(() => { d.show(); }); -}); \ No newline at end of file +}); diff --git a/erpnext/templates/generators/item/item_specifications.html b/erpnext/templates/generators/item/item_specifications.html index 469a45fd7d4..d4dfa8e591a 100644 --- a/erpnext/templates/generators/item/item_specifications.html +++ b/erpnext/templates/generators/item/item_specifications.html @@ -11,4 +11,4 @@ -{%- endif %} \ No newline at end of file +{%- endif %} diff --git a/erpnext/templates/generators/item_group.html b/erpnext/templates/generators/item_group.html index 393c3a43afb..b5f18ba66d1 100644 --- a/erpnext/templates/generators/item_group.html +++ b/erpnext/templates/generators/item_group.html @@ -9,7 +9,7 @@ {% endblock %} {% block page_content %} -
          +
          {% if slideshow %} {{ web_block( @@ -127,15 +127,36 @@
          -
          -
          +
          +
          +
          +
          {% if frappe.form_dict.start|int > 0 %} - + {% endif %} {% if items|length >= page_length %} - + {% endif %}
          -{% endblock %} \ No newline at end of file + + +{% endblock %} diff --git a/erpnext/templates/generators/job_opening.html b/erpnext/templates/generators/job_opening.html index c562db3c25a..135fb3643d3 100644 --- a/erpnext/templates/generators/job_opening.html +++ b/erpnext/templates/generators/job_opening.html @@ -14,17 +14,17 @@
          {{ description }}
          {% endif %} -{%- if publish_salary_range -%} +{%- if publish_salary_range -%}
          {{_("Salary range per month")}}: {{ frappe.format_value(frappe.utils.flt(lower_range), currency=currency) }} - {{ frappe.format_value(frappe.utils.flt(upper_range), currency=currency) }}
          {% endif %}

          {%- if job_application_route -%} - {{ _("Apply Now") }} {% else %} - {{ _("Apply Now") }} {% endif %} diff --git a/erpnext/templates/generators/student_admission.html b/erpnext/templates/generators/student_admission.html index 8b153448eea..8cc58a0a1f2 100644 --- a/erpnext/templates/generators/student_admission.html +++ b/erpnext/templates/generators/student_admission.html @@ -14,7 +14,7 @@ {%- if introduction -%}

          {{ introduction }}
          -{% endif %} +{% endif %} {%- if doc.enable_admission_application -%}

          diff --git a/erpnext/templates/includes/cart/address_picker_card.html b/erpnext/templates/includes/cart/address_picker_card.html index 2334ea2955d..646210e65f1 100644 --- a/erpnext/templates/includes/cart/address_picker_card.html +++ b/erpnext/templates/includes/cart/address_picker_card.html @@ -9,4 +9,4 @@

          {{ _('Edit') }}
          -
          \ No newline at end of file + diff --git a/erpnext/templates/includes/cart/cart_address_picker.html b/erpnext/templates/includes/cart/cart_address_picker.html index 72cc5f51423..66a50ecc9f3 100644 --- a/erpnext/templates/includes/cart/cart_address_picker.html +++ b/erpnext/templates/includes/cart/cart_address_picker.html @@ -1,4 +1,3 @@
          {{ _("Shipping Address") }}
          - diff --git a/erpnext/templates/includes/cart/cart_items_dropdown.html b/erpnext/templates/includes/cart/cart_items_dropdown.html index b2ba4312d6c..5d107fc0d06 100644 --- a/erpnext/templates/includes/cart/cart_items_dropdown.html +++ b/erpnext/templates/includes/cart/cart_items_dropdown.html @@ -9,4 +9,4 @@ {{ d.get_formatted("amount") }} -{% endfor %} \ No newline at end of file +{% endfor %} diff --git a/erpnext/templates/includes/course/macros.html b/erpnext/templates/includes/course/macros.html index c80dca4bcc9..334b5ea200a 100644 --- a/erpnext/templates/includes/course/macros.html +++ b/erpnext/templates/includes/course/macros.html @@ -1 +1 @@ -{% macro back_link(doc) %}&back-to=/courses?course={{ doc.name }}&back-to-title={{ doc.course_name }}{% endmacro %} \ No newline at end of file +{% macro back_link(doc) %}&back-to=/courses?course={{ doc.name }}&back-to-title={{ doc.course_name }}{% endmacro %} diff --git a/erpnext/templates/includes/itemised_tax_breakup.html b/erpnext/templates/includes/itemised_tax_breakup.html index c2f13539cdf..5652bb1dddd 100644 --- a/erpnext/templates/includes/itemised_tax_breakup.html +++ b/erpnext/templates/includes/itemised_tax_breakup.html @@ -43,4 +43,4 @@ {% endfor %} - \ No newline at end of file + diff --git a/erpnext/templates/includes/macros.html b/erpnext/templates/includes/macros.html index c44bfb53848..be0d47f3715 100644 --- a/erpnext/templates/includes/macros.html +++ b/erpnext/templates/includes/macros.html @@ -120,4 +120,4 @@ {% endif %} -{%- endmacro -%} \ No newline at end of file +{%- endmacro -%} diff --git a/erpnext/templates/includes/navbar/navbar_items.html b/erpnext/templates/includes/navbar/navbar_items.html index 133d99e5eb9..291220629c9 100644 --- a/erpnext/templates/includes/navbar/navbar_items.html +++ b/erpnext/templates/includes/navbar/navbar_items.html @@ -9,4 +9,4 @@ -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/erpnext/templates/includes/order/order_macros.html b/erpnext/templates/includes/order/order_macros.html index da4fb8c0460..7b3c9a41318 100644 --- a/erpnext/templates/includes/order/order_macros.html +++ b/erpnext/templates/includes/order/order_macros.html @@ -40,4 +40,4 @@ -{% endmacro %} \ No newline at end of file +{% endmacro %} diff --git a/erpnext/templates/includes/projects.css b/erpnext/templates/includes/projects.css index 5a717fc6699..5d9fc50385e 100644 --- a/erpnext/templates/includes/projects.css +++ b/erpnext/templates/includes/projects.css @@ -86,4 +86,4 @@ .progress-hg{ margin-bottom: 30!important; height:2px; -} \ No newline at end of file +} diff --git a/erpnext/templates/includes/projects/project_search_box.html b/erpnext/templates/includes/projects/project_search_box.html index 6f53bae2e80..d7466873dda 100644 --- a/erpnext/templates/includes/projects/project_search_box.html +++ b/erpnext/templates/includes/projects/project_search_box.html @@ -27,4 +27,4 @@ frappe.ready(function() { }); $(".form-search").on("submit", function() { return false; }); }); - \ No newline at end of file + diff --git a/erpnext/templates/includes/rfq/rfq_items.html b/erpnext/templates/includes/rfq/rfq_items.html index caa15f386b0..04cf922664b 100644 --- a/erpnext/templates/includes/rfq/rfq_items.html +++ b/erpnext/templates/includes/rfq/rfq_items.html @@ -1,4 +1,4 @@ -{% from "erpnext/templates/includes/rfq/rfq_macros.html" import item_name_and_description %} +{% from "templates/includes/rfq/rfq_macros.html" import item_name_and_description %} {% for d in doc.items %}
          diff --git a/erpnext/templates/includes/salary_slip_log.html b/erpnext/templates/includes/salary_slip_log.html index 107df51dd8b..d36ee6e23bb 100644 --- a/erpnext/templates/includes/salary_slip_log.html +++ b/erpnext/templates/includes/salary_slip_log.html @@ -16,4 +16,4 @@ {% endfor %} - \ No newline at end of file + diff --git a/erpnext/templates/includes/topic/topic_row.html b/erpnext/templates/includes/topic/topic_row.html index 3401bd39371..38d46b7fe00 100644 --- a/erpnext/templates/includes/topic/topic_row.html +++ b/erpnext/templates/includes/topic/topic_row.html @@ -1,4 +1,4 @@ -
          + \ No newline at end of file +
          diff --git a/erpnext/templates/pages/cart_terms.html b/erpnext/templates/pages/cart_terms.html index 521c583cb60..6d84fb86a78 100644 --- a/erpnext/templates/pages/cart_terms.html +++ b/erpnext/templates/pages/cart_terms.html @@ -1,2 +1,2 @@ -
          {{doc.terms}}
          \ No newline at end of file +
          {{doc.terms}}
          diff --git a/erpnext/templates/pages/courses.html b/erpnext/templates/pages/courses.html index 42e7f3e70b7..6592f7a2e5c 100644 --- a/erpnext/templates/pages/courses.html +++ b/erpnext/templates/pages/courses.html @@ -8,4 +8,4 @@

          {{ intro }}

          -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/erpnext/templates/pages/courses.py b/erpnext/templates/pages/courses.py index c80d8e7d229..92c38f6fcae 100644 --- a/erpnext/templates/pages/courses.py +++ b/erpnext/templates/pages/courses.py @@ -17,4 +17,3 @@ def get_context(context): context.doc = course context.sidebar_title = sidebar_title context.intro = course.course_intro - diff --git a/erpnext/templates/pages/home.css b/erpnext/templates/pages/home.css index cf5476635bd..785d8059ba0 100644 --- a/erpnext/templates/pages/home.css +++ b/erpnext/templates/pages/home.css @@ -6,4 +6,4 @@ padding: 10rem 0; } {% endif %} -/* csslint ignore:end */ \ No newline at end of file +/* csslint ignore:end */ diff --git a/erpnext/templates/pages/home.html b/erpnext/templates/pages/home.html index 2ef9c105347..9a61eabaf8c 100644 --- a/erpnext/templates/pages/home.html +++ b/erpnext/templates/pages/home.html @@ -72,4 +72,4 @@ {{ render_homepage_section(section) }} {% endfor %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/erpnext/templates/pages/integrations/gocardless_checkout.html b/erpnext/templates/pages/integrations/gocardless_checkout.html index 7193d755a1e..6072db49ea9 100644 --- a/erpnext/templates/pages/integrations/gocardless_checkout.html +++ b/erpnext/templates/pages/integrations/gocardless_checkout.html @@ -13,4 +13,4 @@ {{ _("Loading Payment System") }}

          -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/erpnext/templates/pages/integrations/gocardless_checkout.py b/erpnext/templates/pages/integrations/gocardless_checkout.py index 96a0f42a05c..bdef79cfbed 100644 --- a/erpnext/templates/pages/integrations/gocardless_checkout.py +++ b/erpnext/templates/pages/integrations/gocardless_checkout.py @@ -74,4 +74,4 @@ def check_mandate(data, reference_doctype, reference_docname): except Exception as e: frappe.log_error(e, "GoCardless Payment Error") - return {"redirect_to": '/integrations/payment-failed'} \ No newline at end of file + return {"redirect_to": '/integrations/payment-failed'} diff --git a/erpnext/templates/pages/integrations/gocardless_confirmation.html b/erpnext/templates/pages/integrations/gocardless_confirmation.html index 6ba154a06c7..d961c6344af 100644 --- a/erpnext/templates/pages/integrations/gocardless_confirmation.html +++ b/erpnext/templates/pages/integrations/gocardless_confirmation.html @@ -13,4 +13,4 @@ {{ _("Payment Confirmation") }}

          -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/erpnext/templates/pages/integrations/gocardless_confirmation.py b/erpnext/templates/pages/integrations/gocardless_confirmation.py index cfaa1a15cfc..0b72e9f8b60 100644 --- a/erpnext/templates/pages/integrations/gocardless_confirmation.py +++ b/erpnext/templates/pages/integrations/gocardless_confirmation.py @@ -86,4 +86,4 @@ def create_mandate(data): }).insert(ignore_permissions=True) except Exception: - frappe.log_error(frappe.get_traceback()) \ No newline at end of file + frappe.log_error(frappe.get_traceback()) diff --git a/erpnext/templates/pages/material_request_info.html b/erpnext/templates/pages/material_request_info.html index 0c2772e4d82..151d029ee47 100644 --- a/erpnext/templates/pages/material_request_info.html +++ b/erpnext/templates/pages/material_request_info.html @@ -71,4 +71,4 @@ {% endfor %}
          -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/erpnext/templates/pages/material_request_info.py b/erpnext/templates/pages/material_request_info.py index 28e541a5d92..e29860ddd67 100644 --- a/erpnext/templates/pages/material_request_info.py +++ b/erpnext/templates/pages/material_request_info.py @@ -19,7 +19,7 @@ def get_context(context): if not frappe.has_website_permission(context.doc): frappe.throw(_("Not Permitted"), frappe.PermissionError) - + default_print_format = frappe.db.get_value('Property Setter', dict(property='default_print_format', doc_type=frappe.form_dict.doctype), "value") if default_print_format: context.print_format = default_print_format @@ -45,5 +45,5 @@ def get_more_items_info(items, material_request): item.delivered_qty = flt(frappe.db.sql("""select sum(transfer_qty) from `tabStock Entry Detail` where material_request = %s and item_code = %s and docstatus = 1""", - (material_request, item.item_code))[0][0]) - return items \ No newline at end of file + (material_request, item.item_code))[0][0]) + return items diff --git a/erpnext/templates/pages/non_profit/join-chapter.html b/erpnext/templates/pages/non_profit/join-chapter.html index 89a7d2aace8..4923efc4e8c 100644 --- a/erpnext/templates/pages/non_profit/join-chapter.html +++ b/erpnext/templates/pages/non_profit/join-chapter.html @@ -56,4 +56,4 @@ {% endif %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/erpnext/templates/pages/non_profit/leave-chapter.html b/erpnext/templates/pages/non_profit/leave-chapter.html index bc4242f9196..fd7658b3b1e 100644 --- a/erpnext/templates/pages/non_profit/leave-chapter.html +++ b/erpnext/templates/pages/non_profit/leave-chapter.html @@ -39,4 +39,4 @@ }); }) -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/erpnext/templates/pages/order.py b/erpnext/templates/pages/order.py index 34985d94ea7..816a25963f5 100644 --- a/erpnext/templates/pages/order.py +++ b/erpnext/templates/pages/order.py @@ -32,9 +32,9 @@ def get_context(context): if not frappe.has_website_permission(context.doc): frappe.throw(_("Not Permitted"), frappe.PermissionError) - + # check for the loyalty program of the customer - customer_loyalty_program = frappe.db.get_value("Customer", context.doc.customer, "loyalty_program") + customer_loyalty_program = frappe.db.get_value("Customer", context.doc.customer, "loyalty_program") if customer_loyalty_program: from erpnext.accounts.doctype.loyalty_program.loyalty_program import get_loyalty_program_details_with_points loyalty_program_details = get_loyalty_program_details_with_points(context.doc.customer, customer_loyalty_program) diff --git a/erpnext/templates/pages/product_search.py b/erpnext/templates/pages/product_search.py index d0d72f073a9..9ab76deff73 100644 --- a/erpnext/templates/pages/product_search.py +++ b/erpnext/templates/pages/product_search.py @@ -47,4 +47,3 @@ def get_product_list(search=None, start=0, limit=12): set_product_info_for_website(item) return [get_item_for_list_in_html(r) for r in data] - diff --git a/erpnext/templates/pages/projects.js b/erpnext/templates/pages/projects.js index 262167fc0b9..bd6bcea7ca0 100644 --- a/erpnext/templates/pages/projects.js +++ b/erpnext/templates/pages/projects.js @@ -117,4 +117,4 @@ frappe.ready(function() { }) return false; } -}); \ No newline at end of file +}); diff --git a/erpnext/templates/pages/rfq.html b/erpnext/templates/pages/rfq.html index 6e2edb6391b..6516482c230 100644 --- a/erpnext/templates/pages/rfq.html +++ b/erpnext/templates/pages/rfq.html @@ -86,7 +86,7 @@ {{d.transaction_date}} - Link + Link {% endfor %} @@ -95,6 +95,4 @@ - - {% endblock %} diff --git a/erpnext/templates/pages/task_info.html b/erpnext/templates/pages/task_info.html index 6cd6a7e51af..fe4d304a398 100644 --- a/erpnext/templates/pages/task_info.html +++ b/erpnext/templates/pages/task_info.html @@ -147,4 +147,4 @@ }); -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/erpnext/templates/pages/task_info.py b/erpnext/templates/pages/task_info.py index b832b88048b..260e2788cd0 100644 --- a/erpnext/templates/pages/task_info.py +++ b/erpnext/templates/pages/task_info.py @@ -7,8 +7,8 @@ def get_context(context): context.no_cache = 1 task = frappe.get_doc('Task', frappe.form_dict.task) - + context.comments = frappe.get_all('Communication', filters={'reference_name': task.name, 'comment_type': 'comment'}, fields=['subject', 'sender_full_name', 'communication_date']) - - context.doc = task \ No newline at end of file + + context.doc = task diff --git a/erpnext/templates/pages/timelog_info.html b/erpnext/templates/pages/timelog_info.html index 22ea3e45d38..be13826444c 100644 --- a/erpnext/templates/pages/timelog_info.html +++ b/erpnext/templates/pages/timelog_info.html @@ -45,4 +45,4 @@ -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/erpnext/templates/pages/timelog_info.py b/erpnext/templates/pages/timelog_info.py index 7a3361c2ef5..ee86483fa29 100644 --- a/erpnext/templates/pages/timelog_info.py +++ b/erpnext/templates/pages/timelog_info.py @@ -7,5 +7,5 @@ def get_context(context): context.no_cache = 1 timelog = frappe.get_doc('Time Log', frappe.form_dict.timelog) - - context.doc = timelog \ No newline at end of file + + context.doc = timelog diff --git a/erpnext/templates/print_formats/includes/item_table_qty.html b/erpnext/templates/print_formats/includes/item_table_qty.html index 8e68f1cc638..aaa949192ca 100644 --- a/erpnext/templates/print_formats/includes/item_table_qty.html +++ b/erpnext/templates/print_formats/includes/item_table_qty.html @@ -12,4 +12,3 @@ {%- endif %} {{ doc.get_formatted("qty", doc) }} {%- endif %} - diff --git a/erpnext/tests/test_regional.py b/erpnext/tests/test_regional.py index 282fc6454b7..5b3f45a1af7 100644 --- a/erpnext/tests/test_regional.py +++ b/erpnext/tests/test_regional.py @@ -14,4 +14,4 @@ class TestInit(unittest.TestCase): self.assertEqual(test_method(), 'original') frappe.flags.country = 'France' - self.assertEqual(test_method(), 'overridden') \ No newline at end of file + self.assertEqual(test_method(), 'overridden') diff --git a/erpnext/tests/test_subcontracting.py b/erpnext/tests/test_subcontracting.py index 8b0ce0957d4..f55137bc9cf 100644 --- a/erpnext/tests/test_subcontracting.py +++ b/erpnext/tests/test_subcontracting.py @@ -874,4 +874,4 @@ def make_bom_for_subcontracted_items(): def set_backflush_based_on(based_on): frappe.db.set_value('Buying Settings', None, - 'backflush_raw_materials_of_subcontract_based_on', based_on) \ No newline at end of file + 'backflush_raw_materials_of_subcontract_based_on', based_on) diff --git a/erpnext/tests/ui/setup_wizard.js b/erpnext/tests/ui/setup_wizard.js index aeb8d2a1167..ccff785ec94 100644 --- a/erpnext/tests/ui/setup_wizard.js +++ b/erpnext/tests/ui/setup_wizard.js @@ -44,4 +44,4 @@ module.exports = { after: browser => { browser.end(); }, -}; \ No newline at end of file +}; diff --git a/erpnext/tests/ui_test_helpers.py b/erpnext/tests/ui_test_helpers.py new file mode 100644 index 00000000000..902fd64d686 --- /dev/null +++ b/erpnext/tests/ui_test_helpers.py @@ -0,0 +1,59 @@ +import frappe +from frappe.utils import getdate + +@frappe.whitelist() +def create_employee_records(): + create_company() + create_missing_designation() + + emp1 = create_employee('Test Employee 1', 'CEO') + emp2 = create_employee('Test Employee 2', 'CTO') + emp3 = create_employee('Test Employee 3', 'Head of Marketing and Sales', emp1) + emp4 = create_employee('Test Employee 4', 'Project Manager', emp2) + emp5 = create_employee('Test Employee 5', 'Engineer', emp2) + emp6 = create_employee('Test Employee 6', 'Analyst', emp3) + emp7 = create_employee('Test Employee 7', 'Software Developer', emp4) + + employees = [emp1, emp2, emp3, emp4, emp5, emp6, emp7] + return employees + +@frappe.whitelist() +def get_employee_records(): + return frappe.db.get_list('Employee', filters={ + 'company': 'Test Org Chart' + }, pluck='name', order_by='name') + +def create_company(): + company = frappe.db.exists('Company', 'Test Org Chart') + if not company: + company = frappe.get_doc({ + 'doctype': 'Company', + 'company_name': 'Test Org Chart', + 'country': 'India', + 'default_currency': 'INR' + }).insert().name + + return company + +def create_employee(first_name, designation, reports_to=None): + employee = frappe.db.exists('Employee', {'first_name': first_name, 'designation': designation}) + if not employee: + employee = frappe.get_doc({ + 'doctype': 'Employee', + 'first_name': first_name, + 'company': 'Test Org Chart', + 'gender': 'Female', + 'date_of_birth': getdate('08-12-1998'), + 'date_of_joining': getdate('01-01-2021'), + 'designation': designation, + 'reports_to': reports_to + }).insert().name + + return employee + +def create_missing_designation(): + if not frappe.db.exists('Designation', 'CTO'): + frappe.get_doc({ + 'doctype': 'Designation', + 'designation_name': 'CTO' + }).insert() diff --git a/erpnext/utilities/activation.py b/erpnext/utilities/activation.py index 50c4b255ce1..0f9f2f886de 100644 --- a/erpnext/utilities/activation.py +++ b/erpnext/utilities/activation.py @@ -13,33 +13,33 @@ def get_level(): min_count = 0 doctypes = { "Asset": 5, - "BOM": 3, - "Customer": 5, + "BOM": 3, + "Customer": 5, "Delivery Note": 5, - "Employee": 3, - "Instructor": 5, + "Employee": 3, + "Instructor": 5, "Issue": 5, - "Item": 5, - "Journal Entry": 3, + "Item": 5, + "Journal Entry": 3, "Lead": 3, "Leave Application": 5, "Material Request": 5, - "Opportunity": 5, - "Payment Entry": 2, + "Opportunity": 5, + "Payment Entry": 2, "Project": 5, - "Purchase Order": 2, + "Purchase Order": 2, "Purchase Invoice": 5, "Purchase Receipt": 5, "Quotation": 3, "Salary Slip": 5, "Salary Structure": 5, - "Sales Order": 2, - "Sales Invoice": 2, + "Sales Order": 2, + "Sales Invoice": 2, "Stock Entry": 3, - "Student": 5, + "Student": 5, "Supplier": 5, "Task": 5, - "User": 5, + "User": 5, "Work Order": 5 } diff --git a/erpnext/utilities/bot.py b/erpnext/utilities/bot.py index b2e74da9215..485b0b3383f 100644 --- a/erpnext/utilities/bot.py +++ b/erpnext/utilities/bot.py @@ -36,4 +36,4 @@ class FindItemBot(BotParser): return "\n\n".join(out) else: - return _("Did not find any item called {0}").format(item) \ No newline at end of file + return _("Did not find any item called {0}").format(item) diff --git a/erpnext/utilities/doctype/rename_tool/rename_tool.py b/erpnext/utilities/doctype/rename_tool/rename_tool.py index 0f8a7a385c1..5e3ac1a4c92 100644 --- a/erpnext/utilities/doctype/rename_tool/rename_tool.py +++ b/erpnext/utilities/doctype/rename_tool/rename_tool.py @@ -29,4 +29,3 @@ def upload(select_doctype=None, rows=None): rows = read_csv_content_from_attached_file(frappe.get_doc("Rename Tool", "Rename Tool")) return bulk_rename(select_doctype, rows=rows) - diff --git a/erpnext/utilities/doctype/video/video_list.js b/erpnext/utilities/doctype/video/video_list.js index 8273a4a781f..6f78f6ee127 100644 --- a/erpnext/utilities/doctype/video/video_list.js +++ b/erpnext/utilities/doctype/video/video_list.js @@ -4,4 +4,4 @@ frappe.listview_settings["Video"] = { frappe.set_route("Form","Video Settings", "Video Settings"); }); } -} \ No newline at end of file +} diff --git a/erpnext/utilities/doctype/video_settings/video_settings.py b/erpnext/utilities/doctype/video_settings/video_settings.py index 36fb54f0150..db021b473a4 100644 --- a/erpnext/utilities/doctype/video_settings/video_settings.py +++ b/erpnext/utilities/doctype/video_settings/video_settings.py @@ -19,4 +19,4 @@ class VideoSettings(Document): except Exception: title = _("Failed to Authenticate the API key.") frappe.log_error(title + "\n\n" + frappe.get_traceback(), title=title) - frappe.throw(title + " Please check the error logs.", title=_("Invalid Credentials")) \ No newline at end of file + frappe.throw(title + " Please check the error logs.", title=_("Invalid Credentials")) diff --git a/erpnext/utilities/hierarchy_chart.py b/erpnext/utilities/hierarchy_chart.py new file mode 100644 index 00000000000..384d84194bb --- /dev/null +++ b/erpnext/utilities/hierarchy_chart.py @@ -0,0 +1,29 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _ + +@frappe.whitelist() +def get_all_nodes(parent, parent_name, method, company): + '''Recursively gets all data from nodes''' + method = frappe.get_attr(method) + + if method not in frappe.whitelisted: + frappe.throw(_('Not Permitted'), frappe.PermissionError) + + data = method(parent, company) + result = [dict(parent=parent, parent_name=parent_name, data=data)] + + nodes_to_expand = [{'id': d.get('id'), 'name': d.get('name')} for d in data if d.get('expandable')] + + while nodes_to_expand: + parent = nodes_to_expand.pop(0) + data = method(parent.get('id'), company) + result.append(dict(parent=parent.get('id'), parent_name=parent.get('name'), data=data)) + for d in data: + if d.get('expandable'): + nodes_to_expand.append({'id': d.get('id'), 'name': d.get('name')}) + + return result diff --git a/erpnext/utilities/report/youtube_interactions/youtube_interactions.py b/erpnext/utilities/report/youtube_interactions/youtube_interactions.py index 3516a35097a..29a489ddcc7 100644 --- a/erpnext/utilities/report/youtube_interactions/youtube_interactions.py +++ b/erpnext/utilities/report/youtube_interactions/youtube_interactions.py @@ -110,4 +110,4 @@ def get_chart_summary_data(data): "datatype": "Float", } ] - return chart_data, summary \ No newline at end of file + return chart_data, summary diff --git a/erpnext/utilities/web_form/addresses/addresses.js b/erpnext/utilities/web_form/addresses/addresses.js index 699703c5792..ffc5e984253 100644 --- a/erpnext/utilities/web_form/addresses/addresses.js +++ b/erpnext/utilities/web_form/addresses/addresses.js @@ -1,3 +1,3 @@ frappe.ready(function() { // bind events here -}) \ No newline at end of file +}) diff --git a/erpnext/www/all-products/index.html b/erpnext/www/all-products/index.html index 92c76ad8790..7c18ecc41fe 100644 --- a/erpnext/www/all-products/index.html +++ b/erpnext/www/all-products/index.html @@ -164,4 +164,4 @@ }); -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/erpnext/www/all-products/index.js b/erpnext/www/all-products/index.js index 0721056816b..1c641b59ad1 100644 --- a/erpnext/www/all-products/index.js +++ b/erpnext/www/all-products/index.js @@ -124,6 +124,10 @@ $(() => { attribute_filters: if_key_exists(attribute_filters) }; + const item_group = $(".item-group-content").data('item-group'); + if (item_group) { + Object.assign(field_filters, { item_group }); + } return new Promise((resolve, reject) => { frappe.call('erpnext.portal.product_configurator.utils.get_products_html_for_website', args) .then(r => { diff --git a/erpnext/www/all-products/item_row.html b/erpnext/www/all-products/item_row.html index 20fc9a4878c..a7e994c1e3f 100644 --- a/erpnext/www/all-products/item_row.html +++ b/erpnext/www/all-products/item_row.html @@ -4,4 +4,3 @@ item.item_name or item.name, item.website_image or item.image, item.route, item.website_description or item.description, item.formatted_price, item.item_group ) }} - diff --git a/erpnext/www/all-products/not_found.html b/erpnext/www/all-products/not_found.html index e1986b44154..91989a9ef48 100644 --- a/erpnext/www/all-products/not_found.html +++ b/erpnext/www/all-products/not_found.html @@ -1 +1 @@ -
          {{ _('No products found') }}
          \ No newline at end of file +
          {{ _('No products found') }}
          diff --git a/erpnext/www/book_appointment/index.css b/erpnext/www/book_appointment/index.css index 6c49fde739e..277610876f7 100644 --- a/erpnext/www/book_appointment/index.css +++ b/erpnext/www/book_appointment/index.css @@ -12,7 +12,7 @@ @media (max-width: 768px) { #submit-button-area { display: grid; - grid-template-areas: + grid-template-areas: "submit" "back"; } diff --git a/erpnext/www/book_appointment/index.html b/erpnext/www/book_appointment/index.html index f242f43ad54..207175f89dc 100644 --- a/erpnext/www/book_appointment/index.html +++ b/erpnext/www/book_appointment/index.html @@ -63,4 +63,4 @@ -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/erpnext/www/book_appointment/index.py b/erpnext/www/book_appointment/index.py index ccfa97bc62f..d805c275bbc 100644 --- a/erpnext/www/book_appointment/index.py +++ b/erpnext/www/book_appointment/index.py @@ -148,4 +148,4 @@ def _deltatime_to_datetime(date, deltatime): def _datetime_to_deltatime(date_time): midnight = datetime.datetime.combine(date_time.date(), datetime.time.min) - return (date_time-midnight) \ No newline at end of file + return (date_time-midnight) diff --git a/erpnext/www/book_appointment/verify/index.html b/erpnext/www/book_appointment/verify/index.html index ebb65b1f24e..9bcd3d202e2 100644 --- a/erpnext/www/book_appointment/verify/index.html +++ b/erpnext/www/book_appointment/verify/index.html @@ -3,7 +3,7 @@ {% block title %} {{ _("Verify Email") }} {% endblock%} - + {% block page_content %} {% if success==True %} @@ -15,4 +15,4 @@ Verification failed please check the link {% endif %} -{% endblock%} \ No newline at end of file +{% endblock%} diff --git a/erpnext/www/book_appointment/verify/index.py b/erpnext/www/book_appointment/verify/index.py index d4478ae34a8..bd766c0ea8a 100644 --- a/erpnext/www/book_appointment/verify/index.py +++ b/erpnext/www/book_appointment/verify/index.py @@ -17,4 +17,4 @@ def get_context(context): return context else: context.success = False - return context \ No newline at end of file + return context diff --git a/erpnext/www/lms/content.py b/erpnext/www/lms/content.py index 0c048453625..05cbb16d3cb 100644 --- a/erpnext/www/lms/content.py +++ b/erpnext/www/lms/content.py @@ -65,4 +65,4 @@ def allowed_content_access(program, content, content_type): and `tabTopic Content`.parent = `tabCourse Topic`.topic and `tabProgram Course`.parent = %(program)s""", {'program': program}) - return (content, content_type) in contents_of_program \ No newline at end of file + return (content, content_type) in contents_of_program diff --git a/erpnext/www/lms/course.html b/erpnext/www/lms/course.html index 0d70ed5cefd..c07b9402b10 100644 --- a/erpnext/www/lms/course.html +++ b/erpnext/www/lms/course.html @@ -103,4 +103,4 @@ -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/erpnext/www/lms/index.py b/erpnext/www/lms/index.py index 26f59a2395e..c14b94326b5 100644 --- a/erpnext/www/lms/index.py +++ b/erpnext/www/lms/index.py @@ -13,4 +13,4 @@ def get_context(context): def get_featured_programs(): - return utils.get_portal_programs() or [] \ No newline at end of file + return utils.get_portal_programs() or [] diff --git a/erpnext/www/lms/macros/card.html b/erpnext/www/lms/macros/card.html index dc8fc5c72c7..3cbdec61aa0 100644 --- a/erpnext/www/lms/macros/card.html +++ b/erpnext/www/lms/macros/card.html @@ -31,4 +31,4 @@
          -{% endmacro %} \ No newline at end of file +{% endmacro %} diff --git a/erpnext/www/lms/macros/hero.html b/erpnext/www/lms/macros/hero.html index 94f239eb8ed..e72bfc8175b 100644 --- a/erpnext/www/lms/macros/hero.html +++ b/erpnext/www/lms/macros/hero.html @@ -52,4 +52,4 @@ } {% endblock %} -{% endmacro %} \ No newline at end of file +{% endmacro %} diff --git a/erpnext/www/lms/profile.py b/erpnext/www/lms/profile.py index 4788ea6e70b..7e338e38f13 100644 --- a/erpnext/www/lms/profile.py +++ b/erpnext/www/lms/profile.py @@ -23,4 +23,4 @@ def get_program_progress(student): completion = utils.get_program_completion(program) student_progress.append({'program': program.program_name, 'name': program.name, 'progress':progress, 'completion': completion}) - return student_progress \ No newline at end of file + return student_progress diff --git a/erpnext/www/lms/program.html b/erpnext/www/lms/program.html index 7ad618630a4..30528c667dd 100644 --- a/erpnext/www/lms/program.html +++ b/erpnext/www/lms/program.html @@ -84,4 +84,4 @@ -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/erpnext/www/lms/program.py b/erpnext/www/lms/program.py index 104d3fa315a..a4f588ccf18 100644 --- a/erpnext/www/lms/program.py +++ b/erpnext/www/lms/program.py @@ -26,4 +26,4 @@ def get_program(program_name): def get_course_progress(courses, program): progress = {course.name: utils.get_course_progress(course, program) for course in courses} - return progress or {} \ No newline at end of file + return progress or {} diff --git a/erpnext/www/lms/topic.html b/erpnext/www/lms/topic.html index cd24616cd45..dc69599112a 100644 --- a/erpnext/www/lms/topic.html +++ b/erpnext/www/lms/topic.html @@ -55,4 +55,4 @@ -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/erpnext/www/lms/topic.py b/erpnext/www/lms/topic.py index 8abbc72e918..993828090ce 100644 --- a/erpnext/www/lms/topic.py +++ b/erpnext/www/lms/topic.py @@ -42,4 +42,4 @@ def get_contents(topic, course, program): result = None progress.append({'content': content, 'content_type': content.doctype, 'completed': status, 'score': score, 'result': result}) - return progress \ No newline at end of file + return progress diff --git a/erpnext/www/support/index.html b/erpnext/www/support/index.html index 12b4c2c0819..3c19198cc16 100644 --- a/erpnext/www/support/index.html +++ b/erpnext/www/support/index.html @@ -96,6 +96,6 @@ .search-container { margin-top: 1.2rem; max-width: 500px; - } + } {%- endblock -%} diff --git a/erpnext/www/support/index.py b/erpnext/www/support/index.py index 5d267430c16..70090c7805d 100644 --- a/erpnext/www/support/index.py +++ b/erpnext/www/support/index.py @@ -8,7 +8,7 @@ def get_context(context): context.greeting_title = setting.greeting_title context.greeting_subtitle = setting.greeting_subtitle - + # Support content favorite_articles = get_favorite_articles_by_page_view() if len(favorite_articles) < 6: @@ -16,15 +16,15 @@ def get_context(context): if favorite_articles: for article in favorite_articles: name_list.append(article.name) - for record in (frappe.get_all("Help Article", - fields=["title", "content", "route", "category"], - filters={"name": ['not in', tuple(name_list)], "published": 1}, + for record in (frappe.get_all("Help Article", + fields=["title", "content", "route", "category"], + filters={"name": ['not in', tuple(name_list)], "published": 1}, order_by="creation desc", limit=(6-len(favorite_articles)))): favorite_articles.append(record) - + context.favorite_article_list = get_favorite_articles(favorite_articles) context.help_article_list = get_help_article_list() - + def get_favorite_articles_by_page_view(): return frappe.db.sql( """ @@ -34,13 +34,13 @@ def get_favorite_articles_by_page_view(): t1.content as content, t1.route as route, t1.category as category, - count(t1.route) as count - FROM `tabHelp Article` AS t1 + count(t1.route) as count + FROM `tabHelp Article` AS t1 INNER JOIN - `tabWeb Page View` AS t2 - ON t1.route = t2.path + `tabWeb Page View` AS t2 + ON t1.route = t2.path WHERE t1.published = 1 - GROUP BY route + GROUP BY route ORDER BY count DESC LIMIT 6; """, as_dict=True) @@ -71,4 +71,4 @@ def get_help_article_list(): 'articles': help_articles, } help_article_list.append(help_aricles_per_caetgory) - return help_article_list \ No newline at end of file + return help_article_list diff --git a/package.json b/package.json index c9ee7a622c4..5bc1e56a210 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,8 @@ "snyk": "^1.518.0" }, "dependencies": { - "onscan.js": "^1.5.2" + "onscan.js": "^1.5.2", + "html2canvas": "^1.1.4" }, "scripts": { "snyk-protect": "snyk protect", diff --git a/yarn.lock b/yarn.lock index 0a2ac1affc8..cc01d89344a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -688,6 +688,11 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= +base64-arraybuffer@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.2.0.tgz#4b944fac0191aa5907afe2d8c999ccc57ce80f45" + integrity sha512-7emyCsu1/xiBXgQZrscw/8KPRT44I4Yq9Pe6EGs3aPRTsWuggML1/1DTuZUuIaJPIm1FTDUVXl4x/yW8s0kQDQ== + base64-js@^1.3.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" @@ -997,6 +1002,13 @@ crypto-random-string@^2.0.0: resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== +css-line-break@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/css-line-break/-/css-line-break-1.1.1.tgz#d5e9bdd297840099eb0503c7310fd34927a026ef" + integrity sha512-1feNVaM4Fyzdj4mKPIQNL2n70MmuYzAXZ1aytlROFX1JsOo070OsugwGjj7nl6jnDJWHDM8zRZswkmeYVWZJQA== + dependencies: + base64-arraybuffer "^0.2.0" + debug@^3.1.0, debug@^3.2.6: version "3.2.6" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" @@ -1472,6 +1484,13 @@ hosted-git-info@^3.0.4, hosted-git-info@^3.0.7: dependencies: lru-cache "^6.0.0" +html2canvas@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/html2canvas/-/html2canvas-1.1.4.tgz#53ae91cd26e9e9e623c56533cccb2e3f57c8124c" + integrity sha512-uHgQDwrXsRmFdnlOVFvHin9R7mdjjZvoBoXxicPR+NnucngkaLa5zIDW9fzMkiip0jSffyTyWedE8iVogYOeWg== + dependencies: + css-line-break "1.1.1" + http-cache-semantics@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390"