Merge branch 'develop' into e-commerce-refactor-develop
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
deleted file mode 100644
index c145291..0000000
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ /dev/null
@@ -1,47 +0,0 @@
----
-name: Bug report
-about: Report a bug encountered while using ERPNext
-labels: bug
----
-
-<!--
-Welcome to ERPNext issue tracker! Before creating an issue, please heed the following:
-
-1. This tracker should only be used to report bugs and request features / enhancements to ERPNext
- - For questions and general support, checkout the manual https://erpnext.com/docs/user/manual/en or use https://discuss.erpnext.com
- - For documentation issues, refer to https://github.com/frappe/erpnext_com
-2. Use the search function before creating a new issue. Duplicates will be closed and directed to
- the original discussion.
-3. When making a bug report, make sure you provide all required information. The easier it is for
- maintainers to reproduce, the faster it'll be fixed.
-4. If you think you know what the reason for the bug is, share it with us. Maybe put in a PR 😉
--->
-
-## Description of the issue
-
-## Context information (for bug reports)
-
-**Output of `bench version`**
-```
-(paste here)
-```
-
-## Steps to reproduce the issue
-
-1.
-2.
-3.
-
-### Observed result
-
-### Expected result
-
-### Stacktrace / full error message
-
-```
-(paste here)
-```
-
-## Additional information
-
-OS version / distribution, `ERPNext` install method, etc.
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml
new file mode 100644
index 0000000..4d61f1f
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.yaml
@@ -0,0 +1,89 @@
+name: Bug Report
+description: Report a bug encountered while using ERPNext
+labels: ["bug"]
+
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Welcome to ERPNext issue tracker! Before creating an issue, please heed the following:
+
+ 1. This tracker should only be used to report bugs and request features / enhancements to ERPNext
+ - For questions and general support, checkout the [user manual](https://docs.erpnext.com/) or use [forum](https://discuss.erpnext.com)
+ - For documentation issues, propose edit on [documentation site](https://docs.erpnext.com/) directly.
+ 2. When making a bug report, make sure you provide all required information. The easier it is for
+ maintainers to reproduce, the faster it'll be fixed.
+ 3. If you think you know what the reason for the bug is, share it with us. Maybe put in a PR 😉
+
+ - type: textarea
+ id: bug-info
+ attributes:
+ label: Information about bug
+ description: Also tell us, what did you expect to happen?
+ placeholder: Please provide as much information as possible.
+ validations:
+ required: true
+
+ - type: dropdown
+ id: module
+ attributes:
+ label: Module
+ description: Select affected module of ERPNext.
+ multiple: true
+ options:
+ - accounts
+ - stock
+ - buying
+ - selling
+ - ecommerce
+ - manufacturing
+ - HR
+ - projects
+ - support
+ - CRM
+ - assets
+ - integrations
+ - quality
+ - regional
+ - portal
+ - agriculture
+ - education
+ - non-profit
+ - other
+ validations:
+ required: true
+
+ - type: textarea
+ id: exact-version
+ attributes:
+ label: Version
+ description: Share exact version number of Frappe and ERPNext you are using.
+ placeholder: |
+ Frappe version -
+ ERPNext Verion -
+ validations:
+ required: true
+
+ - type: dropdown
+ id: install-method
+ attributes:
+ label: Installation method
+ options:
+ - docker
+ - easy-install
+ - manual install
+ - FrappeCloud
+ validations:
+ required: false
+
+ - type: textarea
+ id: logs
+ attributes:
+ label: Relevant log output / Stack trace / Full Error Message.
+ description: Please copy and paste any relevant log output. This will be automatically formatted.
+ render: shell
+
+ - type: markdown
+ attributes:
+ value: |
+ By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/frappe/erpnext/blob/develop/CODE_OF_CONDUCT.md)
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
index 6cdad35..418bf3c 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.md
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -1,7 +1,10 @@
---
name: Feature request
about: Suggest an idea to improve ERPNext
+title: ''
labels: feature-request
+assignees: ''
+
---
<!--
diff --git a/.github/ISSUE_TEMPLATE/question-about-using-erpnext.md b/.github/ISSUE_TEMPLATE/question-about-using-erpnext.md
deleted file mode 100644
index 2016bcc..0000000
--- a/.github/ISSUE_TEMPLATE/question-about-using-erpnext.md
+++ /dev/null
@@ -1,17 +0,0 @@
----
-name: Question about using ERPNext
-about: This is not the appropriate channel
-labels: invalid
----
-
-Please post on our forums:
-
-for questions about using `ERPNext`: https://discuss.erpnext.com
-
-for questions about using the `Frappe Framework`: ~~https://discuss.frappe.io~~ => [stackoverflow](https://stackoverflow.com/questions/tagged/frappe) tagged under `frappe`
-
-for questions about using `bench`, probably the best place to start is the [bench repo](https://github.com/frappe/bench)
-
-For documentation issues, use the [ERPNext Documentation](https://erpnext.com/docs/) or [Frappe Framework Documentation](https://frappe.io/docs/user/en) or the [developer cheetsheet](https://github.com/frappe/frappe/wiki/Developer-Cheatsheet)
-
-> **Posts that are not bug reports or feature requests will not be addressed on this issue tracker.**
\ No newline at end of file
diff --git a/.github/helper/install.sh b/.github/helper/install.sh
index 85f146d..9031968 100644
--- a/.github/helper/install.sh
+++ b/.github/helper/install.sh
@@ -12,17 +12,30 @@
bench init --skip-assets --frappe-path ~/frappe --python "$(which python)" frappe-bench
mkdir ~/frappe-bench/sites/test_site
-cp -r "${GITHUB_WORKSPACE}/.github/helper/site_config.json" ~/frappe-bench/sites/test_site/
-mysql --host 127.0.0.1 --port 3306 -u root -e "SET GLOBAL character_set_server = 'utf8mb4'"
-mysql --host 127.0.0.1 --port 3306 -u root -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'"
+if [ "$DB" == "mariadb" ];then
+ cp -r "${GITHUB_WORKSPACE}/.github/helper/site_config_mariadb.json" ~/frappe-bench/sites/test_site/site_config.json
+else
+ cp -r "${GITHUB_WORKSPACE}/.github/helper/site_config_postgres.json" ~/frappe-bench/sites/test_site/site_config.json
+fi
-mysql --host 127.0.0.1 --port 3306 -u root -e "CREATE USER 'test_frappe'@'localhost' IDENTIFIED BY 'test_frappe'"
-mysql --host 127.0.0.1 --port 3306 -u root -e "CREATE DATABASE test_frappe"
-mysql --host 127.0.0.1 --port 3306 -u root -e "GRANT ALL PRIVILEGES ON \`test_frappe\`.* TO 'test_frappe'@'localhost'"
-mysql --host 127.0.0.1 --port 3306 -u root -e "UPDATE mysql.user SET Password=PASSWORD('travis') WHERE User='root'"
-mysql --host 127.0.0.1 --port 3306 -u root -e "FLUSH PRIVILEGES"
+if [ "$DB" == "mariadb" ];then
+ mysql --host 127.0.0.1 --port 3306 -u root -e "SET GLOBAL character_set_server = 'utf8mb4'"
+ mysql --host 127.0.0.1 --port 3306 -u root -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'"
+
+ mysql --host 127.0.0.1 --port 3306 -u root -e "CREATE USER 'test_frappe'@'localhost' IDENTIFIED BY 'test_frappe'"
+ mysql --host 127.0.0.1 --port 3306 -u root -e "CREATE DATABASE test_frappe"
+ mysql --host 127.0.0.1 --port 3306 -u root -e "GRANT ALL PRIVILEGES ON \`test_frappe\`.* TO 'test_frappe'@'localhost'"
+
+ mysql --host 127.0.0.1 --port 3306 -u root -e "UPDATE mysql.user SET Password=PASSWORD('travis') WHERE User='root'"
+ mysql --host 127.0.0.1 --port 3306 -u root -e "FLUSH PRIVILEGES"
+fi
+
+if [ "$DB" == "postgres" ];then
+ echo "travis" | psql -h 127.0.0.1 -p 5432 -c "CREATE DATABASE test_frappe" -U postgres;
+ echo "travis" | psql -h 127.0.0.1 -p 5432 -c "CREATE USER test_frappe WITH PASSWORD 'test_frappe'" -U postgres;
+fi
wget -O /tmp/wkhtmltox.tar.xz https://github.com/frappe/wkhtmltopdf/raw/master/wkhtmltox-0.12.3_linux-generic-amd64.tar.xz
tar -xf /tmp/wkhtmltox.tar.xz -C /tmp
diff --git a/.github/helper/site_config.json b/.github/helper/site_config_mariadb.json
similarity index 99%
rename from .github/helper/site_config.json
rename to .github/helper/site_config_mariadb.json
index 60ef80c..948ad08 100644
--- a/.github/helper/site_config.json
+++ b/.github/helper/site_config_mariadb.json
@@ -13,4 +13,4 @@
"host_name": "http://test_site:8000",
"install_apps": ["erpnext"],
"throttle_user_limit": 100
-}
\ No newline at end of file
+}
diff --git a/.github/helper/site_config.json b/.github/helper/site_config_postgres.json
similarity index 80%
copy from .github/helper/site_config.json
copy to .github/helper/site_config_postgres.json
index 60ef80c..c82905f 100644
--- a/.github/helper/site_config.json
+++ b/.github/helper/site_config_postgres.json
@@ -1,16 +1,18 @@
{
"db_host": "127.0.0.1",
- "db_port": 3306,
+ "db_port": 5432,
"db_name": "test_frappe",
"db_password": "test_frappe",
+ "db_type": "postgres",
+ "allow_tests": true,
"auto_email_id": "test@example.com",
"mail_server": "smtp.example.com",
"mail_login": "test@example.com",
"mail_password": "test",
"admin_password": "admin",
- "root_login": "root",
+ "root_login": "postgres",
"root_password": "travis",
"host_name": "http://test_site:8000",
"install_apps": ["erpnext"],
"throttle_user_limit": 100
-}
\ No newline at end of file
+}
diff --git a/.github/labeler.yml b/.github/labeler.yml
new file mode 100644
index 0000000..fc3f06d
--- /dev/null
+++ b/.github/labeler.yml
@@ -0,0 +1,55 @@
+accounts:
+- 'erpnext/accounts/*'
+- 'erpnext/controllers/accounts_controller.py'
+- 'erpnext/controllers/taxes_and_totals.py'
+
+stock:
+- 'erpnext/stock/*'
+- 'erpnext/controllers/stock_controller.py'
+- 'erpnext/controllers/item_variant.py'
+
+assets:
+- 'erpnext/assets/*'
+
+regional:
+- 'erpnext/regional/*'
+
+selling:
+- 'erpnext/selling/*'
+- 'erpnext/controllers/selling_controller.py'
+
+buying:
+- 'erpnext/buying/*'
+- 'erpnext/controllers/buying_controller.py'
+
+support:
+- 'erpnext/support/*'
+
+POS:
+- 'pos*'
+
+ecommerce:
+- 'erpnext/e_commerce/*'
+
+maintenance:
+- 'erpnext/maintenance/*'
+
+manufacturing:
+- 'erpnext/manufacturing/*'
+
+crm:
+- 'erpnext/crm/*'
+
+HR:
+- 'erpnext/hr/*'
+
+payroll:
+- 'erpnext/payroll*'
+
+projects:
+- 'erpnext/projects/*'
+
+# Any python files modifed but no test files modified
+needs-tests:
+- any: ['erpnext/**/*.py']
+ all: ['!erpnext/**/test*.py']
diff --git a/.github/stale.yml b/.github/stale.yml
index 9322ae8..8b7cb9b 100644
--- a/.github/stale.yml
+++ b/.github/stale.yml
@@ -1,34 +1,36 @@
# Configuration for probot-stale - https://github.com/probot/stale
-# Number of days of inactivity before an Issue or Pull Request becomes stale
-daysUntilStale: 15
+# Label to use when marking as stale
+staleLabel: inactive
-# Number of days of inactivity before a stale Issue or Pull Request is closed.
-# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
-daysUntilClose: 3
-
-# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable
-exemptLabels:
- - hotfix
+# Limit the number of actions per hour, from 1-30. Default is 30
+limitPerRun: 10
# Set to true to ignore issues in a project (defaults to false)
-exemptProjects: false
+exemptProjects: true
# Set to true to ignore issues in a milestone (defaults to false)
exemptMilestones: true
-# Label to use when marking as stale
-staleLabel: inactive
+pulls:
+ daysUntilStale: 15
+ daysUntilClose: 3
+ exemptLabels:
+ - hotfix
+ markComment: >
+ This pull request has been automatically marked as inactive because it has
+ not had recent activity. It will be closed within 3 days if no further
+ activity occurs, but it only takes a comment to keep a contribution alive
+ :) Also, even if it is closed, you can always reopen the PR when you're
+ ready. Thank you for contributing.
-# Comment to post when marking as stale. Set to `false` to disable
-markComment: >
- This pull request has been automatically marked as stale because it has not had
- recent activity. It will be closed within a week if no further activity occurs, but it
- only takes a comment to keep a contribution alive :) Also, even if it is closed,
- you can always reopen the PR when you're ready. Thank you for contributing.
-
-# Limit the number of actions per hour, from 1-30. Default is 30
-limitPerRun: 30
-
-# Limit to only `issues` or `pulls`
-only: pulls
+issues:
+ daysUntilStale: 60
+ daysUntilClose: 7
+ exemptLabels:
+ - valid
+ - to-validate
+ markComment: >
+ This issue has been automatically marked as inactive because it has not had
+ recent activity and it wasn't validated by maintainer team. It will be
+ closed within a week if no further activity occurs.
diff --git a/.github/workflows/docs-checker.yml b/.github/workflows/docs-checker.yml
index db46c56..b644568 100644
--- a/.github/workflows/docs-checker.yml
+++ b/.github/workflows/docs-checker.yml
@@ -12,7 +12,7 @@
- name: 'Setup Environment'
uses: actions/setup-python@v2
with:
- python-version: 3.6
+ python-version: 3.8
- name: 'Clone repo'
uses: actions/checkout@v2
diff --git a/.github/workflows/labeller.yml b/.github/workflows/labeller.yml
new file mode 100644
index 0000000..a774400
--- /dev/null
+++ b/.github/workflows/labeller.yml
@@ -0,0 +1,12 @@
+name: "Pull Request Labeler"
+on:
+ pull_request_target:
+ types: [opened, reopened]
+
+jobs:
+ triage:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/labeler@v3
+ with:
+ repo-token: "${{ secrets.GITHUB_TOKEN }}"
diff --git a/.github/workflows/patch.yml b/.github/workflows/patch.yml
index 97bccf5..d05bbbe 100644
--- a/.github/workflows/patch.yml
+++ b/.github/workflows/patch.yml
@@ -34,7 +34,7 @@
- name: Setup Python
uses: actions/setup-python@v2
with:
- python-version: 3.7
+ python-version: 3.8
- name: Setup Node
uses: actions/setup-node@v2
@@ -80,6 +80,9 @@
- name: Install
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
+ env:
+ DB: mariadb
+ TYPE: server
- name: Run Patch Tests
run: |
diff --git a/.github/workflows/server-tests.yml b/.github/workflows/server-tests-mariadb.yml
similarity index 94%
rename from .github/workflows/server-tests.yml
rename to .github/workflows/server-tests-mariadb.yml
index 77c0aee..7347a58 100644
--- a/.github/workflows/server-tests.yml
+++ b/.github/workflows/server-tests-mariadb.yml
@@ -1,10 +1,11 @@
-name: Server
+name: Server (Mariadb)
on:
pull_request:
paths-ignore:
- '**.js'
- '**.md'
+ - '**.html'
workflow_dispatch:
push:
branches: [ develop ]
@@ -13,7 +14,7 @@
- '**.md'
concurrency:
- group: server-develop-${{ github.event.number }}
+ group: server-mariadb-develop-${{ github.event.number }}
cancel-in-progress: true
jobs:
@@ -45,7 +46,7 @@
- name: Setup Python
uses: actions/setup-python@v2
with:
- python-version: 3.7
+ python-version: 3.8
- name: Setup Node
uses: actions/setup-node@v2
@@ -92,6 +93,7 @@
- name: Install
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
env:
+ DB: mariadb
TYPE: server
- name: Run Tests
diff --git a/.github/workflows/server-tests.yml b/.github/workflows/server-tests-postgres.yml
similarity index 73%
copy from .github/workflows/server-tests.yml
copy to .github/workflows/server-tests-postgres.yml
index 77c0aee..77d3c1a 100644
--- a/.github/workflows/server-tests.yml
+++ b/.github/workflows/server-tests-postgres.yml
@@ -1,51 +1,52 @@
-name: Server
+name: Server (Postgres)
on:
pull_request:
paths-ignore:
- '**.js'
- '**.md'
- workflow_dispatch:
- push:
- branches: [ develop ]
- paths-ignore:
- - '**.js'
- - '**.md'
+ - '**.html'
+ types: [opened, labelled, synchronize, reopened]
concurrency:
- group: server-develop-${{ github.event.number }}
+ group: server-postgres-develop-${{ github.event.number }}
cancel-in-progress: true
jobs:
test:
+ if: ${{ contains(github.event.pull_request.labels.*.name, 'postgres') }}
runs-on: ubuntu-latest
timeout-minutes: 60
strategy:
fail-fast: false
-
matrix:
- container: [1, 2, 3]
+ container: [1, 2, 3]
name: Python Unit Tests
services:
- mysql:
- image: mariadb:10.3
+ postgres:
+ image: postgres:13.3
env:
- MYSQL_ALLOW_EMPTY_PASSWORD: YES
+ POSTGRES_PASSWORD: travis
+ options: >-
+ --health-cmd pg_isready
+ --health-interval 10s
+ --health-timeout 5s
+ --health-retries 5
ports:
- - 3306:3306
- options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
+ - 5432:5432
steps:
+
- name: Clone
uses: actions/checkout@v2
- name: Setup Python
uses: actions/setup-python@v2
with:
- python-version: 3.7
+ python-version: 3.8
- name: Setup Node
uses: actions/setup-node@v2
@@ -89,22 +90,16 @@
restore-keys: |
${{ runner.os }}-yarn-
+
- name: Install
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
env:
+ DB: postgres
TYPE: server
- name: Run Tests
- run: cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --app erpnext --use-orchestrator --with-coverage
+ run: cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --app erpnext --use-orchestrator
env:
TYPE: server
CI_BUILD_ID: ${{ github.run_id }}
ORCHESTRATOR_URL: http://test-orchestrator.frappe.io
-
- - name: Upload coverage data
- uses: codecov/codecov-action@v2
- with:
- name: MariaDB
- fail_ci_if_error: true
- files: /home/runner/frappe-bench/sites/coverage.xml
- verbose: true
diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml
index d765f04..ab6a53b 100644
--- a/.github/workflows/ui-tests.yml
+++ b/.github/workflows/ui-tests.yml
@@ -36,7 +36,7 @@
- name: Setup Python
uses: actions/setup-python@v2
with:
- python-version: 3.7
+ python-version: 3.8
- uses: actions/setup-node@v2
with:
diff --git a/CODEOWNERS b/CODEOWNERS
index a4a14de..bfc2601 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -23,13 +23,13 @@
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
+erpnext/controllers/ @deepeshgarg007 @nextchamp-saqib @rohitwaghchaure @marination @ankush
+erpnext/patches/ @deepeshgarg007 @nextchamp-saqib @marination @ankush
+erpnext/public/ @nextchamp-saqib @marination
-.github/ @surajshetty3416 @ankush
+.github/ @ankush
requirements.txt @gavindsouza
diff --git a/codecov.yml b/codecov.yml
index 67bd445..1fa602a 100644
--- a/codecov.yml
+++ b/codecov.yml
@@ -8,6 +8,16 @@
target: auto
threshold: 0.5%
+ patch:
+ default:
+ target: 85%
+ threshold: 0%
+ base: auto
+ branches:
+ - develop
+ if_ci_failed: ignore
+ only_pulls: true
+
comment:
layout: "diff, files"
require_changes: true
diff --git a/dev-requirements.txt b/dev-requirements.txt
new file mode 100644
index 0000000..15545c0
--- /dev/null
+++ b/dev-requirements.txt
@@ -0,0 +1 @@
+hypothesis~=6.31.0
diff --git a/erpnext/__init__.py b/erpnext/__init__.py
index 9d99ebb..0b4696c 100644
--- a/erpnext/__init__.py
+++ b/erpnext/__init__.py
@@ -4,7 +4,7 @@
from erpnext.hooks import regional_overrides
-__version__ = '13.9.0'
+__version__ = '14.0.0-dev'
def get_default_company(user=None):
'''Get default company for user'''
@@ -55,9 +55,9 @@
company.enable_perpetual_inventory = enable
company.save()
-def encode_company_abbr(name, company):
+def encode_company_abbr(name, company=None, abbr=None):
'''Returns name encoded with company abbreviation'''
- company_abbr = frappe.get_cached_value('Company', company, "abbr")
+ company_abbr = abbr or frappe.get_cached_value('Company', company, "abbr")
parts = name.rsplit(" - ", 1)
if parts[-1].lower() != company_abbr.lower():
diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py
index 5df8269..22c81dd 100644
--- a/erpnext/accounts/deferred_revenue.py
+++ b/erpnext/accounts/deferred_revenue.py
@@ -374,12 +374,13 @@
frappe.db.commit()
except Exception as e:
if frappe.flags.in_test:
+ traceback = frappe.get_traceback()
+ frappe.log_error(title=_('Error while processing deferred accounting for Invoice {0}').format(doc.name), message=traceback)
raise e
else:
frappe.db.rollback()
traceback = frappe.get_traceback()
- frappe.log_error(message=traceback)
-
+ frappe.log_error(title=_('Error while processing deferred accounting for Invoice {0}').format(doc.name), message=traceback)
frappe.flags.deferred_accounting_error = True
def send_mail(deferred_process):
@@ -446,10 +447,12 @@
if submit:
journal_entry.submit()
+
+ frappe.db.commit()
except Exception:
frappe.db.rollback()
traceback = frappe.get_traceback()
- frappe.log_error(message=traceback)
+ frappe.log_error(title=_('Error while processing deferred accounting for Invoice {0}').format(doc.name), message=traceback)
frappe.flags.deferred_accounting_error = True
diff --git a/erpnext/accounts/doctype/account/account.js b/erpnext/accounts/doctype/account/account.js
index 7a1d735..320e1ca 100644
--- a/erpnext/accounts/doctype/account/account.js
+++ b/erpnext/accounts/doctype/account/account.js
@@ -43,12 +43,12 @@
frm.trigger('add_toolbar_buttons');
}
if (frm.has_perm('write')) {
- frm.add_custom_button(__('Update Account Name / Number'), function () {
- frm.trigger("update_account_number");
- });
frm.add_custom_button(__('Merge Account'), function () {
frm.trigger("merge_account");
- });
+ }, __('Actions'));
+ frm.add_custom_button(__('Update Account Name / Number'), function () {
+ frm.trigger("update_account_number");
+ }, __('Actions'));
}
}
},
@@ -59,11 +59,12 @@
}
},
add_toolbar_buttons: function(frm) {
- frm.add_custom_button(__('Chart of Accounts'),
- function () { frappe.set_route("Tree", "Account"); });
+ frm.add_custom_button(__('Chart of Accounts'), () => {
+ frappe.set_route("Tree", "Account");
+ }, __('View'));
if (frm.doc.is_group == 1) {
- frm.add_custom_button(__('Group to Non-Group'), function () {
+ frm.add_custom_button(__('Convert to Non-Group'), function () {
return frappe.call({
doc: frm.doc,
method: 'convert_group_to_ledger',
@@ -71,10 +72,11 @@
frm.refresh();
}
});
- });
+ }, __('Actions'));
+
} else if (cint(frm.doc.is_group) == 0
&& frappe.boot.user.can_read.indexOf("GL Entry") !== -1) {
- frm.add_custom_button(__('Ledger'), function () {
+ frm.add_custom_button(__('General Ledger'), function () {
frappe.route_options = {
"account": frm.doc.name,
"from_date": frappe.sys_defaults.year_start_date,
@@ -82,9 +84,9 @@
"company": frm.doc.company
};
frappe.set_route("query-report", "General Ledger");
- });
+ }, __('View'));
- frm.add_custom_button(__('Non-Group to Group'), function () {
+ frm.add_custom_button(__('Convert to Group'), function () {
return frappe.call({
doc: frm.doc,
method: 'convert_ledger_to_group',
@@ -92,7 +94,7 @@
frm.refresh();
}
});
- });
+ }, __('Actions'));
}
},
diff --git a/erpnext/accounts/doctype/account/account_tree.js b/erpnext/accounts/doctype/account/account_tree.js
index a4b6e0b..a3ef384 100644
--- a/erpnext/accounts/doctype/account/account_tree.js
+++ b/erpnext/accounts/doctype/account/account_tree.js
@@ -78,6 +78,7 @@
const format = (value, currency) => format_currency(Math.abs(value), currency);
if (account.balance!==undefined) {
+ node.parent && node.parent.find('.balance-area').remove();
$('<span class="balance-area pull-right">'
+ (account.balance_in_account_currency ?
(format(account.balance_in_account_currency, account.account_currency) + " / ") : "")
@@ -175,7 +176,7 @@
&& node.expandable && !node.hide_add;
},
click: function() {
- var me = frappe.treeview_settings['Account'].treeview;
+ var me = frappe.views.trees['Account'];
me.new_node();
},
btnClass: "hidden-xs"
diff --git a/erpnext/accounts/doctype/account/tests/test_account.js b/erpnext/accounts/doctype/account/tests/test_account.js
deleted file mode 100644
index 039e33e..0000000
--- a/erpnext/accounts/doctype/account/tests/test_account.js
+++ /dev/null
@@ -1,29 +0,0 @@
-QUnit.module('accounts');
-
-QUnit.test("test account", function(assert) {
- assert.expect(4);
- let done = assert.async();
- frappe.run_serially([
- () => frappe.set_route('Tree', 'Account'),
- () => frappe.timeout(3),
- () => frappe.click_button('Expand All'),
- () => frappe.timeout(1),
- () => frappe.click_link('Debtors'),
- () => frappe.click_button('Edit'),
- () => frappe.timeout(1),
- () => {
- assert.ok(cur_frm.doc.root_type=='Asset');
- assert.ok(cur_frm.doc.report_type=='Balance Sheet');
- assert.ok(cur_frm.doc.account_type=='Receivable');
- },
- () => frappe.click_button('Ledger'),
- () => frappe.timeout(1),
- () => {
- // check if general ledger report shown
- assert.deepEqual(frappe.get_route(), ['query-report', 'General Ledger']);
- window.history.back();
- return frappe.timeout(1);
- },
- () => done()
- ]);
-});
diff --git a/erpnext/accounts/doctype/account/tests/test_account_with_number.js b/erpnext/accounts/doctype/account/tests/test_account_with_number.js
deleted file mode 100644
index c03e278..0000000
--- a/erpnext/accounts/doctype/account/tests/test_account_with_number.js
+++ /dev/null
@@ -1,69 +0,0 @@
-QUnit.module('accounts');
-
-QUnit.test("test account with number", function(assert) {
- assert.expect(7);
- let done = assert.async();
- frappe.run_serially([
- () => frappe.set_route('Tree', 'Account'),
- () => frappe.click_link('Income'),
- () => frappe.click_button('Add Child'),
- () => frappe.timeout(.5),
- () => {
- cur_dialog.fields_dict.account_name.$input.val("Test Income");
- cur_dialog.fields_dict.account_number.$input.val("4010");
- },
- () => frappe.click_button('Create New'),
- () => frappe.timeout(1),
- () => {
- assert.ok($('a:contains("4010 - Test Income"):visible').length!=0, "Account created with number");
- },
- () => frappe.click_link('4010 - Test Income'),
- () => frappe.click_button('Edit'),
- () => frappe.timeout(.5),
- () => frappe.click_button('Update Account Number'),
- () => frappe.timeout(.5),
- () => {
- cur_dialog.fields_dict.account_number.$input.val("4020");
- },
- () => frappe.timeout(1),
- () => cur_dialog.primary_action(),
- () => frappe.timeout(1),
- () => cur_frm.refresh_fields(),
- () => frappe.timeout(.5),
- () => {
- var abbr = frappe.get_abbr(frappe.defaults.get_default("Company"));
- var new_account = "4020 - Test Income - " + abbr;
- assert.ok(cur_frm.doc.name==new_account, "Account renamed");
- assert.ok(cur_frm.doc.account_name=="Test Income", "account name remained same");
- assert.ok(cur_frm.doc.account_number=="4020", "Account number updated to 4020");
- },
- () => frappe.timeout(1),
- () => frappe.click_button('Menu'),
- () => frappe.click_link('Rename'),
- () => frappe.timeout(.5),
- () => {
- cur_dialog.fields_dict.new_name.$input.val("4030 - Test Income");
- },
- () => frappe.timeout(.5),
- () => frappe.click_button("Rename"),
- () => frappe.timeout(2),
- () => {
- assert.ok(cur_frm.doc.account_name=="Test Income", "account name remained same");
- assert.ok(cur_frm.doc.account_number=="4030", "Account number updated to 4030");
- },
- () => frappe.timeout(.5),
- () => frappe.click_button('Chart of Accounts'),
- () => frappe.timeout(.5),
- () => frappe.click_button('Menu'),
- () => frappe.click_link('Refresh'),
- () => frappe.click_button('Expand All'),
- () => frappe.click_link('4030 - Test Income'),
- () => frappe.click_button('Delete'),
- () => frappe.click_button('Yes'),
- () => frappe.timeout(.5),
- () => {
- assert.ok($('a:contains("4030 - Test Account"):visible').length==0, "Account deleted");
- },
- () => done()
- ]);
-});
diff --git a/erpnext/accounts/doctype/account/tests/test_make_tax_account.js b/erpnext/accounts/doctype/account/tests/test_make_tax_account.js
deleted file mode 100644
index a0e09a1..0000000
--- a/erpnext/accounts/doctype/account/tests/test_make_tax_account.js
+++ /dev/null
@@ -1,46 +0,0 @@
-QUnit.module('accounts');
-QUnit.test("test account", assert => {
- assert.expect(3);
- let done = assert.async();
- frappe.run_serially([
- () => frappe.set_route('Tree', 'Account'),
- () => frappe.click_button('Expand All'),
- () => frappe.click_link('Duties and Taxes - '+ frappe.get_abbr(frappe.defaults.get_default("Company"))),
- () => {
- if($('a:contains("CGST"):visible').length == 0){
- return frappe.map_tax.make('CGST', 9);
- }
- },
- () => {
- if($('a:contains("SGST"):visible').length == 0){
- return frappe.map_tax.make('SGST', 9);
- }
- },
- () => {
- if($('a:contains("IGST"):visible').length == 0){
- return frappe.map_tax.make('IGST', 18);
- }
- },
- () => {
- assert.ok($('a:contains("CGST"):visible').length!=0, "CGST Checked");
- assert.ok($('a:contains("SGST"):visible').length!=0, "SGST Checked");
- assert.ok($('a:contains("IGST"):visible').length!=0, "IGST Checked");
- },
- () => done()
- ]);
-});
-
-
-frappe.map_tax = {
- make:function(text,rate){
- return frappe.run_serially([
- () => frappe.click_button('Add Child'),
- () => frappe.timeout(0.2),
- () => cur_dialog.set_value('account_name',text),
- () => cur_dialog.set_value('account_type','Tax'),
- () => cur_dialog.set_value('tax_rate',rate),
- () => cur_dialog.set_value('account_currency','INR'),
- () => frappe.click_button('Create New'),
- ]);
- }
-};
diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py
index aa132a0..4839207 100644
--- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py
+++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py
@@ -10,6 +10,8 @@
from frappe.model.document import Document
from frappe.utils import cint
+from erpnext.stock.utils import check_pending_reposting
+
class AccountsSettings(Document):
def on_update(self):
@@ -19,9 +21,13 @@
frappe.db.set_default("add_taxes_from_item_tax_template",
self.get("add_taxes_from_item_tax_template", 0))
+ frappe.db.set_default("enable_common_party_accounting",
+ self.get("enable_common_party_accounting", 0))
+
self.validate_stale_days()
self.enable_payment_schedule_in_print()
self.toggle_discount_accounting_fields()
+ self.validate_pending_reposts()
def validate_stale_days(self):
if not self.allow_stale and cint(self.stale_days) <= 0:
@@ -53,3 +59,8 @@
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)
+
+
+ def validate_pending_reposts(self):
+ if self.acc_frozen_upto:
+ check_pending_reposting(self.acc_frozen_upto)
diff --git a/erpnext/accounts/doctype/accounts_settings/test_accounts_settings.js b/erpnext/accounts/doctype/accounts_settings/test_accounts_settings.js
deleted file mode 100644
index f9aa166..0000000
--- a/erpnext/accounts/doctype/accounts_settings/test_accounts_settings.js
+++ /dev/null
@@ -1,35 +0,0 @@
-QUnit.module('accounts');
-
-QUnit.test("test: Accounts Settings doesn't allow negatives", function (assert) {
- let done = assert.async();
-
- assert.expect(2);
-
- frappe.run_serially([
- () => frappe.set_route('Form', 'Accounts Settings', 'Accounts Settings'),
- () => frappe.timeout(2),
- () => unchecked_if_checked(cur_frm, 'Allow Stale Exchange Rates', frappe.click_check),
- () => cur_frm.set_value('stale_days', 0),
- () => frappe.click_button('Save'),
- () => frappe.timeout(2),
- () => {
- assert.ok(cur_dialog);
- },
- () => frappe.click_button('Close'),
- () => cur_frm.set_value('stale_days', -1),
- () => frappe.click_button('Save'),
- () => frappe.timeout(2),
- () => {
- assert.ok(cur_dialog);
- },
- () => frappe.click_button('Close'),
- () => done()
- ]);
-
-});
-
-const unchecked_if_checked = function(frm, field_name, fn){
- if (frm.doc.allow_stale) {
- return fn(field_name);
- }
-};
diff --git a/erpnext/agriculture/__init__.py b/erpnext/accounts/doctype/advance_tax/__init__.py
similarity index 100%
copy from erpnext/agriculture/__init__.py
copy to erpnext/accounts/doctype/advance_tax/__init__.py
diff --git a/erpnext/accounts/doctype/advance_tax/advance_tax.json b/erpnext/accounts/doctype/advance_tax/advance_tax.json
new file mode 100644
index 0000000..68706ab
--- /dev/null
+++ b/erpnext/accounts/doctype/advance_tax/advance_tax.json
@@ -0,0 +1,56 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "creation": "2021-11-25 10:24:39.836195",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+ "reference_type",
+ "reference_name",
+ "reference_detail",
+ "account_head",
+ "allocated_amount"
+ ],
+ "fields": [
+ {
+ "fieldname": "reference_type",
+ "fieldtype": "Link",
+ "label": "Reference Type",
+ "options": "DocType"
+ },
+ {
+ "fieldname": "reference_name",
+ "fieldtype": "Dynamic Link",
+ "label": "Reference Name",
+ "options": "reference_type"
+ },
+ {
+ "fieldname": "reference_detail",
+ "fieldtype": "Data",
+ "label": "Reference Detail"
+ },
+ {
+ "fieldname": "account_head",
+ "fieldtype": "Link",
+ "label": "Account Head",
+ "options": "Account"
+ },
+ {
+ "fieldname": "allocated_amount",
+ "fieldtype": "Currency",
+ "label": "Allocated Amount",
+ "options": "party_account_currency"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-11-25 10:27:51.712286",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Advance Tax",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC"
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/advance_tax/advance_tax.py b/erpnext/accounts/doctype/advance_tax/advance_tax.py
new file mode 100644
index 0000000..2e784ef
--- /dev/null
+++ b/erpnext/accounts/doctype/advance_tax/advance_tax.py
@@ -0,0 +1,9 @@
+# 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 AdvanceTax(Document):
+ pass
diff --git a/erpnext/accounts/doctype/advance_taxes_and_charges/advance_taxes_and_charges.json b/erpnext/accounts/doctype/advance_taxes_and_charges/advance_taxes_and_charges.json
index 4d63499..05b284a 100644
--- a/erpnext/accounts/doctype/advance_taxes_and_charges/advance_taxes_and_charges.json
+++ b/erpnext/accounts/doctype/advance_taxes_and_charges/advance_taxes_and_charges.json
@@ -25,8 +25,7 @@
"allocated_amount",
"column_break_13",
"base_tax_amount",
- "base_total",
- "base_allocated_amount"
+ "base_total"
],
"fields": [
{
@@ -169,12 +168,6 @@
"options": "currency"
},
{
- "fieldname": "base_allocated_amount",
- "fieldtype": "Currency",
- "label": "Allocated Amount (Company Currency)",
- "options": "Company:company:default_currency"
- },
- {
"fetch_from": "account_head.account_currency",
"fieldname": "currency",
"fieldtype": "Link",
@@ -186,7 +179,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-06-09 11:46:58.373170",
+ "modified": "2021-11-25 11:10:10.945027",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Advance Taxes and Charges",
diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js
index 78e7ff6..335f850 100644
--- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js
+++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js
@@ -7,7 +7,7 @@
frm.set_query("bank_account", function () {
return {
filters: {
- company: ["in", frm.doc.company],
+ company: frm.doc.company,
'is_company_account': 1
},
};
diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
index 5cbf00b..4211bd0 100644
--- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
+++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
@@ -218,6 +218,8 @@
# updated clear date of all the vouchers based on the bank transaction
vouchers = json.loads(vouchers)
transaction = frappe.get_doc("Bank Transaction", bank_transaction_name)
+ company_account = frappe.db.get_value('Bank Account', transaction.bank_account, 'account')
+
if transaction.unallocated_amount == 0:
frappe.throw(_("This bank transaction is already fully reconciled"))
total_amount = 0
@@ -226,7 +228,7 @@
total_amount += get_paid_amount(frappe._dict({
'payment_document': voucher['payment_doctype'],
'payment_entry': voucher['payment_name'],
- }), transaction.currency)
+ }), transaction.currency, company_account)
if total_amount > transaction.unallocated_amount:
frappe.throw(_("The Sum Total of Amounts of All Selected Vouchers Should be Less than the Unallocated Amount of the Bank Transaction"))
@@ -261,7 +263,7 @@
return matching
def check_matching(bank_account, company, transaction, document_types):
- # combine all types of vocuhers
+ # combine all types of vouchers
subquery = get_queries(bank_account, company, transaction, document_types)
filters = {
"amount": transaction.unallocated_amount,
@@ -343,13 +345,11 @@
def get_je_matching_query(amount_condition, transaction):
# get matching journal entry query
+ # We have mapping at the bank level
+ # So one bank could have both types of bank accounts like asset and liability
+ # So cr_or_dr should be judged only on basis of withdrawal and deposit and not account type
company_account = frappe.get_value("Bank Account", transaction.bank_account, "account")
- root_type = frappe.get_value("Account", company_account, "root_type")
-
- if root_type == "Liability":
- cr_or_dr = "debit" if transaction.withdrawal > 0 else "credit"
- else:
- cr_or_dr = "credit" if transaction.withdrawal > 0 else "debit"
+ cr_or_dr = "credit" if transaction.withdrawal > 0 else "debit"
return f"""
@@ -434,7 +434,7 @@
def get_ec_matching_query(bank_account, company, amount_condition):
# get matching Expense Claim query
- mode_of_payments = [x["parent"] for x in frappe.db.get_list("Mode of Payment Account",
+ mode_of_payments = [x["parent"] for x in frappe.db.get_all("Mode of Payment Account",
filters={"default_account": bank_account}, fields=["parent"])]
mode_of_payments = '(\'' + '\', \''.join(mode_of_payments) + '\' )'
company_currency = get_company_currency(company)
diff --git a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js
index 0a2e0bc..990d6d9 100644
--- a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js
+++ b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js
@@ -239,7 +239,8 @@
"withdrawal",
"description",
"reference_number",
- "bank_account"
+ "bank_account",
+ "currency"
],
},
});
diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
index 4620087..44cea31 100644
--- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
+++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
@@ -102,7 +102,7 @@
AND
bt.docstatus = 1""", (payment_entry.payment_document, payment_entry.payment_entry), as_dict=True)
-def get_paid_amount(payment_entry, currency):
+def get_paid_amount(payment_entry, currency, bank_account):
if payment_entry.payment_document in ["Payment Entry", "Sales Invoice", "Purchase Invoice"]:
paid_amount_field = "paid_amount"
@@ -115,7 +115,7 @@
payment_entry.payment_entry, paid_amount_field)
elif payment_entry.payment_document == "Journal Entry":
- return frappe.db.get_value(payment_entry.payment_document, payment_entry.payment_entry, "total_credit")
+ return frappe.db.get_value('Journal Entry Account', {'parent': payment_entry.payment_entry, 'account': bank_account}, "sum(credit_in_account_currency)")
elif payment_entry.payment_document == "Expense Claim":
return frappe.db.get_value(payment_entry.payment_document, payment_entry.payment_entry, "total_amount_reimbursed")
diff --git a/erpnext/agriculture/doctype/linked_plant_analysis/__init__.py b/erpnext/accounts/doctype/currency_exchange_settings/__init__.py
similarity index 100%
rename from erpnext/agriculture/doctype/linked_plant_analysis/__init__.py
rename to erpnext/accounts/doctype/currency_exchange_settings/__init__.py
diff --git a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.js b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.js
new file mode 100644
index 0000000..6c40f2b
--- /dev/null
+++ b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.js
@@ -0,0 +1,45 @@
+// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Currency Exchange Settings', {
+ service_provider: function(frm) {
+ if (frm.doc.service_provider == "exchangerate.host") {
+ let result = ['result'];
+ let params = {
+ date: '{transaction_date}',
+ from: '{from_currency}',
+ to: '{to_currency}'
+ };
+ add_param(frm, "https://api.exchangerate.host/convert", params, result);
+ } else if (frm.doc.service_provider == "frankfurter.app") {
+ let result = ['rates', '{to_currency}'];
+ let params = {
+ base: '{from_currency}',
+ symbols: '{to_currency}'
+ };
+ add_param(frm, "https://frankfurter.app/{transaction_date}", params, result);
+ }
+ }
+});
+
+
+function add_param(frm, api, params, result) {
+ var row;
+ frm.clear_table("req_params");
+ frm.clear_table("result_key");
+
+ frm.doc.api_endpoint = api;
+
+ $.each(params, function(key, value) {
+ row = frm.add_child("req_params");
+ row.key = key;
+ row.value = value;
+ });
+
+ $.each(result, function(key, value) {
+ row = frm.add_child("result_key");
+ row.key = value;
+ });
+
+ frm.refresh_fields();
+}
diff --git a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json
new file mode 100644
index 0000000..7921fcc
--- /dev/null
+++ b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json
@@ -0,0 +1,126 @@
+{
+ "actions": [],
+ "creation": "2022-01-10 13:03:26.237081",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "api_details_section",
+ "service_provider",
+ "api_endpoint",
+ "url",
+ "column_break_3",
+ "help",
+ "section_break_2",
+ "req_params",
+ "column_break_4",
+ "result_key"
+ ],
+ "fields": [
+ {
+ "fieldname": "api_details_section",
+ "fieldtype": "Section Break",
+ "label": "API Details"
+ },
+ {
+ "fieldname": "api_endpoint",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "API Endpoint",
+ "read_only_depends_on": "eval: doc.service_provider != \"Custom\"",
+ "reqd": 1
+ },
+ {
+ "fieldname": "url",
+ "fieldtype": "Data",
+ "label": "Example URL",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "help",
+ "fieldtype": "HTML",
+ "label": "Help",
+ "options": "<h3>Currency Exchange Settings Help</h3>\n<p>There are 3 variables that could be used within the endpoint, result key and in values of the parameter.</p>\n<p>Exchange rate between {from_currency} and {to_currency} on {transaction_date} is fetched by the API.</p>\n<p>Example: If your endpoint is exchange.com/2021-08-01, then, you will have to input exchange.com/{transaction_date}</p>"
+ },
+ {
+ "fieldname": "section_break_2",
+ "fieldtype": "Section Break",
+ "label": "Request Parameters"
+ },
+ {
+ "fieldname": "req_params",
+ "fieldtype": "Table",
+ "label": "Parameters",
+ "options": "Currency Exchange Settings Details",
+ "read_only_depends_on": "eval: doc.service_provider != \"Custom\"",
+ "reqd": 1
+ },
+ {
+ "fieldname": "column_break_4",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "result_key",
+ "fieldtype": "Table",
+ "label": "Result Key",
+ "options": "Currency Exchange Settings Result",
+ "read_only_depends_on": "eval: doc.service_provider != \"Custom\"",
+ "reqd": 1
+ },
+ {
+ "fieldname": "service_provider",
+ "fieldtype": "Select",
+ "label": "Service Provider",
+ "options": "frankfurter.app\nexchangerate.host\nCustom",
+ "reqd": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "issingle": 1,
+ "links": [],
+ "modified": "2022-01-10 15:51:14.521174",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Currency Exchange Settings",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "role": "Accounts Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "role": "Accounts User",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": [],
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py
new file mode 100644
index 0000000..e16ff3a
--- /dev/null
+++ b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py
@@ -0,0 +1,82 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+import requests
+from frappe import _
+from frappe.model.document import Document
+from frappe.utils import nowdate
+
+
+class CurrencyExchangeSettings(Document):
+ def validate(self):
+ self.set_parameters_and_result()
+ response, value = self.validate_parameters()
+ self.validate_result(response, value)
+
+ def set_parameters_and_result(self):
+ if self.service_provider == 'exchangerate.host':
+ self.set('result_key', [])
+ self.set('req_params', [])
+
+ self.api_endpoint = "https://api.exchangerate.host/convert"
+ self.append('result_key', {'key': 'result'})
+ self.append('req_params', {'key': 'date', 'value': '{transaction_date}'})
+ self.append('req_params', {'key': 'from', 'value': '{from_currency}'})
+ self.append('req_params', {'key': 'to', 'value': '{to_currency}'})
+ elif self.service_provider == 'frankfurter.app':
+ self.set('result_key', [])
+ self.set('req_params', [])
+
+ self.api_endpoint = "https://frankfurter.app/{transaction_date}"
+ self.append('result_key', {'key': 'rates'})
+ self.append('result_key', {'key': '{to_currency}'})
+ self.append('req_params', {'key': 'base', 'value': '{from_currency}'})
+ self.append('req_params', {'key': 'symbols', 'value': '{to_currency}'})
+
+ def validate_parameters(self):
+ if frappe.flags.in_test:
+ return None, None
+
+ params = {}
+ for row in self.req_params:
+ params[row.key] = row.value.format(
+ transaction_date=nowdate(),
+ to_currency='INR',
+ from_currency='USD'
+ )
+
+ api_url = self.api_endpoint.format(
+ transaction_date=nowdate(),
+ to_currency='INR',
+ from_currency='USD'
+ )
+
+ try:
+ response = requests.get(api_url, params=params)
+ except requests.exceptions.RequestException as e:
+ frappe.throw("Error: " + str(e))
+
+ response.raise_for_status()
+ value = response.json()
+
+ return response, value
+
+ def validate_result(self, response, value):
+ if frappe.flags.in_test:
+ return
+
+ try:
+ for key in self.result_key:
+ value = value[str(key.key).format(
+ transaction_date=nowdate(),
+ to_currency='INR',
+ from_currency='USD'
+ )]
+ except Exception:
+ frappe.throw("Invalid result key. Response: " + response.text)
+ if not isinstance(value, (int, float)):
+ frappe.throw(_("Returned exchange rate is neither integer not float."))
+
+ self.url = response.url
+ frappe.msgprint("Exchange rate of USD to INR is " + str(value))
diff --git a/erpnext/accounts/doctype/currency_exchange_settings/test_currency_exchange_settings.py b/erpnext/accounts/doctype/currency_exchange_settings/test_currency_exchange_settings.py
new file mode 100644
index 0000000..2778729
--- /dev/null
+++ b/erpnext/accounts/doctype/currency_exchange_settings/test_currency_exchange_settings.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2021, Wahni Green Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+import unittest
+
+
+class TestCurrencyExchangeSettings(unittest.TestCase):
+ pass
diff --git a/erpnext/agriculture/__init__.py b/erpnext/accounts/doctype/currency_exchange_settings_details/__init__.py
similarity index 100%
copy from erpnext/agriculture/__init__.py
copy to erpnext/accounts/doctype/currency_exchange_settings_details/__init__.py
diff --git a/erpnext/accounts/doctype/currency_exchange_settings_details/currency_exchange_settings_details.json b/erpnext/accounts/doctype/currency_exchange_settings_details/currency_exchange_settings_details.json
new file mode 100644
index 0000000..3093587
--- /dev/null
+++ b/erpnext/accounts/doctype/currency_exchange_settings_details/currency_exchange_settings_details.json
@@ -0,0 +1,39 @@
+{
+ "actions": [],
+ "creation": "2021-09-02 14:54:49.033512",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "key",
+ "value"
+ ],
+ "fields": [
+ {
+ "fieldname": "key",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Key",
+ "reqd": 1
+ },
+ {
+ "fieldname": "value",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Value",
+ "reqd": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-11-03 19:14:55.889037",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Currency Exchange Settings Details",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/currency_exchange_settings_details/currency_exchange_settings_details.py b/erpnext/accounts/doctype/currency_exchange_settings_details/currency_exchange_settings_details.py
new file mode 100644
index 0000000..a6ad763
--- /dev/null
+++ b/erpnext/accounts/doctype/currency_exchange_settings_details/currency_exchange_settings_details.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2021, Wahni Green Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class CurrencyExchangeSettingsDetails(Document):
+ pass
diff --git a/erpnext/agriculture/__init__.py b/erpnext/accounts/doctype/currency_exchange_settings_result/__init__.py
similarity index 100%
copy from erpnext/agriculture/__init__.py
copy to erpnext/accounts/doctype/currency_exchange_settings_result/__init__.py
diff --git a/erpnext/accounts/doctype/currency_exchange_settings_result/currency_exchange_settings_result.json b/erpnext/accounts/doctype/currency_exchange_settings_result/currency_exchange_settings_result.json
new file mode 100644
index 0000000..fff5337
--- /dev/null
+++ b/erpnext/accounts/doctype/currency_exchange_settings_result/currency_exchange_settings_result.json
@@ -0,0 +1,31 @@
+{
+ "actions": [],
+ "creation": "2021-09-03 13:17:22.088259",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "key"
+ ],
+ "fields": [
+ {
+ "fieldname": "key",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Key",
+ "reqd": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-11-03 19:14:40.054245",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Currency Exchange Settings Result",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/currency_exchange_settings_result/currency_exchange_settings_result.py b/erpnext/accounts/doctype/currency_exchange_settings_result/currency_exchange_settings_result.py
new file mode 100644
index 0000000..1774128
--- /dev/null
+++ b/erpnext/accounts/doctype/currency_exchange_settings_result/currency_exchange_settings_result.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2021, Wahni Green Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class CurrencyExchangeSettingsResult(Document):
+ pass
diff --git a/erpnext/accounts/doctype/fiscal_year/test_fiscal_year.py b/erpnext/accounts/doctype/fiscal_year/test_fiscal_year.py
index bc8c6ab..69e13a4 100644
--- a/erpnext/accounts/doctype/fiscal_year/test_fiscal_year.py
+++ b/erpnext/accounts/doctype/fiscal_year/test_fiscal_year.py
@@ -5,10 +5,10 @@
import unittest
import frappe
+from frappe.utils import now_datetime
from erpnext.accounts.doctype.fiscal_year.fiscal_year import FiscalYearIncorrectDate
-test_records = frappe.get_test_records('Fiscal Year')
test_ignore = ["Company"]
class TestFiscalYear(unittest.TestCase):
@@ -25,3 +25,29 @@
})
self.assertRaises(FiscalYearIncorrectDate, fy.insert)
+
+
+def test_record_generator():
+ test_records = [
+ {
+ "doctype": "Fiscal Year",
+ "year": "_Test Short Fiscal Year 2011",
+ "is_short_year": 1,
+ "year_end_date": "2011-04-01",
+ "year_start_date": "2011-12-31"
+ }
+ ]
+
+ start = 2012
+ end = now_datetime().year + 5
+ for year in range(start, end):
+ test_records.append({
+ "doctype": "Fiscal Year",
+ "year": f"_Test Fiscal Year {year}",
+ "year_start_date": f"{year}-01-01",
+ "year_end_date": f"{year}-12-31"
+ })
+
+ return test_records
+
+test_records = test_record_generator()
diff --git a/erpnext/accounts/doctype/fiscal_year/test_records.json b/erpnext/accounts/doctype/fiscal_year/test_records.json
deleted file mode 100644
index 4405253..0000000
--- a/erpnext/accounts/doctype/fiscal_year/test_records.json
+++ /dev/null
@@ -1,69 +0,0 @@
-[
- {
- "doctype": "Fiscal Year",
- "year": "_Test Short Fiscal Year 2011",
- "is_short_year": 1,
- "year_end_date": "2011-04-01",
- "year_start_date": "2011-12-31"
- },
- {
- "doctype": "Fiscal Year",
- "year": "_Test Fiscal Year 2012",
- "year_end_date": "2012-12-31",
- "year_start_date": "2012-01-01"
- },
- {
- "doctype": "Fiscal Year",
- "year": "_Test Fiscal Year 2013",
- "year_end_date": "2013-12-31",
- "year_start_date": "2013-01-01"
- },
- {
- "doctype": "Fiscal Year",
- "year": "_Test Fiscal Year 2014",
- "year_end_date": "2014-12-31",
- "year_start_date": "2014-01-01"
- },
- {
- "doctype": "Fiscal Year",
- "year": "_Test Fiscal Year 2015",
- "year_end_date": "2015-12-31",
- "year_start_date": "2015-01-01"
- },
- {
- "doctype": "Fiscal Year",
- "year": "_Test Fiscal Year 2016",
- "year_end_date": "2016-12-31",
- "year_start_date": "2016-01-01"
- },
- {
- "doctype": "Fiscal Year",
- "year": "_Test Fiscal Year 2017",
- "year_end_date": "2017-12-31",
- "year_start_date": "2017-01-01"
- },
- {
- "doctype": "Fiscal Year",
- "year": "_Test Fiscal Year 2018",
- "year_end_date": "2018-12-31",
- "year_start_date": "2018-01-01"
- },
- {
- "doctype": "Fiscal Year",
- "year": "_Test Fiscal Year 2019",
- "year_end_date": "2019-12-31",
- "year_start_date": "2019-01-01"
- },
- {
- "doctype": "Fiscal Year",
- "year": "_Test Fiscal Year 2020",
- "year_end_date": "2020-12-31",
- "year_start_date": "2020-01-01"
- },
- {
- "doctype": "Fiscal Year",
- "year": "_Test Fiscal Year 2021",
- "year_end_date": "2021-12-31",
- "year_start_date": "2021-01-01"
- }
-]
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js
index 957a50f..617b376 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.js
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js
@@ -31,7 +31,7 @@
if(frm.doc.docstatus==1) {
frm.add_custom_button(__('Reverse Journal Entry'), function() {
return erpnext.journal_entry.reverse_journal_entry(frm);
- }, __('Make'));
+ }, __('Actions'));
}
if (frm.doc.__islocal) {
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.json b/erpnext/accounts/doctype/journal_entry/journal_entry.json
index 20678d7..335fd35 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.json
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.json
@@ -13,6 +13,7 @@
"voucher_type",
"naming_series",
"finance_book",
+ "reversal_of",
"tax_withholding_category",
"column_break1",
"from_template",
@@ -515,13 +516,21 @@
"fieldname": "apply_tds",
"fieldtype": "Check",
"label": "Apply Tax Withholding Amount "
+ },
+ {
+ "depends_on": "eval:doc.docstatus",
+ "fieldname": "reversal_of",
+ "fieldtype": "Link",
+ "label": "Reversal Of",
+ "options": "Journal Entry",
+ "read_only": 1
}
],
"icon": "fa fa-file-text",
"idx": 176,
"is_submittable": 1,
"links": [],
- "modified": "2021-09-09 15:31:14.484029",
+ "modified": "2022-01-04 13:39:36.485954",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Journal Entry",
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py
index ca17265..8fc4e8c 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.py
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py
@@ -1157,9 +1157,8 @@
def make_reverse_journal_entry(source_name, target_doc=None):
from frappe.model.mapper import get_mapped_doc
- def update_accounts(source, target, source_parent):
- target.reference_type = "Journal Entry"
- target.reference_name = source_parent.name
+ def post_process(source, target):
+ target.reversal_of = source.name
doclist = get_mapped_doc("Journal Entry", source_name, {
"Journal Entry": {
@@ -1177,9 +1176,8 @@
"debit": "credit",
"credit_in_account_currency": "debit_in_account_currency",
"credit": "debit",
- },
- "postprocess": update_accounts,
+ }
},
- }, target_doc)
+ }, target_doc, post_process)
return doclist
diff --git a/erpnext/accounts/doctype/journal_entry/test_journal_entry.js b/erpnext/accounts/doctype/journal_entry/test_journal_entry.js
deleted file mode 100644
index 28ccd95..0000000
--- a/erpnext/accounts/doctype/journal_entry/test_journal_entry.js
+++ /dev/null
@@ -1,39 +0,0 @@
-QUnit.module('Journal Entry');
-
-QUnit.test("test journal entry", function(assert) {
- assert.expect(2);
- let done = assert.async();
- frappe.run_serially([
- () => {
- return frappe.tests.make('Journal Entry', [
- {posting_date:frappe.datetime.add_days(frappe.datetime.nowdate(), 0)},
- {accounts: [
- [
- {'account':'Debtors - '+frappe.get_abbr(frappe.defaults.get_default('Company'))},
- {'party_type':'Customer'},
- {'party':'Test Customer 1'},
- {'credit_in_account_currency':1000},
- {'is_advance':'Yes'},
- ],
- [
- {'account':'HDFC - '+frappe.get_abbr(frappe.defaults.get_default('Company'))},
- {'debit_in_account_currency':1000},
- ]
- ]},
- {cheque_no:1234},
- {cheque_date: frappe.datetime.add_days(frappe.datetime.nowdate(), -1)},
- {user_remark: 'Test'},
- ]);
- },
- () => cur_frm.save(),
- () => {
- // get_item_details
- assert.ok(cur_frm.doc.total_debit==1000, "total debit correct");
- assert.ok(cur_frm.doc.total_credit==1000, "total credit correct");
- },
- () => frappe.tests.click_button('Submit'),
- () => frappe.tests.click_button('Yes'),
- () => frappe.timeout(0.3),
- () => done()
- ]);
-});
diff --git a/erpnext/agriculture/__init__.py b/erpnext/accounts/doctype/ledger_merge/__init__.py
similarity index 100%
rename from erpnext/agriculture/__init__.py
rename to erpnext/accounts/doctype/ledger_merge/__init__.py
diff --git a/erpnext/accounts/doctype/ledger_merge/ledger_merge.js b/erpnext/accounts/doctype/ledger_merge/ledger_merge.js
new file mode 100644
index 0000000..b2db98d
--- /dev/null
+++ b/erpnext/accounts/doctype/ledger_merge/ledger_merge.js
@@ -0,0 +1,128 @@
+// Copyright (c) 2021, Wahni Green Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Ledger Merge', {
+ setup: function(frm) {
+ frappe.realtime.on('ledger_merge_refresh', ({ ledger_merge }) => {
+ if (ledger_merge !== frm.doc.name) return;
+ frappe.model.clear_doc(frm.doc.doctype, frm.doc.name);
+ frappe.model.with_doc(frm.doc.doctype, frm.doc.name).then(() => {
+ frm.refresh();
+ });
+ });
+
+ frappe.realtime.on('ledger_merge_progress', data => {
+ if (data.ledger_merge !== frm.doc.name) return;
+ let message = __('Merging {0} of {1}', [data.current, data.total]);
+ let percent = Math.floor((data.current * 100) / data.total);
+ frm.dashboard.show_progress(__('Merge Progress'), percent, message);
+ frm.page.set_indicator(__('In Progress'), 'orange');
+ });
+
+ frm.set_query("account", function(doc) {
+ if (!doc.company) frappe.throw(__('Please set Company'));
+ if (!doc.root_type) frappe.throw(__('Please set Root Type'));
+ return {
+ filters: {
+ root_type: doc.root_type,
+ company: doc.company
+ }
+ };
+ });
+
+ frm.set_query('account', 'merge_accounts', function(doc) {
+ if (!doc.company) frappe.throw(__('Please set Company'));
+ if (!doc.root_type) frappe.throw(__('Please set Root Type'));
+ if (!doc.account) frappe.throw(__('Please set Account'));
+ let acc = [doc.account];
+ frm.doc.merge_accounts.forEach((row) => {
+ acc.push(row.account);
+ });
+ return {
+ filters: {
+ is_group: doc.is_group,
+ root_type: doc.root_type,
+ name: ["not in", acc],
+ company: doc.company
+ }
+ };
+ });
+ },
+
+ refresh: function(frm) {
+ frm.page.hide_icon_group();
+ frm.trigger('set_merge_status');
+ frm.trigger('update_primary_action');
+ },
+
+ after_save: function(frm) {
+ setTimeout(() => {
+ frm.trigger('update_primary_action');
+ }, 500);
+ },
+
+ update_primary_action: function(frm) {
+ if (frm.is_dirty()) {
+ frm.enable_save();
+ return;
+ }
+ frm.disable_save();
+ if (frm.doc.status !== 'Success') {
+ if (!frm.is_new()) {
+ let label = frm.doc.status === 'Pending' ? __('Start Merge') : __('Retry');
+ frm.page.set_primary_action(label, () => frm.events.start_merge(frm));
+ } else {
+ frm.page.set_primary_action(__('Save'), () => frm.save());
+ }
+ }
+ },
+
+ start_merge: function(frm) {
+ frm.call({
+ method: 'form_start_merge',
+ args: { docname: frm.doc.name },
+ btn: frm.page.btn_primary
+ }).then(r => {
+ if (r.message === true) {
+ frm.disable_save();
+ }
+ });
+ },
+
+ set_merge_status: function(frm) {
+ if (frm.doc.status == "Pending") return;
+ let successful_records = 0;
+ frm.doc.merge_accounts.forEach((row) => {
+ if (row.merged) successful_records += 1;
+ });
+ let message_args = [successful_records, frm.doc.merge_accounts.length];
+ frm.dashboard.set_headline(__('Successfully merged {0} out of {1}.', message_args));
+ },
+
+ root_type: function(frm) {
+ frm.set_value('account', '');
+ frm.set_value('merge_accounts', []);
+ },
+
+ company: function(frm) {
+ frm.set_value('account', '');
+ frm.set_value('merge_accounts', []);
+ }
+});
+
+frappe.ui.form.on('Ledger Merge Accounts', {
+ merge_accounts_add: function(frm) {
+ frm.trigger('update_primary_action');
+ },
+
+ merge_accounts_remove: function(frm) {
+ frm.trigger('update_primary_action');
+ },
+
+ account: function(frm, cdt, cdn) {
+ let row = frappe.get_doc(cdt, cdn);
+ row.account_name = row.account;
+ frm.refresh_field('merge_accounts');
+ frm.trigger('update_primary_action');
+ }
+});
diff --git a/erpnext/accounts/doctype/ledger_merge/ledger_merge.json b/erpnext/accounts/doctype/ledger_merge/ledger_merge.json
new file mode 100644
index 0000000..dd816df
--- /dev/null
+++ b/erpnext/accounts/doctype/ledger_merge/ledger_merge.json
@@ -0,0 +1,130 @@
+{
+ "actions": [],
+ "autoname": "format:{account_name} merger on {creation}",
+ "creation": "2021-12-09 15:38:04.556584",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "section_break_1",
+ "root_type",
+ "account",
+ "account_name",
+ "column_break_3",
+ "company",
+ "status",
+ "is_group",
+ "section_break_5",
+ "merge_accounts"
+ ],
+ "fields": [
+ {
+ "depends_on": "root_type",
+ "fieldname": "account",
+ "fieldtype": "Link",
+ "label": "Account",
+ "options": "Account",
+ "reqd": 1,
+ "set_only_once": 1
+ },
+ {
+ "fieldname": "section_break_1",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "merge_accounts",
+ "fieldtype": "Table",
+ "label": "Accounts to Merge",
+ "options": "Ledger Merge Accounts",
+ "reqd": 1
+ },
+ {
+ "depends_on": "account",
+ "fieldname": "section_break_5",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "label": "Company",
+ "options": "Company",
+ "reqd": 1,
+ "set_only_once": 1
+ },
+ {
+ "fieldname": "status",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "label": "Status",
+ "options": "Pending\nSuccess\nPartial Success\nError",
+ "read_only": 1
+ },
+ {
+ "fieldname": "root_type",
+ "fieldtype": "Select",
+ "label": "Root Type",
+ "options": "\nAsset\nLiability\nIncome\nExpense\nEquity",
+ "reqd": 1,
+ "set_only_once": 1
+ },
+ {
+ "depends_on": "account",
+ "fetch_from": "account.account_name",
+ "fetch_if_empty": 1,
+ "fieldname": "account_name",
+ "fieldtype": "Data",
+ "label": "Account Name",
+ "read_only": 1,
+ "reqd": 1
+ },
+ {
+ "default": "0",
+ "depends_on": "account",
+ "fetch_from": "account.is_group",
+ "fieldname": "is_group",
+ "fieldtype": "Check",
+ "label": "Is Group",
+ "read_only": 1
+ }
+ ],
+ "hide_toolbar": 1,
+ "links": [],
+ "modified": "2021-12-12 21:34:55.155146",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Ledger Merge",
+ "naming_rule": "Expression",
+ "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,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/ledger_merge/ledger_merge.py b/erpnext/accounts/doctype/ledger_merge/ledger_merge.py
new file mode 100644
index 0000000..830ad37
--- /dev/null
+++ b/erpnext/accounts/doctype/ledger_merge/ledger_merge.py
@@ -0,0 +1,76 @@
+# Copyright (c) 2021, Wahni Green Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe import _
+from frappe.model.document import Document
+
+from erpnext.accounts.doctype.account.account import merge_account
+
+
+class LedgerMerge(Document):
+ def start_merge(self):
+ from frappe.core.page.background_jobs.background_jobs import get_info
+ from frappe.utils.background_jobs import enqueue
+ from frappe.utils.scheduler import is_scheduler_inactive
+
+ if is_scheduler_inactive() and not frappe.flags.in_test:
+ frappe.throw(
+ _("Scheduler is inactive. Cannot merge accounts."), title=_("Scheduler Inactive")
+ )
+
+ enqueued_jobs = [d.get("job_name") for d in get_info()]
+
+ if self.name not in enqueued_jobs:
+ enqueue(
+ start_merge,
+ queue="default",
+ timeout=6000,
+ event="ledger_merge",
+ job_name=self.name,
+ docname=self.name,
+ now=frappe.conf.developer_mode or frappe.flags.in_test,
+ )
+ return True
+
+ return False
+
+@frappe.whitelist()
+def form_start_merge(docname):
+ return frappe.get_doc("Ledger Merge", docname).start_merge()
+
+def start_merge(docname):
+ ledger_merge = frappe.get_doc("Ledger Merge", docname)
+ successful_merges = 0
+ total = len(ledger_merge.merge_accounts)
+ for row in ledger_merge.merge_accounts:
+ if not row.merged:
+ try:
+ merge_account(
+ row.account,
+ ledger_merge.account,
+ ledger_merge.is_group,
+ ledger_merge.root_type,
+ ledger_merge.company
+ )
+ row.db_set('merged', 1)
+ frappe.db.commit()
+ successful_merges += 1
+ frappe.publish_realtime("ledger_merge_progress", {
+ "ledger_merge": ledger_merge.name,
+ "current": successful_merges,
+ "total": total
+ }
+ )
+ except Exception:
+ frappe.db.rollback()
+ frappe.log_error(title=ledger_merge.name)
+ finally:
+ if successful_merges == total:
+ ledger_merge.db_set('status', 'Success')
+ elif successful_merges > 0:
+ ledger_merge.db_set('status', 'Partial Success')
+ else:
+ ledger_merge.db_set('status', 'Error')
+
+ frappe.publish_realtime("ledger_merge_refresh", {"ledger_merge": ledger_merge.name})
diff --git a/erpnext/accounts/doctype/ledger_merge/test_ledger_merge.py b/erpnext/accounts/doctype/ledger_merge/test_ledger_merge.py
new file mode 100644
index 0000000..f731536
--- /dev/null
+++ b/erpnext/accounts/doctype/ledger_merge/test_ledger_merge.py
@@ -0,0 +1,118 @@
+# Copyright (c) 2021, Wahni Green Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+import unittest
+
+import frappe
+
+from erpnext.accounts.doctype.ledger_merge.ledger_merge import start_merge
+
+
+class TestLedgerMerge(unittest.TestCase):
+ def test_merge_success(self):
+ if not frappe.db.exists("Account", "Indirect Expenses - _TC"):
+ acc = frappe.new_doc("Account")
+ acc.account_name = "Indirect Expenses"
+ acc.is_group = 1
+ acc.parent_account = "Expenses - _TC"
+ acc.company = "_Test Company"
+ acc.insert()
+ if not frappe.db.exists("Account", "Indirect Test Expenses - _TC"):
+ acc = frappe.new_doc("Account")
+ acc.account_name = "Indirect Test Expenses"
+ acc.is_group = 1
+ acc.parent_account = "Expenses - _TC"
+ acc.company = "_Test Company"
+ acc.insert()
+ if not frappe.db.exists("Account", "Administrative Test Expenses - _TC"):
+ acc = frappe.new_doc("Account")
+ acc.account_name = "Administrative Test Expenses"
+ acc.parent_account = "Indirect Test Expenses - _TC"
+ acc.company = "_Test Company"
+ acc.insert()
+
+ doc = frappe.get_doc({
+ "doctype": "Ledger Merge",
+ "company": "_Test Company",
+ "root_type": frappe.db.get_value("Account", "Indirect Test Expenses - _TC", "root_type"),
+ "account": "Indirect Expenses - _TC",
+ "merge_accounts": [
+ {
+ "account": "Indirect Test Expenses - _TC",
+ "account_name": "Indirect Expenses"
+ }
+ ]
+ }).insert(ignore_permissions=True)
+
+ parent = frappe.db.get_value("Account", "Administrative Test Expenses - _TC", "parent_account")
+ self.assertEqual(parent, "Indirect Test Expenses - _TC")
+
+ start_merge(doc.name)
+
+ parent = frappe.db.get_value("Account", "Administrative Test Expenses - _TC", "parent_account")
+ self.assertEqual(parent, "Indirect Expenses - _TC")
+
+ self.assertFalse(frappe.db.exists("Account", "Indirect Test Expenses - _TC"))
+
+ def test_partial_merge_success(self):
+ if not frappe.db.exists("Account", "Indirect Income - _TC"):
+ acc = frappe.new_doc("Account")
+ acc.account_name = "Indirect Income"
+ acc.is_group = 1
+ acc.parent_account = "Income - _TC"
+ acc.company = "_Test Company"
+ acc.insert()
+ if not frappe.db.exists("Account", "Indirect Test Income - _TC"):
+ acc = frappe.new_doc("Account")
+ acc.account_name = "Indirect Test Income"
+ acc.is_group = 1
+ acc.parent_account = "Income - _TC"
+ acc.company = "_Test Company"
+ acc.insert()
+ if not frappe.db.exists("Account", "Administrative Test Income - _TC"):
+ acc = frappe.new_doc("Account")
+ acc.account_name = "Administrative Test Income"
+ acc.parent_account = "Indirect Test Income - _TC"
+ acc.company = "_Test Company"
+ acc.insert()
+
+ doc = frappe.get_doc({
+ "doctype": "Ledger Merge",
+ "company": "_Test Company",
+ "root_type": frappe.db.get_value("Account", "Indirect Income - _TC", "root_type"),
+ "account": "Indirect Income - _TC",
+ "merge_accounts": [
+ {
+ "account": "Indirect Test Income - _TC",
+ "account_name": "Indirect Test Income"
+ },
+ {
+ "account": "Administrative Test Income - _TC",
+ "account_name": "Administrative Test Income"
+ }
+ ]
+ }).insert(ignore_permissions=True)
+
+ parent = frappe.db.get_value("Account", "Administrative Test Income - _TC", "parent_account")
+ self.assertEqual(parent, "Indirect Test Income - _TC")
+
+ start_merge(doc.name)
+
+ parent = frappe.db.get_value("Account", "Administrative Test Income - _TC", "parent_account")
+ self.assertEqual(parent, "Indirect Income - _TC")
+
+ self.assertFalse(frappe.db.exists("Account", "Indirect Test Income - _TC"))
+ self.assertTrue(frappe.db.exists("Account", "Administrative Test Income - _TC"))
+
+ def tearDown(self):
+ for entry in frappe.db.get_all("Ledger Merge"):
+ frappe.delete_doc("Ledger Merge", entry.name)
+
+ test_accounts = [
+ "Indirect Test Expenses - _TC",
+ "Administrative Test Expenses - _TC",
+ "Indirect Test Income - _TC",
+ "Administrative Test Income - _TC"
+ ]
+ for account in test_accounts:
+ frappe.delete_doc_if_exists("Account", account)
diff --git a/erpnext/agriculture/__init__.py b/erpnext/accounts/doctype/ledger_merge_accounts/__init__.py
similarity index 100%
copy from erpnext/agriculture/__init__.py
copy to erpnext/accounts/doctype/ledger_merge_accounts/__init__.py
diff --git a/erpnext/accounts/doctype/ledger_merge_accounts/ledger_merge_accounts.json b/erpnext/accounts/doctype/ledger_merge_accounts/ledger_merge_accounts.json
new file mode 100644
index 0000000..4ce55ad
--- /dev/null
+++ b/erpnext/accounts/doctype/ledger_merge_accounts/ledger_merge_accounts.json
@@ -0,0 +1,52 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "creation": "2021-12-09 15:44:58.033398",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "account",
+ "account_name",
+ "merged"
+ ],
+ "fields": [
+ {
+ "columns": 4,
+ "fieldname": "account",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Account",
+ "options": "Account",
+ "reqd": 1
+ },
+ {
+ "columns": 2,
+ "default": "0",
+ "fieldname": "merged",
+ "fieldtype": "Check",
+ "in_list_view": 1,
+ "label": "Merged",
+ "read_only": 1
+ },
+ {
+ "columns": 4,
+ "fieldname": "account_name",
+ "fieldtype": "Data",
+ "label": "Account Name",
+ "read_only": 1,
+ "reqd": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-12-10 15:27:24.477139",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Ledger Merge Accounts",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC"
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/ledger_merge_accounts/ledger_merge_accounts.py b/erpnext/accounts/doctype/ledger_merge_accounts/ledger_merge_accounts.py
new file mode 100644
index 0000000..30dfd65
--- /dev/null
+++ b/erpnext/accounts/doctype/ledger_merge_accounts/ledger_merge_accounts.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2021, Wahni Green Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class LedgerMergeAccounts(Document):
+ pass
diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.json b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.json
index bc92418..daee8f8 100644
--- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.json
+++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.json
@@ -75,7 +75,7 @@
],
"hide_toolbar": 1,
"issingle": 1,
- "modified": "2019-07-25 14:57:33.187689",
+ "modified": "2022-01-04 15:25:06.053187",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Opening Invoice Creation Tool",
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 2a923f0..ddb833f 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
@@ -159,7 +159,8 @@
frappe.scrub(row.party_type): row.party,
"is_pos": 0,
"doctype": "Sales Invoice" if self.invoice_type == "Sales" else "Purchase Invoice",
- "update_stock": 0
+ "update_stock": 0,
+ "invoice_number": row.invoice_number
})
accounting_dimension = get_accounting_dimensions()
@@ -200,10 +201,13 @@
names = []
for idx, d in enumerate(invoices):
try:
+ invoice_number = None
+ if d.invoice_number:
+ invoice_number = d.invoice_number
publish(idx, len(invoices), d.doctype)
doc = frappe.get_doc(d)
doc.flags.ignore_mandatory = True
- doc.insert()
+ doc.insert(set_name=invoice_number)
doc.submit()
frappe.db.commit()
names.append(doc.name)
diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py
index c795e83..b5aae98 100644
--- a/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py
+++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py
@@ -18,10 +18,10 @@
if not frappe.db.exists("Company", "_Test Opening Invoice Company"):
make_company()
- def make_invoices(self, invoice_type="Sales", company=None, party_1=None, party_2=None):
+ def make_invoices(self, invoice_type="Sales", company=None, party_1=None, party_2=None, invoice_number=None):
doc = frappe.get_single("Opening Invoice Creation Tool")
args = get_opening_invoice_creation_dict(invoice_type=invoice_type, company=company,
- party_1=party_1, party_2=party_2)
+ party_1=party_1, party_2=party_2, invoice_number=invoice_number)
doc.update(args)
return doc.make_invoices()
@@ -92,6 +92,20 @@
# teardown
frappe.db.set_value("Company", company, "default_receivable_account", old_default_receivable_account)
+ def test_renaming_of_invoice_using_invoice_number_field(self):
+ company = "_Test Opening Invoice Company"
+ party_1, party_2 = make_customer("Customer A"), make_customer("Customer B")
+ self.make_invoices(company=company, party_1=party_1, party_2=party_2, invoice_number="TEST-NEW-INV-11")
+
+ sales_inv1 = frappe.get_all('Sales Invoice', filters={'customer':'Customer A'})[0].get("name")
+ sales_inv2 = frappe.get_all('Sales Invoice', filters={'customer':'Customer B'})[0].get("name")
+ self.assertEqual(sales_inv1, "TEST-NEW-INV-11")
+
+ #teardown
+ for inv in [sales_inv1, sales_inv2]:
+ doc = frappe.get_doc('Sales Invoice', inv)
+ doc.cancel()
+
def get_opening_invoice_creation_dict(**args):
party = "Customer" if args.get("invoice_type", "Sales") == "Sales" else "Supplier"
company = args.get("company", "_Test Company")
@@ -107,7 +121,8 @@
"item_name": "Opening Item",
"due_date": "2016-09-10",
"posting_date": "2016-09-05",
- "temporary_opening_account": get_temporary_opening_account(company)
+ "temporary_opening_account": get_temporary_opening_account(company),
+ "invoice_number": args.get("invoice_number")
},
{
"qty": 2.0,
@@ -116,7 +131,8 @@
"item_name": "Opening Item",
"due_date": "2016-09-10",
"posting_date": "2016-09-05",
- "temporary_opening_account": get_temporary_opening_account(company)
+ "temporary_opening_account": get_temporary_opening_account(company),
+ "invoice_number": None
}
]
})
@@ -132,7 +148,7 @@
company.company_name = "_Test Opening Invoice Company"
company.abbr = "_TOIC"
company.default_currency = "INR"
- company.country = "India"
+ company.country = "Pakistan"
company.insert()
return company
diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool_item/opening_invoice_creation_tool_item.json b/erpnext/accounts/doctype/opening_invoice_creation_tool_item/opening_invoice_creation_tool_item.json
index 4ce8cb9..5c19091 100644
--- a/erpnext/accounts/doctype/opening_invoice_creation_tool_item/opening_invoice_creation_tool_item.json
+++ b/erpnext/accounts/doctype/opening_invoice_creation_tool_item/opening_invoice_creation_tool_item.json
@@ -1,9 +1,11 @@
{
+ "actions": [],
"creation": "2017-08-29 04:26:36.159247",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
+ "invoice_number",
"party_type",
"party",
"temporary_opening_account",
@@ -103,10 +105,18 @@
{
"fieldname": "dimension_col_break",
"fieldtype": "Column Break"
+ },
+ {
+ "description": "Reference number of the invoice from the previous system",
+ "fieldname": "invoice_number",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Invoice Number"
}
],
"istable": 1,
- "modified": "2019-07-25 15:00:00.460695",
+ "links": [],
+ "modified": "2021-12-17 19:25:06.053187",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Opening Invoice Creation Tool Item",
diff --git a/erpnext/accounts/doctype/party_link/party_link.py b/erpnext/accounts/doctype/party_link/party_link.py
index daf667c..031a9fa 100644
--- a/erpnext/accounts/doctype/party_link/party_link.py
+++ b/erpnext/accounts/doctype/party_link/party_link.py
@@ -2,7 +2,7 @@
# For license information, please see license.txt
import frappe
-from frappe import _
+from frappe import _, bold
from frappe.model.document import Document
@@ -13,6 +13,17 @@
title=_("Invalid Primary Role"))
existing_party_link = frappe.get_all('Party Link', {
+ 'primary_party': self.primary_party,
+ 'secondary_party': self.secondary_party
+ }, pluck="primary_role")
+ if existing_party_link:
+ frappe.throw(_('{} {} is already linked with {} {}')
+ .format(
+ self.primary_role, bold(self.primary_party),
+ self.secondary_role, bold(self.secondary_party)
+ ))
+
+ existing_party_link = frappe.get_all('Party Link', {
'primary_party': self.secondary_party
}, pluck="primary_role")
if existing_party_link:
@@ -25,3 +36,17 @@
if existing_party_link:
frappe.throw(_('{} {} is already linked with another {}')
.format(self.primary_role, self.primary_party, existing_party_link[0]))
+
+
+@frappe.whitelist()
+def create_party_link(primary_role, primary_party, secondary_party):
+ party_link = frappe.new_doc('Party Link')
+ party_link.primary_role = primary_role
+ party_link.primary_party = primary_party
+ party_link.secondary_role = 'Customer' if primary_role == 'Supplier' else 'Supplier'
+ party_link.secondary_party = secondary_party
+
+ party_link.save(ignore_permissions=True)
+
+ return party_link
+
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.json b/erpnext/accounts/doctype/payment_entry/payment_entry.json
index ee2e319..c8d1db9 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.json
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.json
@@ -61,7 +61,6 @@
"taxes_and_charges_section",
"purchase_taxes_and_charges_template",
"sales_taxes_and_charges_template",
- "advance_tax_account",
"column_break_55",
"apply_tax_withholding_amount",
"tax_withholding_category",
@@ -686,15 +685,6 @@
"hide_border": 1
},
{
- "depends_on": "eval:doc.apply_tax_withholding_amount",
- "description": "Provisional tax account for advance tax. Taxes are parked in this account until payments are allocated to invoices",
- "fieldname": "advance_tax_account",
- "fieldtype": "Link",
- "label": "Advance Tax Account",
- "mandatory_depends_on": "eval:doc.apply_tax_withholding_amount",
- "options": "Account"
- },
- {
"depends_on": "eval:doc.received_amount && doc.payment_type != 'Internal Transfer'",
"fieldname": "received_amount_after_tax",
"fieldtype": "Currency",
@@ -730,7 +720,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2021-10-22 17:50:24.632806",
+ "modified": "2021-11-24 18:58:24.919764",
"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 26fd16a..0e07abd 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -20,7 +20,7 @@
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import (
get_party_tax_withholding_details,
)
-from erpnext.accounts.general_ledger import make_gl_entries
+from erpnext.accounts.general_ledger import make_gl_entries, process_gl_map
from erpnext.accounts.party import get_party_account
from erpnext.accounts.utils import get_account_currency, get_balance_on, get_outstanding_invoices
from erpnext.controllers.accounts_controller import (
@@ -339,7 +339,7 @@
for k, v in no_oustanding_refs.items():
frappe.msgprint(
_("{} - {} now have {} as they had no outstanding amount left before submitting the Payment Entry.")
- .format(k, frappe.bold(", ".join(d.reference_name for d in v)), frappe.bold("negative outstanding amount"))
+ .format(_(k), frappe.bold(", ".join(d.reference_name for d in v)), frappe.bold(_("negative outstanding amount")))
+ "<br><br>" + _("If this is undesirable please cancel the corresponding Payment Entry."),
title=_("Warning"), indicator="orange")
@@ -433,23 +433,12 @@
if not self.apply_tax_withholding_amount:
return
- if not self.advance_tax_account:
- frappe.throw(_("Advance TDS account is mandatory for advance TDS deduction"))
-
net_total = self.paid_amount
- 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({
'company': self.company,
- 'doctype': 'Purchase Invoice',
+ 'doctype': 'Payment Entry',
'supplier': self.party,
'posting_date': self.posting_date,
'net_total': net_total
@@ -461,7 +450,6 @@
return
tax_withholding_details.update({
- 'add_deduct_tax': 'Add',
'cost_center': self.cost_center or erpnext.get_default_cost_center(self.company)
})
@@ -623,7 +611,7 @@
if not total_negative_outstanding:
frappe.throw(_("Cannot {0} {1} {2} without any negative outstanding invoice")
- .format(self.payment_type, ("to" if self.party_type=="Customer" else "from"),
+ .format(_(self.payment_type), (_("to") if self.party_type=="Customer" else _("from")),
self.party_type), InvalidPaymentEntry)
elif paid_amount - additional_charges > total_negative_outstanding:
@@ -689,6 +677,7 @@
self.add_deductions_gl_entries(gl_entries)
self.add_tax_gl_entries(gl_entries)
+ gl_entries = process_gl_map(gl_entries)
make_gl_entries(gl_entries, cancel=cancel, adv_adj=adv_adj)
def add_party_gl_entries(self, gl_entries):
@@ -752,7 +741,8 @@
"against": self.party if self.payment_type=="Pay" else self.paid_to,
"credit_in_account_currency": self.paid_amount,
"credit": self.base_paid_amount,
- "cost_center": self.cost_center
+ "cost_center": self.cost_center,
+ "post_net_value": True
}, item=self)
)
if self.payment_type in ("Receive", "Internal Transfer"):
@@ -782,14 +772,10 @@
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()
+ payment_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,
@@ -798,19 +784,21 @@
dr_or_cr + "_in_account_currency": base_tax_amount
if account_currency==self.company_currency
else d.tax_amount,
- "cost_center": d.cost_center
+ "cost_center": d.cost_center,
+ "post_net_value": True,
}, account_currency, item=d))
- if not d.included_in_paid_amount or self.advance_tax_account:
+ if not d.included_in_paid_amount:
gl_entries.append(
self.get_gl_dict({
- "account": payment_or_advance_account,
+ "account": payment_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,
+ "post_net_value": True,
}, account_currency, item=d))
def add_deductions_gl_entries(self, gl_entries):
@@ -832,9 +820,7 @@
)
def get_party_account_for_taxes(self):
- if self.advance_tax_account:
- return self.advance_tax_account
- elif self.payment_type == 'Receive':
+ if self.payment_type == 'Receive':
return self.paid_to
elif self.payment_type in ('Pay', 'Internal Transfer'):
return self.paid_from
@@ -1106,7 +1092,7 @@
if not data:
frappe.msgprint(_("No outstanding invoices found for the {0} {1} which qualify the filters you have specified.")
- .format(args.get("party_type").lower(), frappe.bold(args.get("party"))))
+ .format(_(args.get("party_type")).lower(), frappe.bold(args.get("party"))))
return data
@@ -1599,13 +1585,6 @@
})
pe.set_difference_amount()
- if doc.doctype == 'Purchase Order' and doc.apply_tds:
- pe.apply_tax_withholding_amount = 1
- pe.tax_withholding_category = doc.tax_withholding_category
-
- if not pe.advance_tax_account:
- pe.advance_tax_account = frappe.db.get_value('Company', pe.company, 'unrealized_profit_loss_account')
-
return pe
def get_bank_cash_account(doc, bank_account):
@@ -1729,7 +1708,10 @@
def apply_early_payment_discount(paid_amount, received_amount, doc):
total_discount = 0
- if doc.doctype in ['Sales Invoice', 'Purchase Invoice'] and doc.payment_schedule:
+ eligible_for_payments = ['Sales Order', 'Sales Invoice', 'Purchase Order', 'Purchase Invoice']
+ has_payment_schedule = hasattr(doc, 'payment_schedule') and doc.payment_schedule
+
+ if doc.doctype in eligible_for_payments and has_payment_schedule:
for term in doc.payment_schedule:
if not term.discounted_amount and term.discount and getdate(nowdate()) <= term.discount_date:
if term.discount_type == 'Percentage':
diff --git a/erpnext/accounts/doctype/payment_entry/tests/test_payment_against_invoice.js b/erpnext/accounts/doctype/payment_entry/tests/test_payment_against_invoice.js
deleted file mode 100644
index 4f27b74..0000000
--- a/erpnext/accounts/doctype/payment_entry/tests/test_payment_against_invoice.js
+++ /dev/null
@@ -1,55 +0,0 @@
-QUnit.module('Payment Entry');
-
-QUnit.test("test payment entry", function(assert) {
- assert.expect(6);
- let done = assert.async();
- frappe.run_serially([
- () => {
- return frappe.tests.make('Sales Invoice', [
- {customer: 'Test Customer 1'},
- {items: [
- [
- {'item_code': 'Test Product 1'},
- {'qty': 1},
- {'rate': 101},
- ]
- ]}
- ]);
- },
- () => cur_frm.save(),
- () => frappe.tests.click_button('Submit'),
- () => frappe.tests.click_button('Yes'),
- () => frappe.timeout(1),
- () => frappe.tests.click_button('Close'),
- () => frappe.timeout(1),
- () => frappe.click_button('Make'),
- () => frappe.timeout(1),
- () => frappe.click_link('Payment'),
- () => frappe.timeout(2),
- () => {
- assert.equal(frappe.get_route()[1], 'Payment Entry',
- 'made payment entry');
- assert.equal(cur_frm.doc.party, 'Test Customer 1',
- 'customer set in payment entry');
- assert.equal(cur_frm.doc.paid_amount, 101,
- 'paid amount set in payment entry');
- assert.equal(cur_frm.doc.references[0].allocated_amount, 101,
- 'amount allocated against sales invoice');
- },
- () => frappe.timeout(1),
- () => cur_frm.set_value('paid_amount', 100),
- () => frappe.timeout(1),
- () => {
- frappe.model.set_value("Payment Entry Reference", cur_frm.doc.references[0].name,
- "allocated_amount", 101);
- },
- () => frappe.timeout(1),
- () => frappe.click_button('Write Off Difference Amount'),
- () => frappe.timeout(1),
- () => {
- assert.equal(cur_frm.doc.difference_amount, 0, 'difference amount is zero');
- assert.equal(cur_frm.doc.deductions[0].amount, 1, 'Write off amount = 1');
- },
- () => done()
- ]);
-});
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
deleted file mode 100644
index e8db2c3..0000000
--- a/erpnext/accounts/doctype/payment_entry/tests/test_payment_against_purchase_invoice.js
+++ /dev/null
@@ -1,60 +0,0 @@
-QUnit.module('Payment Entry');
-
-QUnit.test("test payment entry", function(assert) {
- assert.expect(7 );
- let done = assert.async();
-
- frappe.run_serially([
- () => {
- return frappe.tests.make('Purchase Invoice', [
- {supplier: 'Test Supplier'},
- {bill_no: 'in1234'},
- {items: [
- [
- {'qty': 2},
- {'item_code': 'Test Product 1'},
- {'rate':1000},
- ]
- ]},
- {update_stock:1},
- {supplier_address: 'Test1-Billing'},
- {contact_person: 'Contact 3-Test Supplier'},
- {tc_name: 'Test Term 1'},
- {terms: 'This is just a Test'}
- ]);
- },
-
- () => cur_frm.save(),
- () => frappe.tests.click_button('Submit'),
- () => frappe.tests.click_button('Yes'),
- () => frappe.timeout(1),
- () => frappe.click_button('Make'),
- () => frappe.timeout(2),
- () => frappe.click_link('Payment'),
- () => frappe.timeout(3),
- () => cur_frm.set_value('mode_of_payment','Cash'),
- () => frappe.timeout(3),
- () => {
- assert.equal(frappe.get_route()[1], 'Payment Entry',
- 'made payment entry');
- assert.equal(cur_frm.doc.party, 'Test Supplier',
- 'supplier set in payment entry');
- assert.equal(cur_frm.doc.paid_amount, 2000,
- 'paid amount set in payment entry');
- assert.equal(cur_frm.doc.references[0].allocated_amount, 2000,
- 'amount allocated against purchase invoice');
- assert.equal(cur_frm.doc.references[0].bill_no, 'in1234',
- 'invoice number allocated against purchase invoice');
- assert.equal(cur_frm.get_field('total_allocated_amount').value, 2000,
- 'correct amount allocated in Write Off');
- assert.equal(cur_frm.get_field('unallocated_amount').value, 0,
- 'correct amount unallocated in Write Off');
- },
-
- () => cur_frm.save(),
- () => frappe.tests.click_button('Submit'),
- () => frappe.tests.click_button('Yes'),
- () => frappe.timeout(3),
- () => done()
- ]);
-});
diff --git a/erpnext/accounts/doctype/payment_entry/tests/test_payment_entry.js b/erpnext/accounts/doctype/payment_entry/tests/test_payment_entry.js
deleted file mode 100644
index 34af79f..0000000
--- a/erpnext/accounts/doctype/payment_entry/tests/test_payment_entry.js
+++ /dev/null
@@ -1,28 +0,0 @@
-QUnit.module('Accounts');
-
-QUnit.test("test payment entry", function(assert) {
- assert.expect(1);
- let done = assert.async();
- frappe.run_serially([
- () => {
- return frappe.tests.make('Payment Entry', [
- {payment_type:'Receive'},
- {mode_of_payment:'Cash'},
- {party_type:'Customer'},
- {party:'Test Customer 3'},
- {paid_amount:675},
- {reference_no:123},
- {reference_date: frappe.datetime.add_days(frappe.datetime.nowdate(), 0)},
- ]);
- },
- () => cur_frm.save(),
- () => {
- // get_item_details
- assert.ok(cur_frm.doc.total_allocated_amount==675, "Allocated AmountCorrect");
- },
- () => frappe.tests.click_button('Submit'),
- () => frappe.tests.click_button('Yes'),
- () => frappe.timeout(0.3),
- () => done()
- ]);
-});
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
deleted file mode 100644
index 8c7f6f4..0000000
--- a/erpnext/accounts/doctype/payment_entry/tests/test_payment_entry_write_off.js
+++ /dev/null
@@ -1,67 +0,0 @@
-QUnit.module('Payment Entry');
-
-QUnit.test("test payment entry", function(assert) {
- assert.expect(8);
- let done = assert.async();
- frappe.run_serially([
- () => {
- return frappe.tests.make('Sales Invoice', [
- {customer: 'Test Customer 1'},
- {company: 'For Testing'},
- {currency: 'INR'},
- {selling_price_list: '_Test Price List'},
- {items: [
- [
- {'qty': 1},
- {'item_code': 'Test Product 1'},
- ]
- ]}
- ]);
- },
- () => frappe.timeout(1),
- () => cur_frm.save(),
- () => frappe.tests.click_button('Submit'),
- () => frappe.tests.click_button('Yes'),
- () => frappe.timeout(1.5),
- () => frappe.click_button('Close'),
- () => frappe.timeout(0.5),
- () => frappe.click_button('Make'),
- () => frappe.timeout(1),
- () => frappe.click_link('Payment'),
- () => frappe.timeout(2),
- () => cur_frm.set_value("paid_to", "_Test Cash - FT"),
- () => frappe.timeout(0.5),
- () => {
- assert.equal(frappe.get_route()[1], 'Payment Entry', 'made payment entry');
- assert.equal(cur_frm.doc.party, 'Test Customer 1', 'customer set in payment entry');
- assert.equal(cur_frm.doc.paid_from, 'Debtors - FT', 'customer account set in payment entry');
- assert.equal(cur_frm.doc.paid_amount, 100, 'paid amount set in payment entry');
- assert.equal(cur_frm.doc.references[0].allocated_amount, 100,
- 'amount allocated against sales invoice');
- },
- () => cur_frm.set_value('paid_amount', 95),
- () => frappe.timeout(1),
- () => {
- frappe.model.set_value("Payment Entry Reference",
- cur_frm.doc.references[0].name, "allocated_amount", 100);
- },
- () => frappe.timeout(.5),
- () => {
- assert.equal(cur_frm.doc.difference_amount, 5, 'difference amount is 5');
- },
- () => {
- frappe.db.set_value("Company", "For Testing", "write_off_account", "_Test Write Off - FT");
- frappe.timeout(1);
- frappe.db.set_value("Company", "For Testing",
- "exchange_gain_loss_account", "_Test Exchange Gain/Loss - FT");
- },
- () => frappe.timeout(1),
- () => frappe.click_button('Write Off Difference Amount'),
- () => frappe.timeout(2),
- () => {
- assert.equal(cur_frm.doc.difference_amount, 0, 'difference amount is zero');
- assert.equal(cur_frm.doc.deductions[0].amount, 5, 'Write off amount = 5');
- },
- () => done()
- ]);
-});
diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py
index 649c566..1a833a4 100644
--- a/erpnext/accounts/doctype/payment_request/payment_request.py
+++ b/erpnext/accounts/doctype/payment_request/payment_request.py
@@ -548,10 +548,14 @@
return doclist
-def validate_payment(doc, method=""):
- if not frappe.db.has_column(doc.reference_doctype, 'status'):
+def validate_payment(doc, method=None):
+ if doc.reference_doctype != "Payment Request" or (
+ frappe.db.get_value(doc.reference_doctype, doc.reference_docname, 'status')
+ != "Paid"
+ ):
return
- status = frappe.db.get_value(doc.reference_doctype, doc.reference_docname, 'status')
- if status == 'Paid':
- frappe.throw(_("The Payment Request {0} is already paid, cannot process payment twice").format(doc.reference_docname))
\ No newline at end of file
+ frappe.throw(
+ _("The Payment Request {0} is already paid, cannot process payment twice")
+ .format(doc.reference_docname)
+ )
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 34572fd..d0e555e 100644
--- a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py
+++ b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py
@@ -88,9 +88,10 @@
for acc in pl_accounts:
if flt(acc.bal_in_company_currency):
+ cost_center = acc.cost_center if self.cost_center_wise_pnl else company_cost_center
gl_entry = self.get_gl_dict({
"account": self.closing_account_head,
- "cost_center": acc.cost_center or company_cost_center,
+ "cost_center": 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,
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 0e29755..030b4ca 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
@@ -66,8 +66,8 @@
company = create_company()
surplus_account = create_account()
- cost_center1 = create_cost_center("Test Cost Center 1")
- cost_center2 = create_cost_center("Test Cost Center 2")
+ cost_center1 = create_cost_center("Main")
+ cost_center2 = create_cost_center("Western Branch")
create_sales_invoice(
company=company,
@@ -86,7 +86,10 @@
debit_to="Debtors - TPC"
)
- pcv = self.make_period_closing_voucher()
+ pcv = self.make_period_closing_voucher(submit=False)
+ pcv.cost_center_wise_pnl = 1
+ pcv.save()
+ pcv.submit()
surplus_account = pcv.closing_account_head
expected_gle = (
@@ -149,7 +152,7 @@
self.assertEqual(pcv_gle, expected_gle)
- def make_period_closing_voucher(self):
+ def make_period_closing_voucher(self, submit=True):
surplus_account = create_account()
cost_center = create_cost_center("Test Cost Center 1")
pcv = frappe.get_doc({
@@ -163,7 +166,8 @@
"remarks": "test"
})
pcv.insert()
- pcv.submit()
+ if submit:
+ pcv.submit()
return pcv
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json
index bff8587..0c6e7ed 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json
@@ -171,6 +171,7 @@
"sales_team_section_break",
"sales_partner",
"column_break10",
+ "amount_eligible_for_commission",
"commission_rate",
"total_commission",
"section_break2",
@@ -1561,16 +1562,23 @@
"label": "Coupon Code",
"options": "Coupon Code",
"print_hide": 1
+ },
+ {
+ "fieldname": "amount_eligible_for_commission",
+ "fieldtype": "Currency",
+ "label": "Amount Eligible for Commission",
+ "read_only": 1
}
],
"icon": "fa fa-file-text",
"is_submittable": 1,
"links": [],
- "modified": "2021-08-27 20:12:57.306772",
+ "modified": "2021-10-05 12:11:53.871828",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice",
"name_case": "Title Case",
+ "naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
index 0d6404c..134bccf 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
@@ -15,6 +15,7 @@
update_multi_mode_option,
)
from erpnext.accounts.party import get_due_date, get_party_account
+from erpnext.stock.doctype.batch.batch import get_batch_qty, get_pos_reserved_batch_qty
from erpnext.stock.doctype.serial_no.serial_no import get_pos_reserved_serial_nos, get_serial_nos
@@ -124,9 +125,26 @@
frappe.throw(_("Row #{}: Serial No. {} has already been transacted into another POS Invoice. Please select valid serial no.")
.format(item.idx, bold_invalid_serial_nos), title=_("Item Unavailable"))
elif invalid_serial_nos:
- frappe.throw(_("Row #{}: Serial Nos. {} has already been transacted into another POS Invoice. Please select valid serial no.")
+ frappe.throw(_("Row #{}: Serial Nos. {} have already been transacted into another POS Invoice. Please select valid serial no.")
.format(item.idx, bold_invalid_serial_nos), title=_("Item Unavailable"))
+ def validate_pos_reserved_batch_qty(self, item):
+ filters = {"item_code": item.item_code, "warehouse": item.warehouse, "batch_no":item.batch_no}
+
+ available_batch_qty = get_batch_qty(item.batch_no, item.warehouse, item.item_code)
+ reserved_batch_qty = get_pos_reserved_batch_qty(filters)
+
+ bold_item_name = frappe.bold(item.item_name)
+ bold_extra_batch_qty_needed = frappe.bold(abs(available_batch_qty - reserved_batch_qty - item.qty))
+ bold_invalid_batch_no = frappe.bold(item.batch_no)
+
+ if (available_batch_qty - reserved_batch_qty) == 0:
+ frappe.throw(_("Row #{}: Batch No. {} of item {} has no stock available. Please select valid batch no.")
+ .format(item.idx, bold_invalid_batch_no, bold_item_name), title=_("Item Unavailable"))
+ elif (available_batch_qty - reserved_batch_qty - item.qty) < 0:
+ frappe.throw(_("Row #{}: Batch No. {} of item {} has less than required stock available, {} more required")
+ .format(item.idx, bold_invalid_batch_no, bold_item_name, bold_extra_batch_qty_needed), title=_("Item Unavailable"))
+
def validate_delivered_serial_nos(self, item):
serial_nos = get_serial_nos(item.serial_no)
delivered_serial_nos = frappe.db.get_list('Serial No', {
@@ -149,6 +167,8 @@
if d.serial_no:
self.validate_pos_reserved_serial_nos(d)
self.validate_delivered_serial_nos(d)
+ elif d.batch_no:
+ self.validate_pos_reserved_batch_qty(d)
else:
if allow_negative_stock:
return
@@ -333,7 +353,6 @@
if not for_validate and not self.customer:
self.customer = profile.customer
- self.ignore_pricing_rule = profile.ignore_pricing_rule
self.account_for_change_amount = profile.get('account_for_change_amount') or self.account_for_change_amount
self.set_warehouse = profile.get('warehouse') or self.set_warehouse
diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
index 6696333..56479a0 100644
--- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
@@ -521,6 +521,72 @@
rounded_total = frappe.db.get_value("Sales Invoice", pos_inv2.consolidated_invoice, "rounded_total")
self.assertEqual(rounded_total, 400)
+ def test_pos_batch_item_qty_validation(self):
+ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
+ create_batch_item_with_batch,
+ )
+ create_batch_item_with_batch('_BATCH ITEM', 'TestBatch 01')
+ item = frappe.get_doc('Item', '_BATCH ITEM')
+ batch = frappe.get_doc('Batch', 'TestBatch 01')
+ batch.submit()
+ item.batch_no = 'TestBatch 01'
+ item.save()
+
+ se = make_stock_entry(target="_Test Warehouse - _TC", item_code="_BATCH ITEM", qty=2, basic_rate=100, batch_no='TestBatch 01')
+
+ pos_inv1 = create_pos_invoice(item=item.name, rate=300, qty=1, do_not_submit=1)
+ pos_inv1.items[0].batch_no = 'TestBatch 01'
+ pos_inv1.save()
+ pos_inv1.submit()
+
+ pos_inv2 = create_pos_invoice(item=item.name, rate=300, qty=2, do_not_submit=1)
+ pos_inv2.items[0].batch_no = 'TestBatch 01'
+ pos_inv2.save()
+
+ self.assertRaises(frappe.ValidationError, pos_inv2.submit)
+
+ #teardown
+ pos_inv1.reload()
+ pos_inv1.cancel()
+ pos_inv1.delete()
+ pos_inv2.reload()
+ pos_inv2.delete()
+ se.cancel()
+ batch.reload()
+ batch.cancel()
+ batch.delete()
+
+ def test_ignore_pricing_rule(self):
+ from erpnext.accounts.doctype.pricing_rule.test_pricing_rule import make_pricing_rule
+
+ item_price = frappe.get_doc({
+ 'doctype': 'Item Price',
+ 'item_code': '_Test Item',
+ 'price_list': '_Test Price List',
+ 'price_list_rate': '450',
+ })
+ item_price.insert()
+ pr = make_pricing_rule(selling=1, priority=5, discount_percentage=10)
+ pr.save()
+ pos_inv = create_pos_invoice(qty=1, do_not_submit=1)
+ pos_inv.items[0].rate = 300
+ pos_inv.save()
+ self.assertEquals(pos_inv.items[0].discount_percentage, 10)
+ # rate shouldn't change
+ self.assertEquals(pos_inv.items[0].rate, 405)
+
+ pos_inv.ignore_pricing_rule = 1
+ pos_inv.items[0].rate = 300
+ pos_inv.save()
+ self.assertEquals(pos_inv.ignore_pricing_rule, 1)
+ # rate should change since pricing rules are ignored
+ self.assertEquals(pos_inv.items[0].rate, 300)
+
+ item_price.delete()
+ pos_inv.delete()
+ pr.delete()
+
+
def create_pos_invoice(**args):
args = frappe._dict(args)
pos_profile = None
@@ -557,7 +623,8 @@
"income_account": args.income_account or "Sales - _TC",
"expense_account": args.expense_account or "Cost of Goods Sold - _TC",
"cost_center": args.cost_center or "_Test Cost Center - _TC",
- "serial_no": args.serial_no
+ "serial_no": args.serial_no,
+ "batch_no": args.batch_no
})
if not args.do_not_save:
@@ -570,3 +637,8 @@
pos_inv.payment_schedule = []
return pos_inv
+
+def make_batch_item(item_name):
+ from erpnext.stock.doctype.item.test_item import make_item
+ if not frappe.db.exists(item_name):
+ return make_item(item_name, dict(has_batch_no = 1, create_new_batch = 1, is_stock_item=1))
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json
index 8b71eb0..3f85668 100644
--- a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json
+++ b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json
@@ -46,6 +46,7 @@
"base_amount",
"pricing_rules",
"is_free_item",
+ "grant_commission",
"section_break_21",
"net_rate",
"net_amount",
@@ -800,14 +801,22 @@
"no_copy": 1,
"print_hide": 1,
"read_only": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "grant_commission",
+ "fieldtype": "Check",
+ "label": "Grant Commission",
+ "read_only": 1
}
],
"istable": 1,
"links": [],
- "modified": "2021-01-04 17:34:49.924531",
+ "modified": "2021-10-05 12:23:47.506290",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice Item",
+ "naming_rule": "Random",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.js b/erpnext/accounts/doctype/pos_profile/pos_profile.js
index efdeb1a..813d20d 100755
--- a/erpnext/accounts/doctype/pos_profile/pos_profile.js
+++ b/erpnext/accounts/doctype/pos_profile/pos_profile.js
@@ -3,22 +3,20 @@
{% include "erpnext/public/js/controllers/accounts.js" %}
-frappe.ui.form.on("POS Profile", "onload", function(frm) {
- frm.set_query("selling_price_list", function() {
- return { filters: { selling: 1 } };
- });
-
- frm.set_query("tc_name", function() {
- return { filters: { selling: 1 } };
- });
-
- erpnext.queries.setup_queries(frm, "Warehouse", function() {
- return erpnext.queries.warehouse(frm.doc);
- });
-});
-
frappe.ui.form.on('POS Profile', {
setup: function(frm) {
+ frm.set_query("selling_price_list", function() {
+ return { filters: { selling: 1 } };
+ });
+
+ frm.set_query("tc_name", function() {
+ return { filters: { selling: 1 } };
+ });
+
+ erpnext.queries.setup_queries(frm, "Warehouse", function() {
+ return erpnext.queries.warehouse(frm.doc);
+ });
+
frm.set_query("print_format", function() {
return {
filters: [
@@ -27,10 +25,16 @@
};
});
- frm.set_query("account_for_change_amount", function() {
+ frm.set_query("account_for_change_amount", function(doc) {
+ if (!doc.company) {
+ frappe.throw(__('Please set Company'));
+ }
+
return {
filters: {
- account_type: ['in', ["Cash", "Bank"]]
+ account_type: ['in', ["Cash", "Bank"]],
+ is_group: 0,
+ company: doc.company
}
};
});
@@ -45,7 +49,7 @@
});
frm.set_query('company_address', function(doc) {
- if(!doc.company) {
+ if (!doc.company) {
frappe.throw(__('Please set Company'));
}
@@ -58,11 +62,79 @@
};
});
+ frm.set_query('income_account', function(doc) {
+ if (!doc.company) {
+ frappe.throw(__('Please set Company'));
+ }
+
+ return {
+ filters: {
+ 'is_group': 0,
+ 'company': doc.company,
+ 'account_type': "Income Account"
+ }
+ };
+ });
+
+ frm.set_query('cost_center', function(doc) {
+ if (!doc.company) {
+ frappe.throw(__('Please set Company'));
+ }
+
+ return {
+ filters: {
+ 'company': doc.company,
+ 'is_group': 0
+ }
+ };
+ });
+
+ frm.set_query('expense_account', function(doc) {
+ if (!doc.company) {
+ frappe.throw(__('Please set Company'));
+ }
+
+ return {
+ filters: {
+ "report_type": "Profit and Loss",
+ "company": doc.company,
+ "is_group": 0
+ }
+ };
+ });
+
+ frm.set_query("select_print_heading", function() {
+ return {
+ filters: [
+ ['Print Heading', 'docstatus', '!=', 2]
+ ]
+ };
+ });
+
+ frm.set_query("write_off_account", function(doc) {
+ return {
+ filters: {
+ 'report_type': 'Profit and Loss',
+ 'is_group': 0,
+ 'company': doc.company
+ }
+ };
+ });
+
+ frm.set_query("write_off_cost_center", function(doc) {
+ return {
+ filters: {
+ 'is_group': 0,
+ 'company': doc.company
+ }
+ };
+ });
+
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
},
refresh: function(frm) {
- if(frm.doc.company) {
+ if (frm.doc.company) {
frm.trigger("toggle_display_account_head");
}
},
@@ -76,71 +148,4 @@
frm.toggle_display('expense_account',
erpnext.is_perpetual_inventory_enabled(frm.doc.company));
}
-})
-
-// Income Account
-// --------------------------------
-cur_frm.fields_dict['income_account'].get_query = function(doc,cdt,cdn) {
- return{
- filters:{
- 'is_group': 0,
- 'company': doc.company,
- 'account_type': "Income Account"
- }
- };
-};
-
-
-// Cost Center
-// -----------------------------
-cur_frm.fields_dict['cost_center'].get_query = function(doc,cdt,cdn) {
- return{
- filters:{
- 'company': doc.company,
- 'is_group': 0
- }
- };
-};
-
-
-// Expense Account
-// -----------------------------
-cur_frm.fields_dict["expense_account"].get_query = function(doc) {
- return {
- filters: {
- "report_type": "Profit and Loss",
- "company": doc.company,
- "is_group": 0
- }
- };
-};
-
-// ------------------ Get Print Heading ------------------------------------
-cur_frm.fields_dict['select_print_heading'].get_query = function(doc, cdt, cdn) {
- return{
- filters:[
- ['Print Heading', 'docstatus', '!=', 2]
- ]
- };
-};
-
-cur_frm.fields_dict.write_off_account.get_query = function(doc) {
- return{
- filters:{
- 'report_type': 'Profit and Loss',
- 'is_group': 0,
- 'company': doc.company
- }
- };
-};
-
-// Write off cost center
-// -----------------------
-cur_frm.fields_dict.write_off_cost_center.get_query = function(doc) {
- return{
- filters:{
- 'is_group': 0,
- 'company': doc.company
- }
- };
-};
+});
diff --git a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py
index e05e4a1..5746a84 100644
--- a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py
+++ b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py
@@ -166,7 +166,7 @@
"item_group": "Products",
},
{
- "item_group": "Seed",
+ "item_group": "_Test Item Group",
},
],
"selling": 1,
@@ -543,6 +543,75 @@
frappe.get_doc("Item Price", {"item_code": "Water Flask"}).delete()
item.delete()
+ def test_pricing_rule_for_different_currency(self):
+ make_item("Test Sanitizer Item")
+
+ pricing_rule_record = {
+ "doctype": "Pricing Rule",
+ "title": "_Test Sanitizer Rule",
+ "apply_on": "Item Code",
+ "items": [{
+ "item_code": "Test Sanitizer Item",
+ }],
+ "selling": 1,
+ "currency": "INR",
+ "rate_or_discount": "Rate",
+ "rate": 0,
+ "priority": 2,
+ "margin_type": "Percentage",
+ "margin_rate_or_amount": 0.0,
+ "company": "_Test Company"
+ }
+
+ rule = frappe.get_doc(pricing_rule_record)
+ rule.rate_or_discount = 'Rate'
+ rule.rate = 100.0
+ rule.insert()
+
+ rule1 = frappe.get_doc(pricing_rule_record)
+ rule1.currency = 'USD'
+ rule1.rate_or_discount = 'Rate'
+ rule1.rate = 2.0
+ rule1.priority = 1
+ rule1.insert()
+
+ args = frappe._dict({
+ "item_code": "Test Sanitizer Item",
+ "company": "_Test Company",
+ "price_list": "_Test Price List",
+ "currency": "USD",
+ "doctype": "Sales Invoice",
+ "conversion_rate": 1,
+ "price_list_currency": "_Test Currency",
+ "plc_conversion_rate": 1,
+ "order_type": "Sales",
+ "customer": "_Test Customer",
+ "name": None,
+ "transaction_date": frappe.utils.nowdate()
+ })
+
+ details = get_item_details(args)
+ self.assertEqual(details.price_list_rate, 2.0)
+
+
+ args = frappe._dict({
+ "item_code": "Test Sanitizer Item",
+ "company": "_Test Company",
+ "price_list": "_Test Price List",
+ "currency": "INR",
+ "doctype": "Sales Invoice",
+ "conversion_rate": 1,
+ "price_list_currency": "_Test Currency",
+ "plc_conversion_rate": 1,
+ "order_type": "Sales",
+ "customer": "_Test Customer",
+ "name": None,
+ "transaction_date": frappe.utils.nowdate()
+ })
+
+ details = get_item_details(args)
+ self.assertEqual(details.price_list_rate, 100.0)
+
def test_pricing_rule_for_transaction(self):
make_item("Water Flask 1")
frappe.delete_doc_if_exists('Pricing Rule', '_Test Pricing Rule')
@@ -581,7 +650,7 @@
"rate": args.rate or 0.0,
"margin_rate_or_amount": args.margin_rate_or_amount or 0.0,
"condition": args.condition or '',
- "priority": 1,
+ "priority": args.priority or 1,
"discount_amount": args.discount_amount or 0.0,
"apply_multiple_pricing_rules": args.apply_multiple_pricing_rules or 0
})
@@ -607,6 +676,8 @@
if args.get(applicable_for):
doc.db_set(applicable_for, args.get(applicable_for))
+ return doc
+
def setup_pricing_rule_data():
if not frappe.db.exists('Campaign', '_Test Campaign'):
frappe.get_doc({
diff --git a/erpnext/accounts/doctype/pricing_rule/tests/test_pricing_rule.js b/erpnext/accounts/doctype/pricing_rule/tests/test_pricing_rule.js
deleted file mode 100644
index 8279b59..0000000
--- a/erpnext/accounts/doctype/pricing_rule/tests/test_pricing_rule.js
+++ /dev/null
@@ -1,28 +0,0 @@
-QUnit.module('Pricing Rule');
-
-QUnit.test("test pricing rule", function(assert) {
- assert.expect(2);
- let done = assert.async();
- frappe.run_serially([
- () => {
- return frappe.tests.make("Pricing Rule", [
- {title: 'Test Pricing Rule'},
- {item_code:'Test Product 2'},
- {selling:1},
- {applicable_for:'Customer'},
- {customer:'Test Customer 3'},
- {currency: frappe.defaults.get_default("currency")}
- {min_qty:1},
- {max_qty:20},
- {valid_upto: frappe.datetime.add_days(frappe.defaults.get_default("year_end_date"), 1)},
- {discount_percentage:10},
- {for_price_list:'Standard Selling'}
- ]);
- },
- () => {
- assert.ok(cur_frm.doc.item_code=='Test Product 2');
- assert.ok(cur_frm.doc.customer=='Test Customer 3');
- },
- () => done()
- ]);
-});
diff --git a/erpnext/accounts/doctype/pricing_rule/tests/test_pricing_rule_with_different_currency.js b/erpnext/accounts/doctype/pricing_rule/tests/test_pricing_rule_with_different_currency.js
deleted file mode 100644
index 4a29956..0000000
--- a/erpnext/accounts/doctype/pricing_rule/tests/test_pricing_rule_with_different_currency.js
+++ /dev/null
@@ -1,58 +0,0 @@
-QUnit.module('Pricing Rule');
-
-QUnit.test("test pricing rule with different currency", function(assert) {
- assert.expect(3);
- let done = assert.async();
- frappe.run_serially([
- () => {
- return frappe.tests.make("Pricing Rule", [
- {title: 'Test Pricing Rule 2'},
- {apply_on: 'Item Code'},
- {item_code:'Test Product 4'},
- {selling:1},
- {priority: 1},
- {min_qty:1},
- {max_qty:20},
- {valid_upto: frappe.datetime.add_days(frappe.defaults.get_default("year_end_date"), 1)},
- {margin_type: 'Amount'},
- {margin_rate_or_amount: 20},
- {rate_or_discount: 'Rate'},
- {rate:200},
- {currency:'USD'}
-
- ]);
- },
- () => cur_frm.save(),
- () => frappe.timeout(0.3),
- () => {
- assert.ok(cur_frm.doc.item_code=='Test Product 4');
- },
-
- () => {
- return frappe.tests.make('Sales Order', [
- {customer: 'Test Customer 1'},
- {currency: 'INR'},
- {items: [
- [
- {'delivery_date': frappe.datetime.add_days(frappe.defaults.get_default("year_end_date"), 1)},
- {'qty': 5},
- {'item_code': "Test Product 4"}
- ]
- ]}
- ]);
- },
- () => cur_frm.save(),
- () => frappe.timeout(0.3),
- () => {
- // get_item_details
- assert.ok(cur_frm.doc.items[0].pricing_rule=='Test Pricing Rule 2', "Pricing rule correct");
- // margin not applied because different currency in pricing rule
- assert.ok(cur_frm.doc.items[0].margin_type==null, "Margin correct");
- },
- () => frappe.timeout(0.3),
- () => frappe.tests.click_button('Submit'),
- () => frappe.tests.click_button('Yes'),
- () => frappe.timeout(0.3),
- () => done()
- ]);
-});
diff --git a/erpnext/accounts/doctype/pricing_rule/tests/test_pricing_rule_with_same_currency.js b/erpnext/accounts/doctype/pricing_rule/tests/test_pricing_rule_with_same_currency.js
deleted file mode 100644
index 601ff6b..0000000
--- a/erpnext/accounts/doctype/pricing_rule/tests/test_pricing_rule_with_same_currency.js
+++ /dev/null
@@ -1,56 +0,0 @@
-QUnit.module('Pricing Rule');
-
-QUnit.test("test pricing rule with same currency", function(assert) {
- assert.expect(4);
- let done = assert.async();
- frappe.run_serially([
- () => {
- return frappe.tests.make("Pricing Rule", [
- {title: 'Test Pricing Rule 1'},
- {apply_on: 'Item Code'},
- {item_code:'Test Product 4'},
- {selling:1},
- {min_qty:1},
- {max_qty:20},
- {valid_upto: frappe.datetime.add_days(frappe.defaults.get_default("year_end_date"), 1)},
- {rate_or_discount: 'Rate'},
- {rate:200},
- {currency:'USD'}
-
- ]);
- },
- () => cur_frm.save(),
- () => frappe.timeout(0.3),
- () => {
- assert.ok(cur_frm.doc.item_code=='Test Product 4');
- },
-
- () => {
- return frappe.tests.make('Sales Order', [
- {customer: 'Test Customer 1'},
- {currency: 'USD'},
- {items: [
- [
- {'delivery_date': frappe.datetime.add_days(frappe.defaults.get_default("year_end_date"), 1)},
- {'qty': 5},
- {'item_code': "Test Product 4"}
- ]
- ]}
- ]);
- },
- () => cur_frm.save(),
- () => frappe.timeout(0.3),
- () => {
- // get_item_details
- assert.ok(cur_frm.doc.items[0].pricing_rule=='Test Pricing Rule 1', "Pricing rule correct");
- assert.ok(cur_frm.doc.items[0].price_list_rate==200, "Item rate correct");
- // get_total
- assert.ok(cur_frm.doc.total== 1000, "Total correct");
- },
- () => frappe.timeout(0.3),
- () => frappe.tests.click_button('Submit'),
- () => frappe.tests.click_button('Yes'),
- () => frappe.timeout(0.3),
- () => done()
- ]);
-});
diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py
index 9655ac4..02bfc9d 100644
--- a/erpnext/accounts/doctype/pricing_rule/utils.py
+++ b/erpnext/accounts/doctype/pricing_rule/utils.py
@@ -264,6 +264,11 @@
else:
p.variant_of = None
+ if len(pricing_rules) > 1:
+ filtered_rules = list(filter(lambda x: x.currency==args.get('currency'), pricing_rules))
+ if filtered_rules:
+ pricing_rules = filtered_rules
+
# find pricing rule with highest priority
if pricing_rules:
max_priority = max(cint(p.priority) for p in pricing_rules)
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
index 03cbc4a..bd01164 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
@@ -130,6 +130,7 @@
"allocate_advances_automatically",
"get_advances",
"advances",
+ "advance_tax",
"payment_schedule_section",
"payment_terms_template",
"ignore_default_payment_terms_template",
@@ -1408,13 +1409,21 @@
{
"fieldname": "column_break_147",
"fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "advance_tax",
+ "fieldtype": "Table",
+ "hidden": 1,
+ "label": "Advance Tax",
+ "options": "Advance Tax",
+ "read_only": 1
}
],
"icon": "fa fa-file-text",
"idx": 204,
"is_submittable": 1,
"links": [],
- "modified": "2021-10-12 20:55:16.145651",
+ "modified": "2021-11-25 13:31:02.716727",
"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 62e3dc8..b364218 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -114,6 +114,9 @@
self.set_status()
self.validate_purchase_receipt_if_update_stock()
validate_inter_company_party(self.doctype, self.supplier, self.company, self.inter_company_invoice_reference)
+ self.reset_default_field_value("set_warehouse", "items", "warehouse")
+ self.reset_default_field_value("rejected_warehouse", "items", "rejected_warehouse")
+ self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse")
def validate_release_date(self):
if self.release_date and getdate(nowdate()) >= getdate(self.release_date):
@@ -294,8 +297,15 @@
item.expense_account = stock_not_billed_account
elif item.is_fixed_asset and not is_cwip_accounting_enabled(asset_category):
- item.expense_account = get_asset_category_account('fixed_asset_account', item=item.item_code,
+ asset_category_account = get_asset_category_account('fixed_asset_account', item=item.item_code,
company = self.company)
+ if not asset_category_account:
+ form_link = get_link_to_form('Asset Category', asset_category)
+ throw(
+ _("Please set Fixed Asset Account in {} against {}.").format(form_link, self.company),
+ title=_("Missing Account")
+ )
+ item.expense_account = asset_category_account
elif item.is_fixed_asset and item.pr_detail:
item.expense_account = asset_received_but_not_billed
elif not item.expense_account and for_validate:
@@ -427,6 +437,7 @@
self.update_project()
update_linked_doc(self.doctype, self.name, self.inter_company_invoice_reference)
+ self.update_advance_tax_references()
self.process_common_party_accounting()
@@ -472,8 +483,6 @@
self.make_exchange_gain_loss_gl_entries(gl_entries)
self.make_internal_transfer_gl_entries(gl_entries)
- self.allocate_advance_taxes(gl_entries)
-
gl_entries = make_regional_gl_entries(gl_entries, self)
gl_entries = merge_similar_entries(gl_entries)
@@ -496,11 +505,11 @@
# Checked both rounding_adjustment and rounded_total
# because rounded_total had value even before introcution of posting GLE based on rounded total
grand_total = self.rounded_total if (self.rounding_adjustment and self.rounded_total) else self.grand_total
+ base_grand_total = flt(self.base_rounded_total if (self.base_rounding_adjustment and self.base_rounded_total)
+ else self.base_grand_total, self.precision("base_grand_total"))
if grand_total and not self.is_internal_transfer():
# Did not use base_grand_total to book rounding loss gle
- grand_total_in_company_currency = flt(grand_total * self.conversion_rate,
- self.precision("grand_total"))
gl_entries.append(
self.get_gl_dict({
"account": self.credit_to,
@@ -508,8 +517,8 @@
"party": self.supplier,
"due_date": self.due_date,
"against": self.against_expense_account,
- "credit": grand_total_in_company_currency,
- "credit_in_account_currency": grand_total_in_company_currency \
+ "credit": base_grand_total,
+ "credit_in_account_currency": base_grand_total \
if self.party_account_currency==self.company_currency else grand_total,
"against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name,
"against_voucher_type": self.doctype,
@@ -729,7 +738,7 @@
"account": self.stock_received_but_not_billed,
"against": self.supplier,
"debit": flt(item.item_tax_amount, item.precision("item_tax_amount")),
- "remarks": self.remarks or "Accounting Entry for Stock",
+ "remarks": self.remarks or _("Accounting Entry for Stock"),
"cost_center": self.cost_center,
"project": item.project or self.project
}, item=item)
@@ -937,7 +946,7 @@
"cost_center": tax.cost_center,
"against": self.supplier,
"credit": valuation_tax[tax.name],
- "remarks": self.remarks or "Accounting Entry for Stock"
+ "remarks": self.remarks or _("Accounting Entry for Stock")
}, item=tax))
@property
@@ -1074,6 +1083,7 @@
unlink_inter_company_doc(self.doctype, self.name, self.inter_company_invoice_reference)
self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation')
+ self.update_advance_tax_references(cancel=1)
def update_project(self):
project_list = []
@@ -1150,7 +1160,10 @@
if not self.tax_withholding_category:
return
- tax_withholding_details = get_party_tax_withholding_details(self, self.tax_withholding_category)
+ tax_withholding_details, advance_taxes = get_party_tax_withholding_details(self, self.tax_withholding_category)
+
+ # Adjust TDS paid on advances
+ self.allocate_advance_tds(tax_withholding_details, advance_taxes)
if not tax_withholding_details:
return
@@ -1174,6 +1187,39 @@
# calculate totals again after applying TDS
self.calculate_taxes_and_totals()
+ def allocate_advance_tds(self, tax_withholding_details, advance_taxes):
+ self.set('advance_tax', [])
+ for tax in advance_taxes:
+ allocated_amount = 0
+ pending_amount = flt(tax.tax_amount - tax.allocated_amount)
+ if flt(tax_withholding_details.get('tax_amount')) >= pending_amount:
+ tax_withholding_details['tax_amount'] -= pending_amount
+ allocated_amount = pending_amount
+ elif flt(tax_withholding_details.get('tax_amount')) and flt(tax_withholding_details.get('tax_amount')) < pending_amount:
+ allocated_amount = tax_withholding_details['tax_amount']
+ tax_withholding_details['tax_amount'] = 0
+
+ self.append('advance_tax', {
+ 'reference_type': 'Payment Entry',
+ 'reference_name': tax.parent,
+ 'reference_detail': tax.name,
+ 'account_head': tax.account_head,
+ 'allocated_amount': allocated_amount
+ })
+
+ def update_advance_tax_references(self, cancel=0):
+ for tax in self.get('advance_tax'):
+ at = frappe.qb.DocType("Advance Taxes and Charges").as_("at")
+
+ if cancel:
+ frappe.qb.update(at).set(
+ at.allocated_amount, at.allocated_amount - tax.allocated_amount
+ ).where(at.name == tax.reference_detail).run()
+ else:
+ frappe.qb.update(at).set(
+ at.allocated_amount, at.allocated_amount + tax.allocated_amount
+ ).where(at.name == tax.reference_detail).run()
+
def set_status(self, update=False, status=None, update_modified=True):
if self.is_new():
if self.get('amended_from'):
diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.js
deleted file mode 100644
index 94b3b9e..0000000
--- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.js
+++ /dev/null
@@ -1,74 +0,0 @@
-QUnit.module('Purchase Invoice');
-
-QUnit.test("test purchase invoice", function(assert) {
- assert.expect(9);
- let done = assert.async();
- frappe.run_serially([
- () => {
- return frappe.tests.make('Purchase Invoice', [
- {supplier: 'Test Supplier'},
- {bill_no: 'in123'},
- {items: [
- [
- {'qty': 5},
- {'item_code': 'Test Product 1'},
- {'rate':100},
- ]
- ]},
- {update_stock:1},
- {supplier_address: 'Test1-Billing'},
- {contact_person: 'Contact 3-Test Supplier'},
- {taxes_and_charges: 'TEST In State GST - FT'},
- {tc_name: 'Test Term 1'},
- {terms: 'This is Test'},
- {payment_terms_template: '_Test Payment Term Template UI'}
- ]);
- },
- () => cur_frm.save(),
- () => {
- // get_item_details
- assert.ok(cur_frm.doc.items[0].item_name=='Test Product 1', "Item name correct");
- // get tax details
- assert.ok(cur_frm.doc.taxes_and_charges=='TEST In State GST - FT', "Tax details correct");
- // get tax account head details
- assert.ok(cur_frm.doc.taxes[0].account_head=='CGST - '+frappe.get_abbr(frappe.defaults.get_default('Company')), " Account Head abbr correct");
- // grand_total Calculated
- assert.ok(cur_frm.doc.grand_total==590, "Grad Total correct");
-
- assert.ok(cur_frm.doc.payment_terms_template, "Payment Terms Template is correct");
- assert.ok(cur_frm.doc.payment_schedule.length > 0, "Payment Term Schedule is not empty");
-
- },
- () => {
- let date = cur_frm.doc.due_date;
- frappe.tests.set_control('due_date', frappe.datetime.add_days(date, 1));
- frappe.timeout(0.5);
- assert.ok(cur_dialog && cur_dialog.is_visible, 'Message is displayed to user');
- },
- () => frappe.timeout(1),
- () => frappe.tests.click_button('Close'),
- () => frappe.timeout(0.5),
- () => frappe.tests.set_form_values(cur_frm, [{'payment_terms_schedule': ''}]),
- () => {
- let date = cur_frm.doc.due_date;
- frappe.tests.set_control('due_date', frappe.datetime.add_days(date, 1));
- frappe.timeout(0.5);
- assert.ok(cur_dialog && cur_dialog.is_visible, 'Message is displayed to user');
- },
- () => frappe.timeout(1),
- () => frappe.tests.click_button('Close'),
- () => frappe.timeout(0.5),
- () => frappe.tests.set_form_values(cur_frm, [{'payment_schedule': []}]),
- () => {
- let date = cur_frm.doc.due_date;
- frappe.tests.set_control('due_date', frappe.datetime.add_days(date, 1));
- frappe.timeout(0.5);
- assert.ok(!cur_dialog, 'Message is not shown');
- },
- () => cur_frm.save(),
- () => frappe.tests.click_button('Submit'),
- () => frappe.tests.click_button('Yes'),
- () => frappe.timeout(1),
- () => done()
- ]);
-});
diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
index 6e81c6d..21846bb 100644
--- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
@@ -13,6 +13,7 @@
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
from erpnext.controllers.accounts_controller import get_payment_terms
+from erpnext.controllers.buying_controller import QtyMismatchError
from erpnext.exceptions import InvalidCurrency
from erpnext.projects.doctype.project.test_project import make_project
from erpnext.stock.doctype.item.test_item import create_item
@@ -35,6 +36,27 @@
def tearDownClass(self):
unlink_payment_on_cancel_of_invoice(0)
+ def test_purchase_invoice_received_qty(self):
+ """
+ 1. Test if received qty is validated against accepted + rejected
+ 2. Test if received qty is auto set on save
+ """
+ pi = make_purchase_invoice(
+ qty=1,
+ rejected_qty=1,
+ received_qty=3,
+ item_code="_Test Item Home Desktop 200",
+ rejected_warehouse = "_Test Rejected Warehouse - _TC",
+ update_stock=True, do_not_save=True)
+ self.assertRaises(QtyMismatchError, pi.save)
+
+ pi.items[0].received_qty = 0
+ pi.save()
+ self.assertEqual(pi.items[0].received_qty, 2)
+
+ # teardown
+ pi.delete()
+
def test_gl_entries_without_perpetual_inventory(self):
frappe.db.set_value("Company", "_Test Company", "round_off_account", "Round Off - _TC")
pi = frappe.copy_doc(test_records[0])
@@ -811,29 +833,12 @@
pi.shipping_rule = shipping_rule.name
pi.insert()
-
- shipping_amount = 0.0
- for condition in shipping_rule.get("conditions"):
- if not condition.to_value or (flt(condition.from_value) <= pi.net_total <= flt(condition.to_value)):
- shipping_amount = condition.shipping_amount
-
- shipping_charge = {
- "doctype": "Purchase Taxes and Charges",
- "category": "Valuation and Total",
- "charge_type": "Actual",
- "account_head": shipping_rule.account,
- "cost_center": shipping_rule.cost_center,
- "tax_amount": shipping_amount,
- "description": shipping_rule.name,
- "add_deduct_tax": "Add"
- }
- pi.append("taxes", shipping_charge)
pi.save()
self.assertEqual(pi.net_total, 1250)
- self.assertEqual(pi.total_taxes_and_charges, 462.3)
- self.assertEqual(pi.grand_total, 1712.3)
+ self.assertEqual(pi.total_taxes_and_charges, 354.1)
+ self.assertEqual(pi.grand_total, 1604.1)
def test_make_pi_without_terms(self):
pi = make_purchase_invoice(do_not_save=1)
@@ -981,7 +986,7 @@
pi = make_purchase_invoice(item=item.name, qty=1, rate=100, do_not_save=True)
pi.set_posting_time = 1
- pi.posting_date = '2019-03-15'
+ pi.posting_date = '2019-01-10'
pi.items[0].enable_deferred_expense = 1
pi.items[0].service_start_date = "2019-01-10"
pi.items[0].service_end_date = "2019-03-15"
@@ -1155,25 +1160,21 @@
# Create Purchase Order with TDS applied
po = create_purchase_order(do_not_save=1, supplier=supplier.name, rate=3000, item='_Test Non Stock Item',
posting_date='2021-09-15')
- po.apply_tds = 1
- po.tax_withholding_category = 'TDS - 194 - Dividends - Individual'
po.save()
po.submit()
- # Update Unrealized Profit / Loss Account which is used as default advance tax account
- frappe.db.set_value('Company', '_Test Company', 'unrealized_profit_loss_account', '_Test Account Excise Duty - _TC')
-
# Create Payment Entry Against the order
payment_entry = get_payment_entry(dt='Purchase Order', dn=po.name)
payment_entry.paid_from = 'Cash - _TC'
+ payment_entry.apply_tax_withholding_amount = 1
+ payment_entry.tax_withholding_category = 'TDS - 194 - Dividends - Individual'
payment_entry.save()
payment_entry.submit()
# Check GLE for Payment Entry
expected_gle = [
- ['_Test Account Excise Duty - _TC', 3000, 0],
['Cash - _TC', 0, 27000],
- ['Creditors - _TC', 27000, 0],
+ ['Creditors - _TC', 30000, 0],
['TDS Payable - _TC', 0, 3000],
]
@@ -1199,9 +1200,7 @@
# Zero net effect on final TDS Payable on invoice
expected_gle = [
['_Test Account Cost for Goods Sold - _TC', 30000],
- ['_Test Account Excise Duty - _TC', -3000],
- ['Creditors - _TC', -27000],
- ['TDS Payable - _TC', 0]
+ ['Creditors - _TC', -30000]
]
gl_entries = frappe.db.sql("""select account, sum(debit - credit) as amount
@@ -1214,6 +1213,14 @@
self.assertEqual(expected_gle[i][0], gle.account)
self.assertEqual(expected_gle[i][1], gle.amount)
+ payment_entry.load_from_db()
+ self.assertEqual(payment_entry.taxes[0].allocated_amount, 3000)
+
+ purchase_invoice.cancel()
+
+ payment_entry.load_from_db()
+ self.assertEqual(payment_entry.taxes[0].allocated_amount, 0)
+
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`
@@ -1229,7 +1236,7 @@
def update_tax_witholding_category(company, account):
from erpnext.accounts.utils import get_fiscal_year
- fiscal_year = get_fiscal_year(fiscal_year='2021')
+ fiscal_year = get_fiscal_year(date=nowdate())
if not frappe.db.get_value('Tax Withholding Rate',
{'parent': 'TDS - 194 - Dividends - Individual', 'from_date': ('>=', fiscal_year[1]),
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 d39a9fc..f9b2efd 100644
--- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
+++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
@@ -22,10 +22,10 @@
"received_qty",
"qty",
"rejected_qty",
- "stock_uom",
"col_break2",
"uom",
"conversion_factor",
+ "stock_uom",
"stock_qty",
"sec_break1",
"price_list_rate",
@@ -175,7 +175,8 @@
{
"fieldname": "received_qty",
"fieldtype": "Float",
- "label": "Received Qty"
+ "label": "Received Qty",
+ "read_only": 1
},
{
"bold": 1,
@@ -223,7 +224,7 @@
{
"fieldname": "stock_qty",
"fieldtype": "Float",
- "label": "Stock Qty",
+ "label": "Accepted Qty in Stock UOM",
"print_hide": 1,
"read_only": 1,
"reqd": 1
@@ -870,10 +871,11 @@
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2021-09-01 16:04:03.538643",
+ "modified": "2021-11-15 17:04:07.191013",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice Item",
+ "naming_rule": "Random",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
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
deleted file mode 100644
index 10b05d0..0000000
--- a/erpnext/accounts/doctype/purchase_taxes_and_charges_template/test_purchase_taxes_and_charges_template.js
+++ /dev/null
@@ -1,28 +0,0 @@
-QUnit.module('Sales Taxes and Charges Template');
-
-QUnit.test("test sales taxes and charges template", function(assert) {
- assert.expect(2);
- let done = assert.async();
- frappe.run_serially([
- () => {
- return frappe.tests.make('Purchase Taxes and Charges Template', [
- {title: "TEST In State GST"},
- {taxes:[
- [
- {charge_type:"On Net Total"},
- {account_head:"CGST - "+frappe.get_abbr(frappe.defaults.get_default("Company")) }
- ],
- [
- {charge_type:"On Net Total"},
- {account_head:"SGST - "+frappe.get_abbr(frappe.defaults.get_default("Company")) }
- ]
- ]}
- ]);
- },
- () => {
- assert.ok(cur_frm.doc.title=='TEST In State GST');
- assert.ok(cur_frm.doc.name=='TEST In State GST - FT');
- },
- () => done()
- ]);
-});
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
index a1f3ee4..39dfd8d 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
@@ -516,15 +516,6 @@
}
}
-// project name
-//--------------------------
-cur_frm.fields_dict['project'].get_query = function(doc, cdt, cdn) {
- return{
- query: "erpnext.controllers.queries.get_project_name",
- filters: {'customer': doc.customer}
- }
-}
-
// Income Account in Details Table
// --------------------------------
cur_frm.set_query("income_account", "items", function(doc) {
@@ -978,7 +969,7 @@
}
if (frm.doc.is_debit_note) {
- frm.set_df_property('return_against', 'label', 'Adjustment Against');
+ frm.set_df_property('return_against', 'label', __('Adjustment Against'));
}
if (frappe.boot.active_domains.includes("Healthcare")) {
@@ -988,10 +979,10 @@
if (cint(frm.doc.docstatus==0) && cur_frm.page.current_view_name!=="pos" && !frm.doc.is_return) {
frm.add_custom_button(__('Healthcare Services'), function() {
get_healthcare_services_to_invoice(frm);
- },"Get Items From");
+ },__("Get Items From"));
frm.add_custom_button(__('Prescriptions'), function() {
get_drugs_to_invoice(frm);
- },"Get Items From");
+ },__("Get Items From"));
}
}
else {
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
index 93e32f1..5062c1c 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
@@ -182,6 +182,7 @@
"sales_team_section_break",
"sales_partner",
"column_break10",
+ "amount_eligible_for_commission",
"commission_rate",
"total_commission",
"section_break2",
@@ -650,7 +651,7 @@
"hide_seconds": 1,
"label": "Ignore Pricing Rule",
"no_copy": 1,
- "permlevel": 1,
+ "permlevel": 0,
"print_hide": 1
},
{
@@ -2019,6 +2020,12 @@
"label": "Total Billing Hours",
"print_hide": 1,
"read_only": 1
+ },
+ {
+ "fieldname": "amount_eligible_for_commission",
+ "fieldtype": "Currency",
+ "label": "Amount Eligible for Commission",
+ "read_only": 1
}
],
"icon": "fa fa-file-text",
@@ -2031,7 +2038,7 @@
"link_fieldname": "consolidated_invoice"
}
],
- "modified": "2021-10-11 20:19:38.667508",
+ "modified": "2021-12-23 20:19:38.667508",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",
@@ -2086,4 +2093,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 59d46fc..98bc953 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -155,6 +155,8 @@
if self.redeem_loyalty_points and self.loyalty_program and self.loyalty_points and not self.is_consolidated:
validate_loyalty_points(self, self.loyalty_points)
+ self.reset_default_field_value("set_warehouse", "items", "warehouse")
+
def validate_fixed_asset(self):
for d in self.get("items"):
if d.is_fixed_asset and d.meta.get_field("asset") and d.asset:
@@ -842,8 +844,6 @@
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)
@@ -862,11 +862,11 @@
# Checked both rounding_adjustment and rounded_total
# because rounded_total had value even before introcution of posting GLE based on rounded total
grand_total = self.rounded_total if (self.rounding_adjustment and self.rounded_total) else self.grand_total
+ base_grand_total = flt(self.base_rounded_total if (self.base_rounding_adjustment and self.base_rounded_total)
+ else self.base_grand_total, self.precision("base_grand_total"))
+
if grand_total and not self.is_internal_transfer():
# Didnot use base_grand_total to book rounding loss gle
- grand_total_in_company_currency = flt(grand_total * self.conversion_rate,
- self.precision("grand_total"))
-
gl_entries.append(
self.get_gl_dict({
"account": self.debit_to,
@@ -874,8 +874,8 @@
"party": self.customer,
"due_date": self.due_date,
"against": self.against_income_account,
- "debit": grand_total_in_company_currency,
- "debit_in_account_currency": grand_total_in_company_currency \
+ "debit": base_grand_total,
+ "debit_in_account_currency": base_grand_total \
if self.party_account_currency==self.company_currency else grand_total,
"against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name,
"against_voucher_type": self.doctype,
@@ -1049,6 +1049,8 @@
frappe.flags.is_reverse_depr_entry = False
asset.flags.ignore_validate_update_after_submit = True
schedule.journal_entry = None
+ depreciation_amount = self.get_depreciation_amount_in_je(reverse_journal_entry)
+ asset.finance_books[0].value_after_depreciation += depreciation_amount
asset.save()
def get_posting_date_of_sales_invoice(self):
@@ -1071,6 +1073,12 @@
return False
+ def get_depreciation_amount_in_je(self, journal_entry):
+ if journal_entry.accounts[0].debit_in_account_currency:
+ return journal_entry.accounts[0].debit_in_account_currency
+ else:
+ return journal_entry.accounts[0].credit_in_account_currency
+
@property
def enable_discount_accounting(self):
if not hasattr(self, "_enable_discount_accounting"):
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.js
deleted file mode 100644
index 1c052bd..0000000
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.js
+++ /dev/null
@@ -1,73 +0,0 @@
-QUnit.module('Sales Invoice');
-
-QUnit.test("test sales Invoice", function(assert) {
- assert.expect(9);
- let done = assert.async();
- frappe.run_serially([
- () => {
- return frappe.tests.make('Sales Invoice', [
- {customer: 'Test Customer 1'},
- {items: [
- [
- {'qty': 5},
- {'item_code': 'Test Product 1'},
- ]
- ]},
- {update_stock:1},
- {customer_address: 'Test1-Billing'},
- {shipping_address_name: 'Test1-Shipping'},
- {contact_person: 'Contact 1-Test Customer 1'},
- {taxes_and_charges: 'TEST In State GST - FT'},
- {tc_name: 'Test Term 1'},
- {terms: 'This is Test'},
- {payment_terms_template: '_Test Payment Term Template UI'}
- ]);
- },
- () => cur_frm.save(),
- () => {
- // get_item_details
- assert.ok(cur_frm.doc.items[0].item_name=='Test Product 1', "Item name correct");
- // get tax details
- assert.ok(cur_frm.doc.taxes_and_charges=='TEST In State GST - FT', "Tax details correct");
- // get tax account head details
- assert.ok(cur_frm.doc.taxes[0].account_head=='CGST - '+frappe.get_abbr(frappe.defaults.get_default('Company')), " Account Head abbr correct");
- // grand_total Calculated
- assert.ok(cur_frm.doc.grand_total==590, "Grand Total correct");
-
- assert.ok(cur_frm.doc.payment_terms_template, "Payment Terms Template is correct");
- assert.ok(cur_frm.doc.payment_schedule.length > 0, "Payment Term Schedule is not empty");
-
- },
- () => {
- let date = cur_frm.doc.due_date;
- frappe.tests.set_control('due_date', frappe.datetime.add_days(date, 1));
- frappe.timeout(0.5);
- assert.ok(cur_dialog && cur_dialog.is_visible, 'Message is displayed to user');
- },
- () => frappe.timeout(1),
- () => frappe.tests.click_button('Close'),
- () => frappe.timeout(0.5),
- () => frappe.tests.set_form_values(cur_frm, [{'payment_terms_schedule': ''}]),
- () => {
- let date = cur_frm.doc.due_date;
- frappe.tests.set_control('due_date', frappe.datetime.add_days(date, 1));
- frappe.timeout(0.5);
- assert.ok(cur_dialog && cur_dialog.is_visible, 'Message is displayed to user');
- },
- () => frappe.timeout(1),
- () => frappe.tests.click_button('Close'),
- () => frappe.timeout(0.5),
- () => frappe.tests.set_form_values(cur_frm, [{'payment_schedule': []}]),
- () => {
- let date = cur_frm.doc.due_date;
- frappe.tests.set_control('due_date', frappe.datetime.add_days(date, 1));
- frappe.timeout(0.5);
- assert.ok(!cur_dialog, 'Message is not shown');
- },
- () => cur_frm.save(),
- () => frappe.tests.click_button('Submit'),
- () => frappe.tests.click_button('Yes'),
- () => frappe.timeout(0.3),
- () => done()
- ]);
-});
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index 969756a..c02c80a 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -20,6 +20,7 @@
from erpnext.accounts.utils import PaymentEntryUnlinkError
from erpnext.assets.doctype.asset.depreciation import post_depreciation_entries
from erpnext.assets.doctype.asset.test_asset import create_asset, create_asset_data
+from erpnext.controllers.accounts_controller import update_invoice_status
from erpnext.controllers.taxes_and_totals import get_itemised_tax_breakup_data
from erpnext.exceptions import InvalidAccountCurrency, InvalidCurrency
from erpnext.regional.india.utils import get_ewb_data
@@ -1603,28 +1604,12 @@
si.shipping_rule = shipping_rule.name
si.insert()
-
- shipping_amount = 0.0
- for condition in shipping_rule.get("conditions"):
- if not condition.to_value or (flt(condition.from_value) <= si.net_total <= flt(condition.to_value)):
- shipping_amount = condition.shipping_amount
-
- shipping_charge = {
- "doctype": "Sales Taxes and Charges",
- "category": "Valuation and Total",
- "charge_type": "Actual",
- "account_head": shipping_rule.account,
- "cost_center": shipping_rule.cost_center,
- "tax_amount": shipping_amount,
- "description": shipping_rule.name
- }
- si.append("taxes", shipping_charge)
si.save()
self.assertEqual(si.net_total, 1250)
- self.assertEqual(si.total_taxes_and_charges, 577.05)
- self.assertEqual(si.grand_total, 1827.05)
+ self.assertEqual(si.total_taxes_and_charges, 468.85)
+ self.assertEqual(si.grand_total, 1718.85)
@@ -2316,6 +2301,7 @@
from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import (
make_customer,
)
+ from erpnext.accounts.doctype.party_link.party_link import create_party_link
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
# create a customer
@@ -2324,13 +2310,7 @@
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()
+ party_link = create_party_link("Supplier", supplier, customer)
# enable common party accounting
frappe.db.set_value('Accounts Settings', None, 'enable_common_party_accounting', 1)
@@ -2406,6 +2386,64 @@
si.reload()
self.assertEqual(si.status, "Paid")
+ def test_update_invoice_status(self):
+ today = nowdate()
+
+ # Sales Invoice without Payment Schedule
+ si = create_sales_invoice(posting_date=add_days(today, -5))
+
+ # Sales Invoice with Payment Schedule
+ si_with_payment_schedule = create_sales_invoice(do_not_submit=True)
+ si_with_payment_schedule.extend("payment_schedule", [
+ {
+ "due_date": add_days(today, -5),
+ "invoice_portion": 50,
+ "payment_amount": si_with_payment_schedule.grand_total / 2
+ },
+ {
+ "due_date": add_days(today, 5),
+ "invoice_portion": 50,
+ "payment_amount": si_with_payment_schedule.grand_total / 2
+ }
+ ])
+ si_with_payment_schedule.submit()
+
+
+ for invoice in (si, si_with_payment_schedule):
+ invoice.db_set("status", "Unpaid")
+ update_invoice_status()
+ invoice.reload()
+ self.assertEqual(invoice.status, "Overdue")
+
+ invoice.db_set("status", "Unpaid and Discounted")
+ update_invoice_status()
+ invoice.reload()
+ self.assertEqual(invoice.status, "Overdue and Discounted")
+
+
+ def test_sales_commission(self):
+ si = frappe.copy_doc(test_records[0])
+ item = copy.deepcopy(si.get('items')[0])
+ item.update({
+ "qty": 1,
+ "rate": 500,
+ "grant_commission": 1
+ })
+ si.append("items", item)
+
+ # Test valid values
+ for commission_rate, total_commission in ((0, 0), (10, 50), (100, 500)):
+ si.commission_rate = commission_rate
+ si.save()
+ self.assertEqual(si.amount_eligible_for_commission, 500)
+ self.assertEqual(si.total_commission, total_commission)
+
+ # Test invalid values
+ for commission_rate in (101, -1):
+ si.reload()
+ si.commission_rate = commission_rate
+ self.assertRaises(frappe.ValidationError, si.save)
+
def test_sales_invoice_submission_post_account_freezing_date(self):
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', add_days(getdate(), 1))
si = create_sales_invoice(do_not_save=True)
@@ -2418,6 +2456,32 @@
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', None)
+ def test_over_billing_case_against_delivery_note(self):
+ '''
+ Test a case where duplicating the item with qty = 1 in the invoice
+ allows overbilling even if it is disabled
+ '''
+ from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
+
+ over_billing_allowance = frappe.db.get_single_value('Accounts Settings', 'over_billing_allowance')
+ frappe.db.set_value('Accounts Settings', None, 'over_billing_allowance', 0)
+
+ dn = create_delivery_note()
+ dn.submit()
+
+ si = make_sales_invoice(dn.name)
+ # make a copy of first item and add it to invoice
+ item_copy = frappe.copy_doc(si.items[0])
+ si.append('items', item_copy)
+ si.save()
+
+ with self.assertRaises(frappe.ValidationError) as err:
+ si.submit()
+
+ self.assertTrue("cannot overbill" in str(err.exception).lower())
+
+ frappe.db.set_value('Accounts Settings', None, 'over_billing_allowance', over_billing_allowance)
+
def get_sales_invoice_for_e_invoice():
si = make_sales_invoice_for_ewaybill()
si.naming_series = 'INV-2020-.#####'
diff --git a/erpnext/accounts/doctype/sales_invoice/tests/test_sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/tests/test_sales_invoice.js
deleted file mode 100644
index 61d78e1..0000000
--- a/erpnext/accounts/doctype/sales_invoice/tests/test_sales_invoice.js
+++ /dev/null
@@ -1,42 +0,0 @@
-QUnit.module('Sales Invoice');
-
-QUnit.test("test sales Invoice", function(assert) {
- assert.expect(4);
- let done = assert.async();
- frappe.run_serially([
- () => {
- return frappe.tests.make('Sales Invoice', [
- {customer: 'Test Customer 1'},
- {items: [
- [
- {'qty': 5},
- {'item_code': 'Test Product 1'},
- ]
- ]},
- {update_stock:1},
- {customer_address: 'Test1-Billing'},
- {shipping_address_name: 'Test1-Shipping'},
- {contact_person: 'Contact 1-Test Customer 1'},
- {taxes_and_charges: 'TEST In State GST - FT'},
- {tc_name: 'Test Term 1'},
- {terms: 'This is Test'}
- ]);
- },
- () => cur_frm.save(),
- () => {
- // get_item_details
- assert.ok(cur_frm.doc.items[0].item_name=='Test Product 1', "Item name correct");
- // get tax details
- assert.ok(cur_frm.doc.taxes_and_charges=='TEST In State GST - FT', "Tax details correct");
- // get tax account head details
- assert.ok(cur_frm.doc.taxes[0].account_head=='CGST - '+frappe.get_abbr(frappe.defaults.get_default('Company')), " Account Head abbr correct");
- // grand_total Calculated
- assert.ok(cur_frm.doc.grand_total==590, "Grad Total correct");
-
- },
- () => frappe.tests.click_button('Submit'),
- () => frappe.tests.click_button('Yes'),
- () => frappe.timeout(0.3),
- () => 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
deleted file mode 100644
index cf2d0fb..0000000
--- a/erpnext/accounts/doctype/sales_invoice/tests/test_sales_invoice_with_margin.js
+++ /dev/null
@@ -1,35 +0,0 @@
-QUnit.module('Accounts');
-
-QUnit.test("test sales invoice with margin", function(assert) {
- assert.expect(3);
- let done = assert.async();
- frappe.run_serially([
- () => {
- return frappe.tests.make('Sales Invoice', [
- {customer: 'Test Customer 1'},
- {selling_price_list: 'Test-Selling-USD'},
- {currency: 'USD'},
- {items: [
- [
- {'item_code': 'Test Product 4'},
- {'delivery_date': frappe.datetime.add_days(frappe.defaults.get_default("year_end_date"), 1)},
- {'qty': 1},
- {'margin_type': 'Percentage'},
- {'margin_rate_or_amount': 20}
- ]
- ]}
- ]);
- },
- () => cur_frm.save(),
- () => {
- assert.ok(cur_frm.doc.items[0].rate_with_margin == 240, "Margin rate correct");
- assert.ok(cur_frm.doc.items[0].base_rate_with_margin == cur_frm.doc.conversion_rate * 240, "Base margin rate correct");
- assert.ok(cur_frm.doc.total == 240, "Amount correct");
-
- },
- () => frappe.tests.click_button('Submit'),
- () => frappe.tests.click_button('Yes'),
- () => frappe.timeout(0.3),
- () => 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
deleted file mode 100644
index 45d9a14..0000000
--- a/erpnext/accounts/doctype/sales_invoice/tests/test_sales_invoice_with_payment.js
+++ /dev/null
@@ -1,56 +0,0 @@
-QUnit.module('Sales Invoice');
-
-QUnit.test("test sales Invoice with payment", function(assert) {
- assert.expect(4);
- let done = assert.async();
- frappe.run_serially([
- () => {
- return frappe.tests.make('Sales Invoice', [
- {customer: 'Test Customer 1'},
- {items: [
- [
- {'qty': 5},
- {'item_code': 'Test Product 1'},
- ]
- ]},
- {update_stock:1},
- {customer_address: 'Test1-Billing'},
- {shipping_address_name: 'Test1-Shipping'},
- {contact_person: 'Contact 1-Test Customer 1'},
- {taxes_and_charges: 'TEST In State GST - FT'},
- {tc_name: 'Test Term 1'},
- {terms: 'This is Test'},
- {payment_terms_template: '_Test Payment Term Template UI'}
- ]);
- },
- () => cur_frm.save(),
- () => {
- // get_item_details
- assert.ok(cur_frm.doc.items[0].item_name=='Test Product 1', "Item name correct");
- // get tax details
- assert.ok(cur_frm.doc.taxes_and_charges=='TEST In State GST - FT', "Tax details correct");
- // grand_total Calculated
- assert.ok(cur_frm.doc.grand_total==590, "Grad Total correct");
-
- },
- () => frappe.tests.click_button('Submit'),
- () => frappe.tests.click_button('Yes'),
- () => frappe.timeout(2),
- () => frappe.tests.click_button('Close'),
- () => frappe.tests.click_button('Make'),
- () => frappe.tests.click_link('Payment'),
- () => frappe.timeout(0.2),
- () => { cur_frm.set_value('mode_of_payment','Cash');},
- () => { cur_frm.set_value('paid_to','Cash - '+frappe.get_abbr(frappe.defaults.get_default('Company')));},
- () => {cur_frm.set_value('reference_no','TEST1234');},
- () => {cur_frm.set_value('reference_date',frappe.datetime.add_days(frappe.datetime.nowdate(), 0));},
- () => cur_frm.save(),
- () => {
- // get payment details
- assert.ok(cur_frm.doc.paid_amount==590, "Paid Amount Correct");
- },
- () => frappe.tests.click_button('Submit'),
- () => frappe.tests.click_button('Yes'),
- () => 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
deleted file mode 100644
index 0464e45..0000000
--- a/erpnext/accounts/doctype/sales_invoice/tests/test_sales_invoice_with_payment_request.js
+++ /dev/null
@@ -1,51 +0,0 @@
-QUnit.module('Sales Invoice');
-
-QUnit.test("test sales Invoice with payment request", function(assert) {
- assert.expect(4);
- let done = assert.async();
- frappe.run_serially([
- () => {
- return frappe.tests.make('Sales Invoice', [
- {customer: 'Test Customer 1'},
- {items: [
- [
- {'qty': 5},
- {'item_code': 'Test Product 1'},
- ]
- ]},
- {update_stock:1},
- {customer_address: 'Test1-Billing'},
- {shipping_address_name: 'Test1-Shipping'},
- {contact_person: 'Contact 1-Test Customer 1'},
- {taxes_and_charges: 'TEST In State GST - FT'},
- {tc_name: 'Test Term 1'},
- {terms: 'This is Test'}
- ]);
- },
- () => cur_frm.save(),
- () => {
- // get_item_details
- assert.ok(cur_frm.doc.items[0].item_name=='Test Product 1', "Item name correct");
- // get tax details
- assert.ok(cur_frm.doc.taxes_and_charges=='TEST In State GST - FT', "Tax details correct");
- // grand_total Calculated
- assert.ok(cur_frm.doc.grand_total==590, "Grad Total correct");
-
- },
- () => frappe.tests.click_button('Submit'),
- () => frappe.tests.click_button('Yes'),
- () => frappe.timeout(2),
- () => frappe.tests.click_button('Close'),
- () => frappe.tests.click_button('Make'),
- () => frappe.tests.click_link('Payment Request'),
- () => frappe.timeout(0.2),
- () => { cur_frm.set_value('print_format','GST Tax Invoice');},
- () => { cur_frm.set_value('email_to','test@gmail.com');},
- () => cur_frm.save(),
- () => {
- // get payment details
- assert.ok(cur_frm.doc.grand_total==590, "grand total Correct");
- },
- () => 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
deleted file mode 100644
index af484d7..0000000
--- a/erpnext/accounts/doctype/sales_invoice/tests/test_sales_invoice_with_serialize_item.js
+++ /dev/null
@@ -1,44 +0,0 @@
-QUnit.module('Sales Invoice');
-
-QUnit.test("test sales Invoice with serialize item", function(assert) {
- assert.expect(5);
- let done = assert.async();
- frappe.run_serially([
- () => {
- return frappe.tests.make('Sales Invoice', [
- {customer: 'Test Customer 1'},
- {items: [
- [
- {'qty': 2},
- {'item_code': 'Test Product 4'},
- ]
- ]},
- {update_stock:1},
- {customer_address: 'Test1-Billing'},
- {shipping_address_name: 'Test1-Shipping'},
- {contact_person: 'Contact 1-Test Customer 1'},
- {taxes_and_charges: 'TEST In State GST - FT'},
- {tc_name: 'Test Term 1'},
- {terms: 'This is Test'}
- ]);
- },
- () => cur_frm.save(),
- () => {
- // get_item_details
- assert.ok(cur_frm.doc.items[0].item_name=='Test Product 4', "Item name correct");
- // get tax details
- assert.ok(cur_frm.doc.taxes_and_charges=='TEST In State GST - FT', "Tax details correct");
- // get tax account head details
- assert.ok(cur_frm.doc.taxes[0].account_head=='CGST - '+frappe.get_abbr(frappe.defaults.get_default('Company')), " Account Head abbr correct");
- // get batch number
- assert.ok(cur_frm.doc.items[0].batch_no=='TEST-BATCH-001', " Batch Details correct");
- // grand_total Calculated
- assert.ok(cur_frm.doc.grand_total==218, "Grad Total correct");
-
- },
- () => frappe.tests.click_button('Submit'),
- () => frappe.tests.click_button('Yes'),
- () => frappe.timeout(0.3),
- () => done()
- ]);
-});
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 b90f3f0..ae9ac35 100644
--- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json
+++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json
@@ -47,6 +47,7 @@
"pricing_rules",
"stock_uom_rate",
"is_free_item",
+ "grant_commission",
"section_break_21",
"net_rate",
"net_amount",
@@ -828,15 +829,23 @@
"fieldtype": "Link",
"label": "Discount Account",
"options": "Account"
+ },
+ {
+ "default": "0",
+ "fieldname": "grant_commission",
+ "fieldtype": "Check",
+ "label": "Grant Commission",
+ "read_only": 1
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2021-08-19 13:41:53.435827",
+ "modified": "2021-10-05 12:24:54.968907",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Item",
+ "naming_rule": "Random",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
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
deleted file mode 100644
index 8cd42f6..0000000
--- a/erpnext/accounts/doctype/sales_taxes_and_charges_template/test_sales_taxes_and_charges_template.js
+++ /dev/null
@@ -1,28 +0,0 @@
-QUnit.module('Sales Taxes and Charges Template');
-
-QUnit.test("test sales taxes and charges template", function(assert) {
- assert.expect(2);
- let done = assert.async();
- frappe.run_serially([
- () => {
- return frappe.tests.make('Sales Taxes and Charges Template', [
- {title: "TEST In State GST"},
- {taxes:[
- [
- {charge_type:"On Net Total"},
- {account_head:"CGST - "+frappe.get_abbr(frappe.defaults.get_default("Company")) }
- ],
- [
- {charge_type:"On Net Total"},
- {account_head:"SGST - "+frappe.get_abbr(frappe.defaults.get_default("Company")) }
- ]
- ]}
- ]);
- },
- () => {
- assert.ok(cur_frm.doc.title=='TEST In State GST');
- assert.ok(cur_frm.doc.name=='TEST In State GST - FT');
- },
- () => done()
- ]);
-});
diff --git a/erpnext/accounts/doctype/shipping_rule/test_shipping_rule.js b/erpnext/accounts/doctype/shipping_rule/test_shipping_rule.js
deleted file mode 100644
index 63ea1bf..0000000
--- a/erpnext/accounts/doctype/shipping_rule/test_shipping_rule.js
+++ /dev/null
@@ -1,36 +0,0 @@
-QUnit.module('Shipping Rule');
-
-QUnit.test("test Shipping Rule", function(assert) {
- assert.expect(1);
- let done = assert.async();
- frappe.run_serially([
- () => {
- return frappe.tests.make("Shipping Rule", [
- {label: "Next Day Shipping"},
- {shipping_rule_type: "Selling"},
- {calculate_based_on: 'Net Total'},
- {conditions:[
- [
- {from_value:1},
- {to_value:200},
- {shipping_amount:100}
- ],
- [
- {from_value:201},
- {to_value:2000},
- {shipping_amount:50}
- ],
- ]},
- {countries:[
- [
- {country:'India'}
- ]
- ]},
- {account:'Accounts Payable - '+frappe.get_abbr(frappe.defaults.get_default("Company"))},
- {cost_center:'Main - '+frappe.get_abbr(frappe.defaults.get_default("Company"))}
- ]);
- },
- () => {assert.ok(cur_frm.doc.name=='Next Day Shipping');},
- () => 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
deleted file mode 100644
index f3668b8..0000000
--- a/erpnext/accounts/doctype/shipping_rule/tests/test_shipping_rule_for_buying.js
+++ /dev/null
@@ -1,36 +0,0 @@
-QUnit.module('Shipping Rule');
-
-QUnit.test("test Shipping Rule", function(assert) {
- assert.expect(1);
- let done = assert.async();
- frappe.run_serially([
- () => {
- return frappe.tests.make("Shipping Rule", [
- {label: "Two Day Shipping"},
- {shipping_rule_type: "Buying"},
- {fixed_shipping_amount: 0},
- {conditions:[
- [
- {from_value:1},
- {to_value:200},
- {shipping_amount:100}
- ],
- [
- {from_value:201},
- {to_value:3000},
- {shipping_amount:200}
- ],
- ]},
- {countries:[
- [
- {country:'India'}
- ]
- ]},
- {account:'Accounts Payable - '+frappe.get_abbr(frappe.defaults.get_default("Company"))},
- {cost_center:'Main - '+frappe.get_abbr(frappe.defaults.get_default("Company"))}
- ]);
- },
- () => {assert.ok(cur_frm.doc.name=='Two Day Shipping');},
- () => done()
- ]);
-});
diff --git a/erpnext/accounts/doctype/subscription/subscription.py b/erpnext/accounts/doctype/subscription/subscription.py
index 1dae87f..467d4a1 100644
--- a/erpnext/accounts/doctype/subscription/subscription.py
+++ b/erpnext/accounts/doctype/subscription/subscription.py
@@ -23,6 +23,7 @@
get_accounting_dimensions,
)
from erpnext.accounts.doctype.subscription_plan.subscription_plan import get_plan_rate
+from erpnext.accounts.party import get_party_account_currency
class Subscription(Document):
@@ -355,6 +356,9 @@
if frappe.db.get_value('Supplier', self.party, 'tax_withholding_category'):
invoice.apply_tds = 1
+ ### Add party currency to invoice
+ invoice.currency = get_party_account_currency(self.party_type, self.party, self.company)
+
## Add dimensions in invoice for subscription:
accounting_dimensions = get_accounting_dimensions()
diff --git a/erpnext/accounts/doctype/subscription/test_subscription.js b/erpnext/accounts/doctype/subscription/test_subscription.js
deleted file mode 100644
index 2872a21..0000000
--- a/erpnext/accounts/doctype/subscription/test_subscription.js
+++ /dev/null
@@ -1,32 +0,0 @@
-/* eslint-disable */
-// rename this file from _test_[name] to test_[name] to activate
-// and remove above this line
-
-QUnit.test("test: Subscription", function (assert) {
- assert.expect(4);
- let done = assert.async();
- frappe.run_serially([
- // insert a new Subscription
- () => {
- return frappe.tests.make("Subscription", [
- {reference_doctype: 'Sales Invoice'},
- {reference_document: 'SINV-00004'},
- {start_date: frappe.datetime.month_start()},
- {end_date: frappe.datetime.month_end()},
- {frequency: 'Weekly'}
- ]);
- },
- () => cur_frm.savesubmit(),
- () => frappe.timeout(1),
- () => frappe.click_button('Yes'),
- () => frappe.timeout(2),
- () => {
- assert.ok(cur_frm.doc.frequency.includes("Weekly"), "Set frequency Weekly");
- assert.ok(cur_frm.doc.reference_doctype.includes("Sales Invoice"), "Set base doctype Sales Invoice");
- assert.equal(cur_frm.doc.docstatus, 1, "Submitted subscription");
- assert.equal(cur_frm.doc.next_schedule_date,
- frappe.datetime.add_days(frappe.datetime.get_today(), 7), "Set schedule date");
- },
- () => done()
- ]);
-});
diff --git a/erpnext/accounts/doctype/subscription/test_subscription.py b/erpnext/accounts/doctype/subscription/test_subscription.py
index 9dd370b..6f67bc5 100644
--- a/erpnext/accounts/doctype/subscription/test_subscription.py
+++ b/erpnext/accounts/doctype/subscription/test_subscription.py
@@ -60,15 +60,38 @@
plan.billing_interval_count = 3
plan.insert()
+ if not frappe.db.exists('Subscription Plan', '_Test Plan Multicurrency'):
+ plan = frappe.new_doc('Subscription Plan')
+ plan.plan_name = '_Test Plan Multicurrency'
+ plan.item = '_Test Non Stock Item'
+ plan.price_determination = "Fixed Rate"
+ plan.cost = 50
+ plan.currency = 'USD'
+ plan.billing_interval = 'Month'
+ plan.billing_interval_count = 1
+ plan.insert()
+
+def create_parties():
if not frappe.db.exists('Supplier', '_Test Supplier'):
supplier = frappe.new_doc('Supplier')
supplier.supplier_name = '_Test Supplier'
supplier.supplier_group = 'All Supplier Groups'
supplier.insert()
+ if not frappe.db.exists('Customer', '_Test Subscription Customer'):
+ customer = frappe.new_doc('Customer')
+ customer.customer_name = '_Test Subscription Customer'
+ customer.billing_currency = 'USD'
+ customer.append('accounts', {
+ 'company': '_Test Company',
+ 'account': '_Test Receivable USD - _TC'
+ })
+ customer.insert()
+
class TestSubscription(unittest.TestCase):
def setUp(self):
create_plan()
+ create_parties()
def test_create_subscription_with_trial_with_correct_period(self):
subscription = frappe.new_doc('Subscription')
@@ -637,3 +660,22 @@
subscription.process()
self.assertEqual(len(subscription.invoices), 1)
+
+ def test_multicurrency_subscription(self):
+ subscription = frappe.new_doc('Subscription')
+ subscription.party_type = 'Customer'
+ subscription.party = '_Test Subscription Customer'
+ subscription.generate_invoice_at_period_start = 1
+ subscription.company = '_Test Company'
+ # select subscription start date as '2018-01-15'
+ subscription.start_date = '2018-01-01'
+ subscription.append('plans', {'plan': '_Test Plan Multicurrency', 'qty': 1})
+ subscription.save()
+
+ subscription.process()
+ self.assertEqual(len(subscription.invoices), 1)
+ self.assertEqual(subscription.status, 'Unpaid')
+
+ # Check the currency of the created invoice
+ currency = frappe.db.get_value('Sales Invoice', subscription.invoices[0].invoice, 'currency')
+ self.assertEqual(currency, 'USD')
\ 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 878ae09..563df79 100644
--- a/erpnext/accounts/doctype/subscription_plan/subscription_plan.json
+++ b/erpnext/accounts/doctype/subscription_plan/subscription_plan.json
@@ -75,7 +75,8 @@
"fieldname": "cost",
"fieldtype": "Currency",
"in_list_view": 1,
- "label": "Cost"
+ "label": "Cost",
+ "options": "currency"
},
{
"depends_on": "eval:doc.price_determination==\"Based On Price List\"",
@@ -147,7 +148,7 @@
}
],
"links": [],
- "modified": "2021-08-13 10:53:44.205774",
+ "modified": "2021-12-10 15:24:15.794477",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Subscription Plan",
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 dc1818a..5bb9b93 100644
--- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
+++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
@@ -95,7 +95,7 @@
frappe.throw(_('Tax Withholding Category {} against Company {} for Customer {} should have Cumulative Threshold value.')
.format(tax_withholding_category, inv.company, party))
- tax_amount, tax_deducted = get_tax_amount(
+ tax_amount, tax_deducted, tax_deducted_on_advances = get_tax_amount(
party_type, parties,
inv, tax_details,
posting_date, pan_no
@@ -106,7 +106,10 @@
else:
tax_row = get_tax_row_for_tcs(inv, tax_details, tax_amount, tax_deducted)
- return tax_row
+ if inv.doctype == 'Purchase Invoice':
+ return tax_row, tax_deducted_on_advances
+ else:
+ return tax_row
def get_tax_withholding_details(tax_withholding_category, posting_date, company):
tax_withholding = frappe.get_doc("Tax Withholding Category", tax_withholding_category)
@@ -194,6 +197,10 @@
advance_vouchers = get_advance_vouchers(parties, company=inv.company, from_date=tax_details.from_date,
to_date=tax_details.to_date, party_type=party_type)
taxable_vouchers = vouchers + advance_vouchers
+ tax_deducted_on_advances = 0
+
+ if inv.doctype == 'Purchase Invoice':
+ tax_deducted_on_advances = get_taxes_deducted_on_advances_allocated(inv, tax_details)
tax_deducted = 0
if taxable_vouchers:
@@ -223,7 +230,7 @@
if cint(tax_details.round_off_tax_amount):
tax_amount = round(tax_amount)
- return tax_amount, tax_deducted
+ return tax_amount, tax_deducted, tax_deducted_on_advances
def get_invoice_vouchers(parties, tax_details, company, party_type='Supplier'):
dr_or_cr = 'credit' if party_type == 'Supplier' else 'debit'
@@ -281,6 +288,29 @@
return frappe.get_all('GL Entry', filters=filters, distinct=1, pluck='voucher_no') or [""]
+def get_taxes_deducted_on_advances_allocated(inv, tax_details):
+ advances = [d.reference_name for d in inv.get('advances')]
+ tax_info = []
+
+ if advances:
+ pe = frappe.qb.DocType("Payment Entry").as_("pe")
+ at = frappe.qb.DocType("Advance Taxes and Charges").as_("at")
+
+ tax_info = frappe.qb.from_(at).inner_join(pe).on(
+ pe.name == at.parent
+ ).select(
+ at.parent, at.name, at.tax_amount, at.allocated_amount
+ ).where(
+ pe.tax_withholding_category == tax_details.get('tax_withholding_category')
+ ).where(
+ at.parent.isin(advances)
+ ).where(
+ at.account_head == tax_details.account_head
+ ).run(as_dict=True)
+
+ return tax_info
+
+
def get_deducted_tax(taxable_vouchers, tax_details):
# check if TDS / TCS account is already charged on taxable vouchers
filters = {
diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py
index 8ef7d7e..1836db6 100644
--- a/erpnext/accounts/general_ledger.py
+++ b/erpnext/accounts/general_ledger.py
@@ -73,8 +73,28 @@
flt(entry.debit_in_account_currency) - flt(entry.credit_in_account_currency)
entry.credit_in_account_currency = 0.0
+ update_net_values(entry)
+
return gl_map
+def update_net_values(entry):
+ # In some scenarios net value needs to be shown in the ledger
+ # This method updates net values as debit or credit
+ if entry.post_net_value and entry.debit and entry.credit:
+ if entry.debit > entry.credit:
+ entry.debit = entry.debit - entry.credit
+ entry.debit_in_account_currency = entry.debit_in_account_currency \
+ - entry.credit_in_account_currency
+ entry.credit = 0
+ entry.credit_in_account_currency = 0
+ else:
+ entry.credit = entry.credit - entry.debit
+ entry.credit_in_account_currency = entry.credit_in_account_currency \
+ - entry.debit_in_account_currency
+
+ entry.debit = 0
+ entry.debit_in_account_currency = 0
+
def merge_similar_entries(gl_map, precision=None):
merged_gl_map = []
accounting_dimensions = get_accounting_dimensions()
diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py
index e904768..6b4b43d 100644
--- a/erpnext/accounts/party.py
+++ b/erpnext/accounts/party.py
@@ -68,10 +68,12 @@
party_details["tax_category"] = get_address_tax_category(party.get("tax_category"),
party_address, shipping_address if party_type != "Supplier" else party_address)
- if not party_details.get("taxes_and_charges"):
- party_details["taxes_and_charges"] = set_taxes(party.name, party_type, posting_date, 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)
+ tax_template = set_taxes(party.name, party_type, posting_date, 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 tax_template:
+ party_details['taxes_and_charges'] = tax_template
if cint(fetch_payment_terms_template):
party_details["payment_terms_template"] = get_payment_terms_template(party.name, party_type, company)
diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
index 88bcdad..a990f23 100755
--- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
+++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
@@ -109,7 +109,11 @@
invoiced = 0.0,
paid = 0.0,
credit_note = 0.0,
- outstanding = 0.0
+ outstanding = 0.0,
+ invoiced_in_account_currency = 0.0,
+ paid_in_account_currency = 0.0,
+ credit_note_in_account_currency = 0.0,
+ outstanding_in_account_currency = 0.0
)
self.get_invoices(gle)
@@ -150,21 +154,28 @@
# gle_balance will be the total "debit - credit" for receivable type reports and
# and vice-versa for payable type reports
gle_balance = self.get_gle_balance(gle)
+ gle_balance_in_account_currency = self.get_gle_balance_in_account_currency(gle)
+
if gle_balance > 0:
if gle.voucher_type in ('Journal Entry', 'Payment Entry') and gle.against_voucher:
# debit against sales / purchase invoice
row.paid -= gle_balance
+ row.paid_in_account_currency -= gle_balance_in_account_currency
else:
# invoice
row.invoiced += gle_balance
+ row.invoiced_in_account_currency += gle_balance_in_account_currency
else:
# payment or credit note for receivables
if self.is_invoice(gle):
# stand alone debit / credit note
row.credit_note -= gle_balance
+ row.credit_note_in_account_currency -= gle_balance_in_account_currency
else:
# advance / unlinked payment or other adjustment
row.paid -= gle_balance
+ row.paid_in_account_currency -= gle_balance_in_account_currency
+
if gle.cost_center:
row.cost_center = str(gle.cost_center)
@@ -216,8 +227,13 @@
# as we can use this to filter out invoices without outstanding
for key, row in self.voucher_balance.items():
row.outstanding = flt(row.invoiced - row.paid - row.credit_note, self.currency_precision)
+ row.outstanding_in_account_currency = flt(row.invoiced_in_account_currency - row.paid_in_account_currency - \
+ row.credit_note_in_account_currency, self.currency_precision)
+
row.invoice_grand_total = row.invoiced
- if abs(row.outstanding) > 1.0/10 ** self.currency_precision:
+
+ if (abs(row.outstanding) > 1.0/10 ** self.currency_precision) and \
+ (abs(row.outstanding_in_account_currency) > 1.0/10 ** self.currency_precision):
# non-zero oustanding, we must consider this row
if self.is_invoice(row) and self.filters.based_on_payment_terms:
@@ -529,7 +545,9 @@
def set_ageing(self, row):
if self.filters.ageing_based_on == "Due Date":
- entry_date = row.due_date
+ # use posting date as a fallback for advances posted via journal and payment entry
+ # when ageing viewed by due date
+ entry_date = row.due_date or row.posting_date
elif self.filters.ageing_based_on == "Supplier Invoice Date":
entry_date = row.bill_date
else:
@@ -583,12 +601,14 @@
else:
select_fields = "debit, credit"
+ doc_currency_fields = "debit_in_account_currency, credit_in_account_currency"
+
remarks = ", remarks" if self.filters.get("show_remarks") else ""
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, {0} {remarks}
+ against_voucher_type, against_voucher, account_currency, {0}, {1} {remarks}
from
`tabGL Entry`
where
@@ -596,8 +616,8 @@
and is_cancelled = 0
and party_type=%s
and (party is not null and party != '')
- {1} {2} {3}"""
- .format(select_fields, date_condition, conditions, order_by, remarks=remarks), values, as_dict=True)
+ {2} {3} {4}"""
+ .format(select_fields, doc_currency_fields, date_condition, conditions, order_by, remarks=remarks), values, as_dict=True)
def get_sales_invoices_or_customers_based_on_sales_person(self):
if self.filters.get("sales_person"):
@@ -718,6 +738,13 @@
# get the balance of the GL (debit - credit) or reverse balance based on report type
return gle.get(self.dr_or_cr) - self.get_reverse_balance(gle)
+ def get_gle_balance_in_account_currency(self, gle):
+ # get the balance of the GL (debit - credit) or reverse balance based on report type
+ return gle.get(self.dr_or_cr + '_in_account_currency') - self.get_reverse_balance_in_account_currency(gle)
+
+ def get_reverse_balance_in_account_currency(self, gle):
+ return gle.get('debit_in_account_currency' if self.dr_or_cr=='credit' else 'credit_in_account_currency')
+
def get_reverse_balance(self, gle):
# get "credit" balance if report type is "debit" and vice versa
return gle.get('debit' if self.dr_or_cr=='credit' else 'credit')
diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js
index 305cddb..715cd64 100644
--- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js
+++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js
@@ -117,6 +117,11 @@
"label": __("Show Future Payments"),
"fieldtype": "Check",
},
+ {
+ "fieldname":"show_gl_balance",
+ "label": __("Show GL Balance"),
+ "fieldtype": "Check",
+ },
],
onload: function(report) {
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 3c94629..4559fa9 100644
--- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py
+++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py
@@ -4,7 +4,8 @@
import frappe
from frappe import _, scrub
-from frappe.utils import cint
+from frappe.utils import cint, flt
+from six import iteritems
from erpnext.accounts.party import get_partywise_advanced_payment_amount
from erpnext.accounts.report.accounts_receivable.accounts_receivable import ReceivablePayableReport
@@ -36,7 +37,10 @@
party_advance_amount = get_partywise_advanced_payment_amount(self.party_type,
self.filters.report_date, self.filters.show_future_payments, self.filters.company) or {}
- for party, party_dict in self.party_total.items():
+ if self.filters.show_gl_balance:
+ gl_balance_map = get_gl_balance(self.filters.report_date)
+
+ for party, party_dict in iteritems(self.party_total):
if party_dict.outstanding == 0:
continue
@@ -55,6 +59,10 @@
# but in summary report advance shown in separate column
row.paid -= row.advance
+ if self.filters.show_gl_balance:
+ row.gl_balance = gl_balance_map.get(party)
+ row.diff = flt(row.outstanding) - flt(row.gl_balance)
+
self.data.append(row)
def get_party_total(self, args):
@@ -114,6 +122,10 @@
self.add_column(_(credit_debit_label), fieldname='credit_note')
self.add_column(_('Outstanding Amount'), fieldname='outstanding')
+ if self.filters.show_gl_balance:
+ self.add_column(_('GL Balance'), fieldname='gl_balance')
+ self.add_column(_('Difference'), fieldname='diff')
+
self.setup_ageing_columns()
if self.party_type == "Customer":
@@ -140,3 +152,7 @@
# Add column for total due amount
self.add_column(label="Total Amount Due", fieldname='total_due')
+
+def get_gl_balance(report_date):
+ return frappe._dict(frappe.db.get_all("GL Entry", fields=['party', 'sum(debit - credit)'],
+ filters={'posting_date': ("<=", report_date), 'is_cancelled': 0}, group_by='party', as_list=1))
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 e24a5f9..d3e836a 100644
--- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js
+++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js
@@ -92,6 +92,11 @@
"label": __("Include Default Book Entries"),
"fieldtype": "Check",
"default": 1
+ },
+ {
+ "fieldname": "show_zero_values",
+ "label": __("Show zero values"),
+ "fieldtype": "Check"
}
],
"formatter": function(value, row, column, data, default_formatter) {
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 c71bc17..758e3e9 100644
--- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
+++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
@@ -22,7 +22,11 @@
get_cash_flow_accounts,
)
from erpnext.accounts.report.cash_flow.cash_flow import get_report_summary as get_cash_flow_summary
-from erpnext.accounts.report.financial_statements import get_fiscal_year_data, sort_accounts
+from erpnext.accounts.report.financial_statements import (
+ filter_out_zero_value_rows,
+ get_fiscal_year_data,
+ sort_accounts,
+)
from erpnext.accounts.report.profit_and_loss_statement.profit_and_loss_statement import (
get_chart_data as get_pl_chart_data,
)
@@ -265,7 +269,7 @@
return columns
def get_data(companies, root_type, balance_must_be, fiscal_year, filters=None, ignore_closing_entries=False):
- accounts, accounts_by_name = get_account_heads(root_type,
+ accounts, accounts_by_name, parent_children_map = get_account_heads(root_type,
companies, filters)
if not accounts: return []
@@ -294,6 +298,8 @@
out = prepare_data(accounts, start_date, end_date, balance_must_be, companies, company_currency, filters)
+ out = filter_out_zero_value_rows(out, parent_children_map, show_zero_values=filters.get("show_zero_values"))
+
if out:
add_total_row(out, root_type, balance_must_be, companies, company_currency)
@@ -364,13 +370,13 @@
accounts = get_accounts(root_type, filters)
if not accounts:
- return None, None
+ return None, None, None
accounts = update_parent_account_names(accounts)
accounts, accounts_by_name, parent_children_map = filter_accounts(accounts)
- return accounts, accounts_by_name
+ return accounts, accounts_by_name, parent_children_map
def update_parent_account_names(accounts):
"""Update parent_account_name in accounts list.
diff --git a/erpnext/agriculture/doctype/detected_disease/__init__.py b/erpnext/accounts/report/deferred_revenue_and_expense/__init__.py
similarity index 100%
rename from erpnext/agriculture/doctype/detected_disease/__init__.py
rename to erpnext/accounts/report/deferred_revenue_and_expense/__init__.py
diff --git a/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.js b/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.js
new file mode 100644
index 0000000..0056b9e
--- /dev/null
+++ b/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.js
@@ -0,0 +1,114 @@
+// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+/* eslint-disable */
+
+function get_filters() {
+ let filters = [
+ {
+ "fieldname":"company",
+ "label": __("Company"),
+ "fieldtype": "Link",
+ "options": "Company",
+ "default": frappe.defaults.get_user_default("Company"),
+ "reqd": 1
+ },
+ {
+ "fieldname":"filter_based_on",
+ "label": __("Filter Based On"),
+ "fieldtype": "Select",
+ "options": ["Fiscal Year", "Date Range"],
+ "default": ["Fiscal Year"],
+ "reqd": 1,
+ on_change: function() {
+ let filter_based_on = frappe.query_report.get_filter_value('filter_based_on');
+ frappe.query_report.toggle_filter_display('from_fiscal_year', filter_based_on === 'Date Range');
+ frappe.query_report.toggle_filter_display('to_fiscal_year', filter_based_on === 'Date Range');
+ frappe.query_report.toggle_filter_display('period_start_date', filter_based_on === 'Fiscal Year');
+ frappe.query_report.toggle_filter_display('period_end_date', filter_based_on === 'Fiscal Year');
+
+ frappe.query_report.refresh();
+ }
+ },
+ {
+ "fieldname":"period_start_date",
+ "label": __("Start Date"),
+ "fieldtype": "Date",
+ "hidden": 1,
+ "reqd": 1
+ },
+ {
+ "fieldname":"period_end_date",
+ "label": __("End Date"),
+ "fieldtype": "Date",
+ "hidden": 1,
+ "reqd": 1
+ },
+ {
+ "fieldname":"from_fiscal_year",
+ "label": __("Start Year"),
+ "fieldtype": "Link",
+ "options": "Fiscal Year",
+ "default": frappe.defaults.get_user_default("fiscal_year"),
+ "reqd": 1
+ },
+ {
+ "fieldname":"to_fiscal_year",
+ "label": __("End Year"),
+ "fieldtype": "Link",
+ "options": "Fiscal Year",
+ "default": frappe.defaults.get_user_default("fiscal_year"),
+ "reqd": 1
+ },
+ {
+ "fieldname": "periodicity",
+ "label": __("Periodicity"),
+ "fieldtype": "Select",
+ "options": [
+ { "value": "Monthly", "label": __("Monthly") },
+ { "value": "Quarterly", "label": __("Quarterly") },
+ { "value": "Half-Yearly", "label": __("Half-Yearly") },
+ { "value": "Yearly", "label": __("Yearly") }
+ ],
+ "default": "Monthly",
+ "reqd": 1
+ },
+ {
+ "fieldname": "type",
+ "label": __("Invoice Type"),
+ "fieldtype": "Select",
+ "options": [
+ { "value": "Revenue", "label": __("Revenue") },
+ { "value": "Expense", "label": __("Expense") }
+ ],
+ "default": "Revenue",
+ "reqd": 1
+ },
+ {
+ "fieldname" : "with_upcoming_postings",
+ "label": __("Show with upcoming revenue/expense"),
+ "fieldtype": "Check",
+ "default": 1
+ }
+ ]
+
+ return filters;
+}
+
+frappe.query_reports["Deferred Revenue and Expense"] = {
+ "filters": get_filters(),
+ "formatter": function(value, row, column, data, default_formatter){
+ return default_formatter(value, row, column, data);
+ },
+ onload: function(report){
+ let fiscal_year = frappe.defaults.get_user_default("fiscal_year");
+
+ frappe.model.with_doc("Fiscal Year", fiscal_year, function(r) {
+ var fy = frappe.model.get_doc("Fiscal Year", fiscal_year);
+ frappe.query_report.set_filter_value({
+ period_start_date: fy.year_start_date,
+ period_end_date: fy.year_end_date
+ });
+ });
+ }
+};
+
diff --git a/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.json b/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.json
new file mode 100644
index 0000000..c7dfb3b
--- /dev/null
+++ b/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.json
@@ -0,0 +1,32 @@
+{
+ "add_total_row": 0,
+ "columns": [],
+ "creation": "2021-12-10 19:27:14.654220",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "modified": "2021-12-10 19:27:14.654220",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Deferred Revenue and Expense",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "GL Entry",
+ "report_name": "Deferred Revenue and Expense",
+ "report_type": "Script Report",
+ "roles": [
+ {
+ "role": "Accounts User"
+ },
+ {
+ "role": "Accounts Manager"
+ },
+ {
+ "role": "Auditor"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py b/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py
new file mode 100644
index 0000000..3a51db8
--- /dev/null
+++ b/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py
@@ -0,0 +1,441 @@
+# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
+# License: MIT. See LICENSE
+
+import frappe
+from frappe import _, qb
+from frappe.query_builder import Column, functions
+from frappe.utils import add_days, date_diff, flt, get_first_day, get_last_day, rounded
+
+from erpnext.accounts.report.financial_statements import get_period_list
+
+
+class Deferred_Item(object):
+ """
+ Helper class for processing items with deferred revenue/expense
+ """
+
+ def __init__(self, item, inv, gle_entries):
+ self.name = item
+ self.parent = inv.name
+ self.item_name = gle_entries[0].item_name
+ self.service_start_date = gle_entries[0].service_start_date
+ self.service_end_date = gle_entries[0].service_end_date
+ self.base_net_amount = gle_entries[0].base_net_amount
+ self.filters = inv.filters
+ self.period_list = inv.period_list
+
+ if gle_entries[0].deferred_revenue_account:
+ self.type = "Deferred Sale Item"
+ self.deferred_account = gle_entries[0].deferred_revenue_account
+ elif gle_entries[0].deferred_expense_account:
+ self.type = "Deferred Purchase Item"
+ self.deferred_account = gle_entries[0].deferred_expense_account
+
+ self.gle_entries = []
+ # holds period wise total for item
+ self.period_total = []
+ self.last_entry_date = self.service_start_date
+
+ if gle_entries:
+ self.gle_entries = gle_entries
+ for x in self.gle_entries:
+ if self.get_amount(x):
+ self.last_entry_date = x.gle_posting_date
+
+ def report_data(self):
+ """
+ Generate report data for output
+ """
+ ret_data = frappe._dict({"name": self.item_name})
+ for period in self.period_total:
+ ret_data[period.key] = period.total
+ ret_data.indent = 1
+ return ret_data
+
+ def get_amount(self, entry):
+ """
+ For a given GL/Journal posting, get balance based on item type
+ """
+ if self.type == "Deferred Sale Item":
+ return entry.debit - entry.credit
+ elif self.type == "Deferred Purchase Item":
+ return -(entry.credit - entry.debit)
+ return 0
+
+ def get_item_total(self):
+ """
+ Helper method - calculate booked amount. Includes simulated postings as well
+ """
+ total = 0
+ for gle_posting in self.gle_entries:
+ total += self.get_amount(gle_posting)
+
+ return total
+
+ def calculate_amount(self, start_date, end_date):
+ """
+ start_date, end_date - datetime.datetime.date
+ return - estimated amount to post for given period
+ Calculated based on already booked amount and item service period
+ """
+ total_months = (
+ (self.service_end_date.year - self.service_start_date.year) * 12
+ + (self.service_end_date.month - self.service_start_date.month)
+ + 1
+ )
+
+ prorate = date_diff(self.service_end_date, self.service_start_date) / date_diff(
+ get_last_day(self.service_end_date), get_first_day(self.service_start_date)
+ )
+
+ actual_months = rounded(total_months * prorate, 1)
+
+ already_booked_amount = self.get_item_total()
+ base_amount = self.base_net_amount / actual_months
+
+ if base_amount + already_booked_amount > self.base_net_amount:
+ base_amount = self.base_net_amount - already_booked_amount
+
+ if not (get_first_day(start_date) == start_date and get_last_day(end_date) == end_date):
+ partial_month = flt(date_diff(end_date, start_date)) / flt(
+ date_diff(get_last_day(end_date), get_first_day(start_date))
+ )
+ base_amount *= rounded(partial_month, 1)
+
+ return base_amount
+
+ def make_dummy_gle(self, name, date, amount):
+ """
+ return - frappe._dict() of a dummy gle entry
+ """
+ entry = frappe._dict(
+ {"name": name, "gle_posting_date": date, "debit": 0, "credit": 0, "posted": "not"}
+ )
+ if self.type == "Deferred Sale Item":
+ entry.debit = amount
+ elif self.type == "Deferred Purchase Item":
+ entry.credit = amount
+ return entry
+
+ def simulate_future_posting(self):
+ """
+ simulate future posting by creating dummy gl entries. starts from the last posting date.
+ """
+ if self.service_start_date != self.service_end_date:
+ if add_days(self.last_entry_date, 1) < self.period_list[-1].to_date:
+ self.estimate_for_period_list = get_period_list(
+ self.filters.from_fiscal_year,
+ self.filters.to_fiscal_year,
+ add_days(self.last_entry_date, 1),
+ self.period_list[-1].to_date,
+ "Date Range",
+ "Monthly",
+ company=self.filters.company,
+ )
+ for period in self.estimate_for_period_list:
+ amount = self.calculate_amount(period.from_date, period.to_date)
+ gle = self.make_dummy_gle(period.key, period.to_date, amount)
+ self.gle_entries.append(gle)
+
+ def calculate_item_revenue_expense_for_period(self):
+ """
+ calculate item postings for each period and update period_total list
+ """
+ for period in self.period_list:
+ period_sum = 0
+ actual = 0
+ for posting in self.gle_entries:
+ # if period.from_date <= posting.posting_date <= period.to_date:
+ if period.from_date <= posting.gle_posting_date <= period.to_date:
+ period_sum += self.get_amount(posting)
+ if posting.posted == "posted":
+ actual += self.get_amount(posting)
+
+ self.period_total.append(
+ frappe._dict({"key": period.key, "total": period_sum, "actual": actual})
+ )
+ return self.period_total
+
+
+class Deferred_Invoice(object):
+ def __init__(self, invoice, items, filters, period_list):
+ """
+ Helper class for processing invoices with deferred revenue/expense items
+ invoice - string : invoice name
+ items - list : frappe._dict() with item details. Refer Deferred_Item for required fields
+ """
+ self.name = invoice
+ self.posting_date = items[0].posting_date
+ self.filters = filters
+ self.period_list = period_list
+ # holds period wise total for invoice
+ self.period_total = []
+
+ if items[0].deferred_revenue_account:
+ self.type = "Sales"
+ elif items[0].deferred_expense_account:
+ self.type = "Purchase"
+
+ self.items = []
+ # for each uniq items
+ self.uniq_items = set([x.item for x in items])
+ for item in self.uniq_items:
+ self.items.append(Deferred_Item(item, self, [x for x in items if x.item == item]))
+
+ def calculate_invoice_revenue_expense_for_period(self):
+ """
+ calculate deferred revenue/expense for all items in invoice
+ """
+ # initialize period_total list for invoice
+ for period in self.period_list:
+ self.period_total.append(frappe._dict({"key": period.key, "total": 0, "actual": 0}))
+
+ for item in self.items:
+ item_total = item.calculate_item_revenue_expense_for_period()
+ # update invoice total
+ for idx, period in enumerate(self.period_list, 0):
+ self.period_total[idx].total += item_total[idx].total
+ self.period_total[idx].actual += item_total[idx].actual
+ return self.period_total
+
+ def estimate_future(self):
+ """
+ create dummy GL entries for upcoming months for all items in invoice
+ """
+ [item.simulate_future_posting() for item in self.items]
+
+ def report_data(self):
+ """
+ generate report data for invoice, includes invoice total
+ """
+ ret_data = []
+ inv_total = frappe._dict({"name": self.name})
+ for x in self.period_total:
+ inv_total[x.key] = x.total
+ inv_total.indent = 0
+ ret_data.append(inv_total)
+ list(map(lambda item: ret_data.append(item.report_data()), self.items))
+ return ret_data
+
+
+class Deferred_Revenue_and_Expense_Report(object):
+ def __init__(self, filters=None):
+ """
+ Initialize deferred revenue/expense report with user provided filters or system defaults, if none is provided
+ """
+
+ # If no filters are provided, get user defaults
+ if not filters:
+ fiscal_year = frappe.get_doc("Fiscal Year", frappe.defaults.get_user_default("fiscal_year"))
+ self.filters = frappe._dict(
+ {
+ "company": frappe.defaults.get_user_default("Company"),
+ "filter_based_on": "Fiscal Year",
+ "period_start_date": fiscal_year.year_start_date,
+ "period_end_date": fiscal_year.year_end_date,
+ "from_fiscal_year": fiscal_year.year,
+ "to_fiscal_year": fiscal_year.year,
+ "periodicity": "Monthly",
+ "type": "Revenue",
+ "with_upcoming_postings": True,
+ }
+ )
+ else:
+ self.filters = frappe._dict(filters)
+
+ self.period_list = None
+ self.deferred_invoices = []
+ # holds period wise total for report
+ self.period_total = []
+
+ def get_period_list(self):
+ """
+ Figure out selected period based on filters
+ """
+ self.period_list = get_period_list(
+ self.filters.from_fiscal_year,
+ self.filters.to_fiscal_year,
+ self.filters.period_start_date,
+ self.filters.period_end_date,
+ self.filters.filter_based_on,
+ self.filters.periodicity,
+ company=self.filters.company,
+ )
+
+ def get_invoices(self):
+ """
+ Get all sales and purchase invoices which has deferred revenue/expense items
+ """
+ gle = qb.DocType("GL Entry")
+ # column doesn't have an alias option
+ posted = Column("posted")
+
+ if self.filters.type == "Revenue":
+ inv = qb.DocType("Sales Invoice")
+ inv_item = qb.DocType("Sales Invoice Item")
+ deferred_flag_field = inv_item["enable_deferred_revenue"]
+ deferred_account_field = inv_item["deferred_revenue_account"]
+
+ elif self.filters.type == "Expense":
+ inv = qb.DocType("Purchase Invoice")
+ inv_item = qb.DocType("Purchase Invoice Item")
+ deferred_flag_field = inv_item["enable_deferred_expense"]
+ deferred_account_field = inv_item["deferred_expense_account"]
+
+ query = (
+ qb.from_(inv_item)
+ .join(inv)
+ .on(inv.name == inv_item.parent)
+ .join(gle)
+ .on((inv_item.name == gle.voucher_detail_no) & (deferred_account_field == gle.account))
+ .select(
+ inv.name.as_("doc"),
+ inv.posting_date,
+ inv_item.name.as_("item"),
+ inv_item.item_name,
+ inv_item.service_start_date,
+ inv_item.service_end_date,
+ inv_item.base_net_amount,
+ deferred_account_field,
+ gle.posting_date.as_("gle_posting_date"),
+ functions.Sum(gle.debit).as_("debit"),
+ functions.Sum(gle.credit).as_("credit"),
+ posted,
+ )
+ .where(
+ (inv.docstatus == 1)
+ & (deferred_flag_field == 1)
+ & (
+ (
+ (self.period_list[0].from_date >= inv_item.service_start_date)
+ & (inv_item.service_end_date >= self.period_list[0].from_date)
+ )
+ | (
+ (inv_item.service_start_date >= self.period_list[0].from_date)
+ & (inv_item.service_start_date <= self.period_list[-1].to_date)
+ )
+ )
+ )
+ .groupby(inv.name, inv_item.name, gle.posting_date)
+ .orderby(gle.posting_date)
+ )
+ self.invoices = query.run(as_dict=True)
+
+ uniq_invoice = set([x.doc for x in self.invoices])
+ for inv in uniq_invoice:
+ self.deferred_invoices.append(
+ Deferred_Invoice(
+ inv, [x for x in self.invoices if x.doc == inv], self.filters, self.period_list
+ )
+ )
+
+ def estimate_future(self):
+ """
+ For all Invoices estimate upcoming postings
+ """
+ for x in self.deferred_invoices:
+ x.estimate_future()
+
+ def calculate_revenue_and_expense(self):
+ """
+ calculate the deferred revenue/expense for all invoices
+ """
+ # initialize period_total list for report
+ for period in self.period_list:
+ self.period_total.append(frappe._dict({"key": period.key, "total": 0, "actual": 0}))
+
+ for inv in self.deferred_invoices:
+ inv_total = inv.calculate_invoice_revenue_expense_for_period()
+ # calculate total for whole report
+ for idx, period in enumerate(self.period_list, 0):
+ self.period_total[idx].total += inv_total[idx].total
+ self.period_total[idx].actual += inv_total[idx].actual
+
+ def get_columns(self):
+ columns = []
+ columns.append({"label": _("Name"), "fieldname": "name", "fieldtype": "Data", "read_only": 1})
+ for period in self.period_list:
+ columns.append(
+ {
+ "label": _(period.label),
+ "fieldname": period.key,
+ "fieldtype": "Currency",
+ "read_only": 1,
+ })
+ return columns
+
+ def generate_report_data(self):
+ """
+ Generate report data for all invoices. Adds total rows for revenue and expense
+ """
+ ret = []
+
+ for inv in self.deferred_invoices:
+ ret += inv.report_data()
+
+ # empty row for padding
+ ret += [{}]
+
+ # add total row
+ if ret is not []:
+ if self.filters.type == "Revenue":
+ total_row = frappe._dict({"name": "Total Deferred Income"})
+ elif self.filters.type == "Expense":
+ total_row = frappe._dict({"name": "Total Deferred Expense"})
+
+ for idx, period in enumerate(self.period_list, 0):
+ total_row[period.key] = self.period_total[idx].total
+ ret.append(total_row)
+
+ return ret
+
+ def prepare_chart(self):
+ chart = {
+ "data": {
+ "labels": [period.label for period in self.period_list],
+ "datasets": [
+ {
+ "name": "Actual Posting",
+ "chartType": "bar",
+ "values": [x.actual for x in self.period_total],
+ }
+ ],
+ },
+ "type": "axis-mixed",
+ "height": 500,
+ "axisOptions": {"xAxisMode": "Tick", "xIsSeries": True},
+ "barOptions": {"stacked": False, "spaceRatio": 0.5},
+ }
+
+ if self.filters.with_upcoming_postings:
+ chart["data"]["datasets"].append({
+ "name": "Expected",
+ "chartType": "line",
+ "values": [x.total for x in self.period_total]
+ })
+
+ return chart
+
+ def run(self, *args, **kwargs):
+ """
+ Run report and generate data
+ """
+ self.deferred_invoices.clear()
+ self.get_period_list()
+ self.get_invoices()
+
+ if self.filters.with_upcoming_postings:
+ self.estimate_future()
+ self.calculate_revenue_and_expense()
+
+
+def execute(filters=None):
+ report = Deferred_Revenue_and_Expense_Report(filters=filters)
+ report.run()
+
+ columns = report.get_columns()
+ data = report.generate_report_data()
+ message = []
+ chart = report.prepare_chart()
+
+ return columns, data, message, chart
diff --git a/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py b/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py
new file mode 100644
index 0000000..1de6fb6
--- /dev/null
+++ b/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py
@@ -0,0 +1,253 @@
+import unittest
+
+import frappe
+from frappe import qb
+from frappe.utils import nowdate
+
+from erpnext.accounts.doctype.account.test_account import create_account
+from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
+from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
+from erpnext.accounts.report.deferred_revenue_and_expense.deferred_revenue_and_expense import (
+ Deferred_Revenue_and_Expense_Report,
+)
+from erpnext.buying.doctype.supplier.test_supplier import create_supplier
+from erpnext.stock.doctype.item.test_item import create_item
+
+
+class TestDeferredRevenueAndExpense(unittest.TestCase):
+ @classmethod
+ def setUpClass(self):
+ clear_old_entries()
+ create_company()
+
+ def test_deferred_revenue(self):
+ # created deferred expense accounts, if not found
+ deferred_revenue_account = create_account(
+ account_name="Deferred Revenue",
+ parent_account="Current Liabilities - _CD",
+ company="_Test Company DR",
+ )
+
+ acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings")
+ acc_settings.book_deferred_entries_based_on = "Months"
+ acc_settings.save()
+
+ customer = frappe.new_doc("Customer")
+ customer.customer_name = "_Test Customer DR"
+ customer.type = "Individual"
+ customer.insert()
+
+ item = create_item(
+ "_Test Internet Subscription",
+ is_stock_item=0,
+ warehouse="All Warehouses - _CD",
+ company="_Test Company DR",
+ )
+ item.enable_deferred_revenue = 1
+ item.deferred_revenue_account = deferred_revenue_account
+ item.no_of_months = 3
+ item.save()
+
+ si = create_sales_invoice(
+ item=item.name,
+ company="_Test Company DR",
+ customer="_Test Customer DR",
+ debit_to="Debtors - _CD",
+ posting_date="2021-05-01",
+ parent_cost_center="Main - _CD",
+ cost_center="Main - _CD",
+ do_not_submit=True,
+ rate=300,
+ price_list_rate=300,
+ )
+ si.items[0].enable_deferred_revenue = 1
+ si.items[0].service_start_date = "2021-05-01"
+ si.items[0].service_end_date = "2021-08-01"
+ si.items[0].deferred_revenue_account = deferred_revenue_account
+ si.items[0].income_account = "Sales - _CD"
+ si.save()
+ si.submit()
+
+ pda = frappe.get_doc(
+ dict(
+ doctype="Process Deferred Accounting",
+ posting_date=nowdate(),
+ start_date="2021-05-01",
+ end_date="2021-08-01",
+ type="Income",
+ company="_Test Company DR",
+ )
+ )
+ pda.insert()
+ pda.submit()
+
+ # execute report
+ fiscal_year = frappe.get_doc("Fiscal Year", frappe.defaults.get_user_default("fiscal_year"))
+ self.filters = frappe._dict(
+ {
+ "company": frappe.defaults.get_user_default("Company"),
+ "filter_based_on": "Date Range",
+ "period_start_date": "2021-05-01",
+ "period_end_date": "2021-08-01",
+ "from_fiscal_year": fiscal_year.year,
+ "to_fiscal_year": fiscal_year.year,
+ "periodicity": "Monthly",
+ "type": "Revenue",
+ "with_upcoming_postings": False,
+ }
+ )
+
+ report = Deferred_Revenue_and_Expense_Report(filters=self.filters)
+ report.run()
+ expected = [
+ {"key": "may_2021", "total": 100.0, "actual": 100.0},
+ {"key": "jun_2021", "total": 100.0, "actual": 100.0},
+ {"key": "jul_2021", "total": 100.0, "actual": 100.0},
+ {"key": "aug_2021", "total": 0, "actual": 0},
+ ]
+ self.assertEqual(report.period_total, expected)
+
+ def test_deferred_expense(self):
+ # created deferred expense accounts, if not found
+ deferred_expense_account = create_account(
+ account_name="Deferred Expense",
+ parent_account="Current Assets - _CD",
+ company="_Test Company DR",
+ )
+
+ acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings")
+ acc_settings.book_deferred_entries_based_on = "Months"
+ acc_settings.save()
+
+ supplier = create_supplier(
+ supplier_name="_Test Furniture Supplier", supplier_group="Local", supplier_type="Company"
+ )
+ supplier.save()
+
+ item = create_item(
+ "_Test Office Desk",
+ is_stock_item=0,
+ warehouse="All Warehouses - _CD",
+ company="_Test Company DR",
+ )
+ item.enable_deferred_expense = 1
+ item.deferred_expense_account = deferred_expense_account
+ item.no_of_months_exp = 3
+ item.save()
+
+ pi = make_purchase_invoice(
+ item=item.name,
+ company="_Test Company DR",
+ supplier="_Test Furniture Supplier",
+ is_return=False,
+ update_stock=False,
+ posting_date=frappe.utils.datetime.date(2021, 5, 1),
+ parent_cost_center="Main - _CD",
+ cost_center="Main - _CD",
+ do_not_save=True,
+ rate=300,
+ price_list_rate=300,
+ warehouse="All Warehouses - _CD",
+ qty=1,
+ )
+ pi.set_posting_time = True
+ pi.items[0].enable_deferred_expense = 1
+ pi.items[0].service_start_date = "2021-05-01"
+ pi.items[0].service_end_date = "2021-08-01"
+ pi.items[0].deferred_expense_account = deferred_expense_account
+ pi.items[0].expense_account = "Office Maintenance Expenses - _CD"
+ pi.save()
+ pi.submit()
+
+ pda = frappe.get_doc(
+ dict(
+ doctype="Process Deferred Accounting",
+ posting_date=nowdate(),
+ start_date="2021-05-01",
+ end_date="2021-08-01",
+ type="Expense",
+ company="_Test Company DR",
+ )
+ )
+ pda.insert()
+ pda.submit()
+
+ # execute report
+ fiscal_year = frappe.get_doc("Fiscal Year", frappe.defaults.get_user_default("fiscal_year"))
+ self.filters = frappe._dict(
+ {
+ "company": frappe.defaults.get_user_default("Company"),
+ "filter_based_on": "Date Range",
+ "period_start_date": "2021-05-01",
+ "period_end_date": "2021-08-01",
+ "from_fiscal_year": fiscal_year.year,
+ "to_fiscal_year": fiscal_year.year,
+ "periodicity": "Monthly",
+ "type": "Expense",
+ "with_upcoming_postings": False,
+ }
+ )
+
+ report = Deferred_Revenue_and_Expense_Report(filters=self.filters)
+ report.run()
+ expected = [
+ {"key": "may_2021", "total": -100.0, "actual": -100.0},
+ {"key": "jun_2021", "total": -100.0, "actual": -100.0},
+ {"key": "jul_2021", "total": -100.0, "actual": -100.0},
+ {"key": "aug_2021", "total": 0, "actual": 0},
+ ]
+ self.assertEqual(report.period_total, expected)
+
+
+def create_company():
+ company = frappe.db.exists("Company", "_Test Company DR")
+ if not company:
+ company = frappe.new_doc("Company")
+ company.company_name = "_Test Company DR"
+ company.default_currency = "INR"
+ company.chart_of_accounts = "Standard"
+ company.insert()
+
+
+def clear_old_entries():
+ item = qb.DocType("Item")
+ account = qb.DocType("Account")
+ customer = qb.DocType("Customer")
+ supplier = qb.DocType("Supplier")
+ sinv = qb.DocType("Sales Invoice")
+ sinv_item = qb.DocType("Sales Invoice Item")
+ pinv = qb.DocType("Purchase Invoice")
+ pinv_item = qb.DocType("Purchase Invoice Item")
+
+ qb.from_(account).delete().where(
+ (account.account_name == "Deferred Revenue")
+ | (account.account_name == "Deferred Expense") & (account.company == "_Test Company DR")
+ ).run()
+ qb.from_(item).delete().where(
+ (item.item_code == "_Test Internet Subscription") | (item.item_code == "_Test Office Rent")
+ ).run()
+ qb.from_(customer).delete().where(customer.customer_name == "_Test Customer DR").run()
+ qb.from_(supplier).delete().where(supplier.supplier_name == "_Test Furniture Supplier").run()
+
+ # delete existing invoices with deferred items
+ deferred_invoices = (
+ qb.from_(sinv)
+ .join(sinv_item)
+ .on(sinv.name == sinv_item.parent)
+ .select(sinv.name)
+ .where(sinv_item.enable_deferred_revenue == 1)
+ .run()
+ )
+ if deferred_invoices:
+ qb.from_(sinv).delete().where(sinv.name.isin(deferred_invoices)).run()
+
+ deferred_invoices = (
+ qb.from_(pinv)
+ .join(pinv_item)
+ .on(pinv.name == pinv_item.parent)
+ .select(pinv.name)
+ .where(pinv_item.enable_deferred_expense == 1)
+ .run()
+ )
+ if deferred_invoices:
+ qb.from_(pinv).delete().where(pinv.name.isin(deferred_invoices)).run()
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.js b/erpnext/accounts/report/general_ledger/general_ledger.js
index b296876..010284c 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.js
+++ b/erpnext/accounts/report/general_ledger/general_ledger.js
@@ -167,7 +167,7 @@
"fieldname": "include_dimensions",
"label": __("Consider Accounting Dimensions"),
"fieldtype": "Check",
- "default": 0
+ "default": 1
},
{
"fieldname": "show_opening_entries",
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py
index 385c8b2..7f27920 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.py
+++ b/erpnext/accounts/report/general_ledger/general_ledger.py
@@ -448,9 +448,11 @@
elif group_by_voucher_consolidated:
keylist = [gle.get("voucher_type"), gle.get("voucher_no"), gle.get("account")]
- for dim in accounting_dimensions:
- keylist.append(gle.get(dim))
- keylist.append(gle.get("cost_center"))
+ if filters.get("include_dimensions"):
+ for dim in accounting_dimensions:
+ keylist.append(gle.get(dim))
+ keylist.append(gle.get("cost_center"))
+
key = tuple(keylist)
if key not in consolidated_gle:
consolidated_gle.setdefault(key, gle)
@@ -547,10 +549,7 @@
"fieldname": "balance",
"fieldtype": "Float",
"width": 130
- }
- ]
-
- columns.extend([
+ },
{
"label": _("Voucher Type"),
"fieldname": "voucher_type",
@@ -584,7 +583,7 @@
"fieldname": "project",
"width": 100
}
- ])
+ ]
if filters.get("include_dimensions"):
for dim in get_accounting_dimensions(as_list = False):
@@ -594,14 +593,14 @@
"fieldname": dim.fieldname,
"width": 100
})
-
- columns.extend([
- {
+ columns.append({
"label": _("Cost Center"),
"options": "Cost Center",
"fieldname": "cost_center",
"width": 100
- },
+ })
+
+ columns.extend([
{
"label": _("Against Voucher Type"),
"fieldname": "against_voucher_type",
diff --git a/erpnext/accounts/report/gross_profit/gross_profit.js b/erpnext/accounts/report/gross_profit/gross_profit.js
index 856b97d..685f2d6 100644
--- a/erpnext/accounts/report/gross_profit/gross_profit.js
+++ b/erpnext/accounts/report/gross_profit/gross_profit.js
@@ -44,7 +44,7 @@
"formatter": function(value, row, column, data, default_formatter) {
value = default_formatter(value, row, column, data);
- if (data && data.indent == 0.0) {
+ if (data && (data.indent == 0.0 || row[1].content == "Total")) {
value = $(`<span>${value}</span>`);
var $value = $(value).css("font-weight", "bold");
value = $value.wrap("<p></p>").parent().html();
diff --git a/erpnext/accounts/report/gross_profit/gross_profit.json b/erpnext/accounts/report/gross_profit/gross_profit.json
index 5fff3fd..76c560a 100644
--- a/erpnext/accounts/report/gross_profit/gross_profit.json
+++ b/erpnext/accounts/report/gross_profit/gross_profit.json
@@ -9,7 +9,7 @@
"filters": [],
"idx": 3,
"is_standard": "Yes",
- "modified": "2021-08-19 18:57:07.468202",
+ "modified": "2021-11-13 19:14:23.730198",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Gross Profit",
diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py
index 9d5a242..84effc0 100644
--- a/erpnext/accounts/report/gross_profit/gross_profit.py
+++ b/erpnext/accounts/report/gross_profit/gross_profit.py
@@ -19,7 +19,7 @@
data = []
group_wise_columns = frappe._dict({
- "invoice": ["parent", "customer", "customer_group", "posting_date","item_code", "item_name","item_group", "brand", "description", \
+ "invoice": ["invoice_or_item", "customer", "customer_group", "posting_date","item_code", "item_name","item_group", "brand", "description",
"warehouse", "qty", "base_rate", "buying_rate", "base_amount",
"buying_amount", "gross_profit", "gross_profit_percent", "project"],
"item_code": ["item_code", "item_name", "brand", "description", "qty", "base_rate",
@@ -77,13 +77,15 @@
row.append(filters.currency)
if idx == len(gross_profit_data.grouped_data)-1:
- row[0] = frappe.bold("Total")
+ row[0] = "Total"
+
data.append(row)
def get_columns(group_wise_columns, filters):
columns = []
column_map = frappe._dict({
"parent": _("Sales Invoice") + ":Link/Sales Invoice:120",
+ "invoice_or_item": _("Sales Invoice") + ":Link/Sales Invoice:120",
"posting_date": _("Posting Date") + ":Date:100",
"posting_time": _("Posting Time") + ":Data:100",
"item_code": _("Item Code") + ":Link/Item:100",
@@ -122,7 +124,7 @@
def get_column_names():
return frappe._dict({
- 'parent': 'sales_invoice',
+ 'invoice_or_item': 'sales_invoice',
'customer': 'customer',
'customer_group': 'customer_group',
'posting_date': 'posting_date',
@@ -245,19 +247,28 @@
self.add_to_totals(new_row)
else:
for i, row in enumerate(self.grouped[key]):
- if row.parent in self.returned_invoices \
- and row.item_code in self.returned_invoices[row.parent]:
- returned_item_rows = self.returned_invoices[row.parent][row.item_code]
- for returned_item_row in returned_item_rows:
- row.qty += flt(returned_item_row.qty)
- row.base_amount += flt(returned_item_row.base_amount, self.currency_precision)
- row.buying_amount = flt(flt(row.qty) * flt(row.buying_rate), self.currency_precision)
- if (flt(row.qty) or row.base_amount) and self.is_not_invoice_row(row):
- row = self.set_average_rate(row)
- self.grouped_data.append(row)
- self.add_to_totals(row)
+ if row.indent == 1.0:
+ if row.parent in self.returned_invoices \
+ and row.item_code in self.returned_invoices[row.parent]:
+ returned_item_rows = self.returned_invoices[row.parent][row.item_code]
+ for returned_item_row in returned_item_rows:
+ row.qty += flt(returned_item_row.qty)
+ row.base_amount += flt(returned_item_row.base_amount, self.currency_precision)
+ row.buying_amount = flt(flt(row.qty) * flt(row.buying_rate), self.currency_precision)
+ if (flt(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)
+
+ if self.filters.get("group_by") == "Invoice":
+ self.totals.indent = 0.0
+ self.totals.parent_invoice = ""
+ self.totals.invoice_or_item = "Total"
+ self.si_list.append(self.totals)
+ else:
+ self.grouped_data.append(self.totals)
def is_not_invoice_row(self, row):
return (self.filters.get("group_by") == "Invoice" and row.indent != 0.0) or self.filters.get("group_by") != "Invoice"
@@ -446,7 +457,7 @@
if not row.indent:
row.indent = 1.0
row.parent_invoice = row.parent
- row.parent = row.item_code
+ row.invoice_or_item = row.item_code
if frappe.db.exists('Product Bundle', row.item_code):
self.add_bundle_items(row, index)
@@ -455,7 +466,8 @@
return frappe._dict({
'parent_invoice': "",
'indent': 0.0,
- 'parent': row.parent,
+ 'invoice_or_item': row.parent,
+ 'parent': None,
'posting_date': row.posting_date,
'posting_time': row.posting_time,
'project': row.project,
@@ -499,7 +511,8 @@
return frappe._dict({
'parent_invoice': product_bundle.item_code,
'indent': product_bundle.indent + 1,
- 'parent': item.item_code,
+ 'parent': None,
+ 'invoice_or_item': item.item_code,
'posting_date': product_bundle.posting_date,
'posting_time': product_bundle.posting_time,
'project': product_bundle.project,
diff --git a/erpnext/accounts/report/sales_partners_commission/sales_partners_commission.json b/erpnext/accounts/report/sales_partners_commission/sales_partners_commission.json
index a740de3..9dd4e43 100644
--- a/erpnext/accounts/report/sales_partners_commission/sales_partners_commission.json
+++ b/erpnext/accounts/report/sales_partners_commission/sales_partners_commission.json
@@ -1,27 +1,30 @@
{
- "add_total_row": 0,
- "apply_user_permissions": 1,
- "creation": "2013-05-06 12:28:23",
- "disabled": 0,
- "docstatus": 0,
- "doctype": "Report",
- "idx": 3,
- "is_standard": "Yes",
- "modified": "2017-03-06 05:52:57.645281",
- "modified_by": "Administrator",
- "module": "Accounts",
- "name": "Sales Partners Commission",
- "owner": "Administrator",
- "query": "SELECT\n sales_partner as \"Sales Partner:Link/Sales Partner:150\",\n\tsum(base_net_total) as \"Invoiced Amount (Exclusive Tax):Currency:210\",\n\tsum(total_commission) as \"Total Commission:Currency:150\",\n\tsum(total_commission)*100/sum(base_net_total) as \"Average Commission Rate:Currency:170\"\nFROM\n\t`tabSales Invoice`\nWHERE\n\tdocstatus = 1 and ifnull(base_net_total, 0) > 0 and ifnull(total_commission, 0) > 0\nGROUP BY\n\tsales_partner\nORDER BY\n\t\"Total Commission:Currency:120\"",
- "ref_doctype": "Sales Invoice",
- "report_name": "Sales Partners Commission",
- "report_type": "Query Report",
+ "add_total_row": 0,
+ "columns": [],
+ "creation": "2013-05-06 12:28:23",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 3,
+ "is_standard": "Yes",
+ "modified": "2021-10-06 06:26:07.881340",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Sales Partners Commission",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "query": "SELECT\n sales_partner as \"Sales Partner:Link/Sales Partner:220\",\n\tsum(base_net_total) as \"Invoiced Amount (Excl. Tax):Currency:220\",\n\tsum(amount_eligible_for_commission) as \"Amount Eligible for Commission:Currency:220\",\n\tsum(total_commission) as \"Total Commission:Currency:170\",\n\tsum(total_commission)*100/sum(amount_eligible_for_commission) as \"Average Commission Rate:Percent:220\"\nFROM\n\t`tabSales Invoice`\nWHERE\n\tdocstatus = 1 and ifnull(base_net_total, 0) > 0 and ifnull(total_commission, 0) > 0\nGROUP BY\n\tsales_partner\nORDER BY\n\t\"Total Commission:Currency:120\"",
+ "ref_doctype": "Sales Invoice",
+ "report_name": "Sales Partners Commission",
+ "report_type": "Query Report",
"roles": [
{
"role": "Accounts Manager"
- },
+ },
{
"role": "Accounts User"
}
]
-}
+}
\ No newline at end of file
diff --git a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py
index a3a45d1..caee1a1 100644
--- a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py
+++ b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py
@@ -36,12 +36,16 @@
posting_date = entry.posting_date
voucher_type = entry.voucher_type
+ if not tax_withholding_category:
+ tax_withholding_category = supplier_map.get(supplier, {}).get('tax_withholding_category')
+ rate = tax_rate_map.get(tax_withholding_category)
+
if entry.account in tds_accounts:
tds_deducted += (entry.credit - entry.debit)
total_amount_credited += (entry.credit - entry.debit)
- if rate and tds_deducted:
+ if tds_deducted:
row = {
'pan' if frappe.db.has_column('Supplier', 'pan') else 'tax_id': supplier_map.get(supplier, {}).get('pan'),
'supplier': supplier_map.get(supplier, {}).get('name')
@@ -67,7 +71,7 @@
def get_supplier_pan_map():
supplier_map = frappe._dict()
- suppliers = frappe.db.get_all('Supplier', fields=['name', 'pan', 'supplier_type', 'supplier_name'])
+ suppliers = frappe.db.get_all('Supplier', fields=['name', 'pan', 'supplier_type', 'supplier_name', 'tax_withholding_category'])
for d in suppliers:
supplier_map[d.name] = d
diff --git a/erpnext/accounts/test/test_reports.py b/erpnext/accounts/test/test_reports.py
new file mode 100644
index 0000000..78c109a
--- /dev/null
+++ b/erpnext/accounts/test/test_reports.py
@@ -0,0 +1,48 @@
+import unittest
+from typing import List, Tuple
+
+from erpnext.tests.utils import ReportFilters, ReportName, execute_script_report
+
+DEFAULT_FILTERS = {
+ "company": "_Test Company",
+ "from_date": "2010-01-01",
+ "to_date": "2030-01-01",
+ "period_start_date": "2010-01-01",
+ "period_end_date": "2030-01-01"
+}
+
+
+REPORT_FILTER_TEST_CASES: List[Tuple[ReportName, ReportFilters]] = [
+ ("General Ledger", {"group_by": "Group by Voucher (Consolidated)"} ),
+ ("General Ledger", {"group_by": "Group by Voucher (Consolidated)", "include_dimensions": 1} ),
+ ("Accounts Payable", {"range1": 30, "range2": 60, "range3": 90, "range4": 120}),
+ ("Accounts Receivable", {"range1": 30, "range2": 60, "range3": 90, "range4": 120}),
+ ("Consolidated Financial Statement", {"report": "Balance Sheet"} ),
+ ("Consolidated Financial Statement", {"report": "Profit and Loss Statement"} ),
+ ("Consolidated Financial Statement", {"report": "Cash Flow"} ),
+ ("Gross Profit", {"group_by": "Invoice"}),
+ ("Gross Profit", {"group_by": "Item Code"}),
+ ("Gross Profit", {"group_by": "Item Group"}),
+ ("Gross Profit", {"group_by": "Customer"}),
+ ("Gross Profit", {"group_by": "Customer Group"}),
+ ("Item-wise Sales Register", {}),
+ ("Item-wise Purchase Register", {}),
+ ("Sales Register", {}),
+ ("Purchase Register", {}),
+ ("Tax Detail", {"mode": "run", "report_name": "Tax Detail"},),
+]
+
+OPTIONAL_FILTERS = {}
+
+
+class TestReports(unittest.TestCase):
+ def test_execute_all_accounts_reports(self):
+ """Test that all script report in stock modules are executable with supported filters"""
+ for report, filter in REPORT_FILTER_TEST_CASES:
+ execute_script_report(
+ report_name=report,
+ module="Accounts",
+ filters=filter,
+ default_filters=DEFAULT_FILTERS,
+ optional_filters=OPTIONAL_FILTERS if filter.get("_optional") else None,
+ )
diff --git a/erpnext/agriculture/doctype/__init__.py b/erpnext/agriculture/doctype/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/erpnext/agriculture/doctype/__init__.py
+++ /dev/null
diff --git a/erpnext/agriculture/doctype/agriculture_analysis_criteria/__init__.py b/erpnext/agriculture/doctype/agriculture_analysis_criteria/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/erpnext/agriculture/doctype/agriculture_analysis_criteria/__init__.py
+++ /dev/null
diff --git a/erpnext/agriculture/doctype/agriculture_analysis_criteria/agriculture_analysis_criteria.js b/erpnext/agriculture/doctype/agriculture_analysis_criteria/agriculture_analysis_criteria.js
deleted file mode 100644
index e236cc6..0000000
--- a/erpnext/agriculture/doctype/agriculture_analysis_criteria/agriculture_analysis_criteria.js
+++ /dev/null
@@ -1,8 +0,0 @@
-// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('Agriculture Analysis Criteria', {
- refresh: function(frm) {
-
- }
-});
diff --git a/erpnext/agriculture/doctype/agriculture_analysis_criteria/agriculture_analysis_criteria.json b/erpnext/agriculture/doctype/agriculture_analysis_criteria/agriculture_analysis_criteria.json
deleted file mode 100644
index bb5e4d9..0000000
--- a/erpnext/agriculture/doctype/agriculture_analysis_criteria/agriculture_analysis_criteria.json
+++ /dev/null
@@ -1,182 +0,0 @@
-{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "autoname": "field:title",
- "beta": 0,
- "creation": "2017-12-05 16:37:46.599982",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "title",
- "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": "Title",
- "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": 1
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "standard",
- "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": "Standard",
- "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": "linked_doctype",
- "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": "Linked Doctype",
- "length": 0,
- "no_copy": 0,
- "options": "\nWater Analysis\nSoil Analysis\nPlant Analysis\nFertilizer\nSoil Texture\nWeather",
- "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
- }
- ],
- "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-11-04 03:27:36.678832",
- "modified_by": "Administrator",
- "module": "Agriculture",
- "name": "Agriculture Analysis Criteria",
- "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": "Agriculture Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 1
- },
- {
- "amend": 0,
- "cancel": 0,
- "create": 0,
- "delete": 0,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Agriculture User",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 1
- }
- ],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "restrict_to_domain": "Agriculture",
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "title_field": "",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
-}
\ No newline at end of file
diff --git a/erpnext/agriculture/doctype/agriculture_analysis_criteria/agriculture_analysis_criteria.py b/erpnext/agriculture/doctype/agriculture_analysis_criteria/agriculture_analysis_criteria.py
deleted file mode 100644
index 1945992..0000000
--- a/erpnext/agriculture/doctype/agriculture_analysis_criteria/agriculture_analysis_criteria.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-from frappe.model.document import Document
-
-
-class AgricultureAnalysisCriteria(Document):
- pass
diff --git a/erpnext/agriculture/doctype/agriculture_analysis_criteria/test_agriculture_analysis_criteria.py b/erpnext/agriculture/doctype/agriculture_analysis_criteria/test_agriculture_analysis_criteria.py
deleted file mode 100644
index 91e6f3f..0000000
--- a/erpnext/agriculture/doctype/agriculture_analysis_criteria/test_agriculture_analysis_criteria.py
+++ /dev/null
@@ -1,8 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
-
-import unittest
-
-
-class TestAgricultureAnalysisCriteria(unittest.TestCase):
- pass
diff --git a/erpnext/agriculture/doctype/agriculture_task/agriculture_task.js b/erpnext/agriculture/doctype/agriculture_task/agriculture_task.js
deleted file mode 100644
index 4d6b959..0000000
--- a/erpnext/agriculture/doctype/agriculture_task/agriculture_task.js
+++ /dev/null
@@ -1,8 +0,0 @@
-// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('Agriculture Task', {
- refresh: function(frm) {
-
- }
-});
diff --git a/erpnext/agriculture/doctype/agriculture_task/agriculture_task.json b/erpnext/agriculture/doctype/agriculture_task/agriculture_task.json
deleted file mode 100644
index d943d77..0000000
--- a/erpnext/agriculture/doctype/agriculture_task/agriculture_task.json
+++ /dev/null
@@ -1,212 +0,0 @@
-{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "autoname": "AG-TASK-.#####",
- "beta": 0,
- "creation": "2017-10-26 15:51:19.602452",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "task_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": "Task Name",
- "length": 0,
- "no_copy": 0,
- "options": "",
- "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
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "",
- "fieldname": "start_day",
- "fieldtype": "Int",
- "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": "Start Day",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "",
- "fieldname": "end_day",
- "fieldtype": "Int",
- "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": "End Day",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "Ignore holidays",
- "fieldname": "holiday_management",
- "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": "Holiday Management",
- "length": 0,
- "no_copy": 0,
- "options": "Ignore holidays\nPrevious Business Day\nNext Business Day",
- "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
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "Low",
- "fieldname": "priority",
- "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": "Priority",
- "length": 0,
- "no_copy": 0,
- "options": "Low\nMedium\nHigh\nUrgent",
- "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
- }
- ],
- "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-11-04 03:28:08.679157",
- "modified_by": "Administrator",
- "module": "Agriculture",
- "name": "Agriculture Task",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "restrict_to_domain": "Agriculture",
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
-}
\ No newline at end of file
diff --git a/erpnext/agriculture/doctype/agriculture_task/agriculture_task.py b/erpnext/agriculture/doctype/agriculture_task/agriculture_task.py
deleted file mode 100644
index dab2998..0000000
--- a/erpnext/agriculture/doctype/agriculture_task/agriculture_task.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-from frappe.model.document import Document
-
-
-class AgricultureTask(Document):
- pass
diff --git a/erpnext/agriculture/doctype/agriculture_task/test_agriculture_task.py b/erpnext/agriculture/doctype/agriculture_task/test_agriculture_task.py
deleted file mode 100644
index 94d7915..0000000
--- a/erpnext/agriculture/doctype/agriculture_task/test_agriculture_task.py
+++ /dev/null
@@ -1,8 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
-
-import unittest
-
-
-class TestAgricultureTask(unittest.TestCase):
- pass
diff --git a/erpnext/agriculture/doctype/crop/__init__.py b/erpnext/agriculture/doctype/crop/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/erpnext/agriculture/doctype/crop/__init__.py
+++ /dev/null
diff --git a/erpnext/agriculture/doctype/crop/crop.js b/erpnext/agriculture/doctype/crop/crop.js
deleted file mode 100644
index 5508246..0000000
--- a/erpnext/agriculture/doctype/crop/crop.js
+++ /dev/null
@@ -1,55 +0,0 @@
-// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-// For license information, please see license.txt
-
-frappe.provide("erpnext.crop");
-
-frappe.ui.form.on('Crop', {
- refresh: (frm) => {
- frm.fields_dict.materials_required.grid.set_column_disp('bom_no', false);
- }
-});
-
-frappe.ui.form.on("BOM Item", {
- item_code: (frm, cdt, cdn) => {
- erpnext.crop.update_item_rate_uom(frm, cdt, cdn);
- },
- qty: (frm, cdt, cdn) => {
- erpnext.crop.update_item_qty_amount(frm, cdt, cdn);
- },
- rate: (frm, cdt, cdn) => {
- erpnext.crop.update_item_qty_amount(frm, cdt, cdn);
- }
-});
-
-erpnext.crop.update_item_rate_uom = function(frm, cdt, cdn) {
- let material_list = ['materials_required', 'produce', 'byproducts'];
- material_list.forEach((material) => {
- frm.doc[material].forEach((item, index) => {
- if (item.name == cdn && item.item_code){
- frappe.call({
- method:'erpnext.agriculture.doctype.crop.crop.get_item_details',
- args: {
- item_code: item.item_code
- },
- callback: (r) => {
- frappe.model.set_value('BOM Item', item.name, 'uom', r.message.uom);
- frappe.model.set_value('BOM Item', item.name, 'rate', r.message.rate);
- }
- });
- }
- });
- });
-};
-
-erpnext.crop.update_item_qty_amount = function(frm, cdt, cdn) {
- let material_list = ['materials_required', 'produce', 'byproducts'];
- material_list.forEach((material) => {
- frm.doc[material].forEach((item, index) => {
- if (item.name == cdn){
- if (!frappe.model.get_value('BOM Item', item.name, 'qty'))
- frappe.model.set_value('BOM Item', item.name, 'qty', 1);
- frappe.model.set_value('BOM Item', item.name, 'amount', item.qty * item.rate);
- }
- });
- });
-};
diff --git a/erpnext/agriculture/doctype/crop/crop.json b/erpnext/agriculture/doctype/crop/crop.json
deleted file mode 100644
index e357abb..0000000
--- a/erpnext/agriculture/doctype/crop/crop.json
+++ /dev/null
@@ -1,1110 +0,0 @@
-{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "autoname": "field:title",
- "beta": 0,
- "creation": "2017-10-20 01:16:17.606174",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "title",
- "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": "Title",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 1
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "section_break_2",
- "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,
- "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": "crop_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": "Crop 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": 1,
- "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": "column_break_4",
- "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
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "scientific_name",
- "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": "Scientific 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
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "description": "You can define all the tasks which need to carried out for this crop here. The day field is used to mention the day on which the task needs to be carried out, 1 being the 1st day, etc.. ",
- "fieldname": "section_break_20",
- "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": "Tasks",
- "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": "agriculture_task",
- "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": "Agriculture Task",
- "length": 0,
- "no_copy": 0,
- "options": "Agriculture Task",
- "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
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "0",
- "fieldname": "period",
- "fieldtype": "Int",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Period",
- "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": "section_break_9",
- "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,
- "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": "crop_spacing",
- "fieldtype": "Float",
- "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": "Crop Spacing",
- "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": "crop_spacing_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": "Crop Spacing UOM",
- "length": 0,
- "no_copy": 0,
- "options": "UOM",
- "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": "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
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "row_spacing",
- "fieldtype": "Float",
- "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": "Row Spacing",
- "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": "row_spacing_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": "Row Spacing UOM",
- "length": 0,
- "no_copy": 0,
- "options": "UOM",
- "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": "section_break_4",
- "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,
- "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": "type",
- "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": "Type",
- "length": 0,
- "no_copy": 0,
- "options": "Annual\nPerennial\nBiennial",
- "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": "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,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "category",
- "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": "Category",
- "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": "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,
- "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": "target_warehouse",
- "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": "Target 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
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "section_break_12",
- "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,
- "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": "planting_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": "Planting UOM",
- "length": 0,
- "no_copy": 0,
- "options": "UOM",
- "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": "planting_area",
- "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": "Planting Area",
- "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": "column_break_14",
- "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
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "yield_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": "Yield UOM",
- "length": 0,
- "no_copy": 0,
- "options": "UOM",
- "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": "section_break_16",
- "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,
- "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": "section_break_17",
- "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": "Materials Required",
- "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": "materials_required",
- "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": "Materials Required",
- "length": 0,
- "no_copy": 0,
- "options": "BOM 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
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "section_break_19",
- "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": "Produced Items",
- "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": "produce",
- "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": "Produce",
- "length": 0,
- "no_copy": 0,
- "options": "BOM 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
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "section_break_18",
- "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": "Byproducts",
- "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": "byproducts",
- "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": "Byproducts",
- "length": 0,
- "no_copy": 0,
- "options": "BOM 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
- }
- ],
- "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-11-04 03:27:10.651075",
- "modified_by": "Administrator",
- "module": "Agriculture",
- "name": "Crop",
- "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": "Agriculture Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 1
- },
- {
- "amend": 0,
- "cancel": 0,
- "create": 0,
- "delete": 0,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Agriculture User",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 1
- }
- ],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "restrict_to_domain": "Agriculture",
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
-}
\ No newline at end of file
diff --git a/erpnext/agriculture/doctype/crop/crop.py b/erpnext/agriculture/doctype/crop/crop.py
deleted file mode 100644
index ed2073c..0000000
--- a/erpnext/agriculture/doctype/crop/crop.py
+++ /dev/null
@@ -1,31 +0,0 @@
-# Copyright (c) 2017, 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 Crop(Document):
- def validate(self):
- self.validate_crop_tasks()
-
- def validate_crop_tasks(self):
- for task in self.agriculture_task:
- if task.start_day > task.end_day:
- frappe.throw(_("Start day is greater than end day in task '{0}'").format(task.task_name))
-
- # Verify that the crop period is correct
- max_crop_period = max([task.end_day for task in self.agriculture_task])
- self.period = max(self.period, max_crop_period)
-
- # Sort the crop tasks based on start days,
- # maintaining the order for same-day tasks
- self.agriculture_task.sort(key=lambda task: task.start_day)
-
-
-@frappe.whitelist()
-def get_item_details(item_code):
- item = frappe.get_doc('Item', item_code)
- return {"uom": item.stock_uom, "rate": item.valuation_rate}
diff --git a/erpnext/agriculture/doctype/crop/crop_dashboard.py b/erpnext/agriculture/doctype/crop/crop_dashboard.py
deleted file mode 100644
index 37cdbb2..0000000
--- a/erpnext/agriculture/doctype/crop/crop_dashboard.py
+++ /dev/null
@@ -1,12 +0,0 @@
-from frappe import _
-
-
-def get_data():
- return {
- 'transactions': [
- {
- 'label': _('Crop Cycle'),
- 'items': ['Crop Cycle']
- }
- ]
- }
diff --git a/erpnext/agriculture/doctype/crop/test_crop.js b/erpnext/agriculture/doctype/crop/test_crop.js
deleted file mode 100644
index 4055563..0000000
--- a/erpnext/agriculture/doctype/crop/test_crop.js
+++ /dev/null
@@ -1,116 +0,0 @@
-/* eslint-disable */
-// rename this file from _test_[name] to test_[name] to activate
-// and remove above this line
-
-QUnit.test("test: Crop", function (assert) {
- let done = assert.async();
-
- // number of asserts
- assert.expect(2);
-
- frappe.run_serially([
- // insert a new Item
- () => frappe.tests.make('Item', [
- // values to be set
- {item_code: 'Basil Seeds'},
- {item_name: 'Basil Seeds'},
- {item_group: 'Seed'}
- ]),
- // insert a new Item
- () => frappe.tests.make('Item', [
- // values to be set
- {item_code: 'Twigs'},
- {item_name: 'Twigs'},
- {item_group: 'By-product'}
- ]),
- // insert a new Item
- () => frappe.tests.make('Item', [
- // values to be set
- {item_code: 'Basil Leaves'},
- {item_name: 'Basil Leaves'},
- {item_group: 'Produce'}
- ]),
- // insert a new Crop
- () => frappe.tests.make('Crop', [
- // values to be set
- {title: 'Basil from seed'},
- {crop_name: 'Basil'},
- {scientific_name: 'Ocimum basilicum'},
- {materials_required: [
- [
- {item_code: 'Basil Seeds'},
- {qty: '25'},
- {uom: 'Nos'},
- {rate: '1'}
- ],
- [
- {item_code: 'Urea'},
- {qty: '5'},
- {uom: 'Kg'},
- {rate: '10'}
- ]
- ]},
- {byproducts: [
- [
- {item_code: 'Twigs'},
- {qty: '25'},
- {uom: 'Nos'},
- {rate: '1'}
- ]
- ]},
- {produce: [
- [
- {item_code: 'Basil Leaves'},
- {qty: '100'},
- {uom: 'Nos'},
- {rate: '1'}
- ]
- ]},
- {agriculture_task: [
- [
- {task_name: "Plough the field"},
- {start_day: 1},
- {end_day: 1},
- {holiday_management: "Ignore holidays"}
- ],
- [
- {task_name: "Plant the seeds"},
- {start_day: 2},
- {end_day: 3},
- {holiday_management: "Ignore holidays"}
- ],
- [
- {task_name: "Water the field"},
- {start_day: 4},
- {end_day: 4},
- {holiday_management: "Ignore holidays"}
- ],
- [
- {task_name: "First harvest"},
- {start_day: 8},
- {end_day: 8},
- {holiday_management: "Ignore holidays"}
- ],
- [
- {task_name: "Add the fertilizer"},
- {start_day: 10},
- {end_day: 12},
- {holiday_management: "Ignore holidays"}
- ],
- [
- {task_name: "Final cut"},
- {start_day: 15},
- {end_day: 15},
- {holiday_management: "Ignore holidays"}
- ]
- ]}
- ]),
- // agriculture task list
- () => {
- assert.equal(cur_frm.doc.name, 'Basil from seed');
- assert.equal(cur_frm.doc.period, 15);
- },
- () => done()
- ]);
-
-});
diff --git a/erpnext/agriculture/doctype/crop/test_crop.py b/erpnext/agriculture/doctype/crop/test_crop.py
deleted file mode 100644
index c79a367..0000000
--- a/erpnext/agriculture/doctype/crop/test_crop.py
+++ /dev/null
@@ -1,13 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
-
-import unittest
-
-import frappe
-
-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)
diff --git a/erpnext/agriculture/doctype/crop/test_records.json b/erpnext/agriculture/doctype/crop/test_records.json
deleted file mode 100644
index 41ddb9a..0000000
--- a/erpnext/agriculture/doctype/crop/test_records.json
+++ /dev/null
@@ -1,80 +0,0 @@
-[
- {
- "doctype": "Item",
- "item_code": "Basil Seeds",
- "item_name": "Basil Seeds",
- "item_group": "Seed"
- },
- {
- "doctype": "Item",
- "item_code": "Twigs",
- "item_name": "Twigs",
- "item_group": "By-product"
- },
- {
- "doctype": "Item",
- "item_code": "Basil Leaves",
- "item_name": "Basil Leaves",
- "item_group": "Produce"
- },
- {
- "doctype": "Crop",
- "title": "Basil from seed",
- "crop_name": "Basil",
- "scientific_name": "Ocimum basilicum",
- "materials_required": [{
- "item_code": "Basil Seeds",
- "qty": "25",
- "uom": "Nos",
- "rate": "1"
- }, {
- "item_code": "Urea",
- "qty": "5",
- "uom": "Kg",
- "rate": "10"
- }],
- "byproducts": [{
- "item_code": "Twigs",
- "qty": "25",
- "uom": "Nos",
- "rate": "1"
- }],
- "produce": [{
- "item_code": "Basil Leaves",
- "qty": "100",
- "uom": "Nos",
- "rate": "1"
- }],
- "agriculture_task": [{
- "task_name": "Plough the field",
- "start_day": 1,
- "end_day": 1,
- "holiday_management": "Ignore holidays"
- }, {
- "task_name": "Plant the seeds",
- "start_day": 2,
- "end_day": 3,
- "holiday_management": "Ignore holidays"
- }, {
- "task_name": "Water the field",
- "start_day": 4,
- "end_day": 4,
- "holiday_management": "Ignore holidays"
- }, {
- "task_name": "First harvest",
- "start_day": 8,
- "end_day": 8,
- "holiday_management": "Ignore holidays"
- }, {
- "task_name": "Add the fertilizer",
- "start_day": 10,
- "end_day": 12,
- "holiday_management": "Ignore holidays"
- }, {
- "task_name": "Final cut",
- "start_day": 15,
- "end_day": 15,
- "holiday_management": "Ignore holidays"
- }]
- }
-]
\ No newline at end of file
diff --git a/erpnext/agriculture/doctype/crop_cycle/__init__.py b/erpnext/agriculture/doctype/crop_cycle/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/erpnext/agriculture/doctype/crop_cycle/__init__.py
+++ /dev/null
diff --git a/erpnext/agriculture/doctype/crop_cycle/crop_cycle.js b/erpnext/agriculture/doctype/crop_cycle/crop_cycle.js
deleted file mode 100644
index 94392e7..0000000
--- a/erpnext/agriculture/doctype/crop_cycle/crop_cycle.js
+++ /dev/null
@@ -1,48 +0,0 @@
-// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('Crop Cycle', {
- refresh: (frm) => {
- if (!frm.doc.__islocal)
- frm.add_custom_button(__('Reload Linked Analysis'), () => frm.call("reload_linked_analysis"));
-
- frappe.realtime.on("List of Linked Docs", (output) => {
- let analysis_doctypes = ['Soil Texture', 'Plant Analysis', 'Soil Analysis'];
- let analysis_doctypes_docs = ['soil_texture', 'plant_analysis', 'soil_analysis'];
- let obj_to_append = {soil_analysis: [], soil_texture: [], plant_analysis: []};
- output['Location'].forEach( (land_doc) => {
- analysis_doctypes.forEach( (doctype) => {
- output[doctype].forEach( (analysis_doc) => {
- let point_to_be_tested = JSON.parse(analysis_doc.location).features[0].geometry.coordinates;
- let poly_of_land = JSON.parse(land_doc.location).features[0].geometry.coordinates[0];
- if (is_in_land_unit(point_to_be_tested, poly_of_land)){
- obj_to_append[analysis_doctypes_docs[analysis_doctypes.indexOf(doctype)]].push(analysis_doc.name);
- }
- });
- });
- });
- frm.call('append_to_child', {
- obj_to_append: obj_to_append
- });
- });
- }
-});
-
-function is_in_land_unit(point, vs) {
- // ray-casting algorithm based on
- // http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html
-
- var x = point[0], y = point[1];
-
- var inside = false;
- for (var i = 0, j = vs.length - 1; i < vs.length; j = i++) {
- var xi = vs[i][0], yi = vs[i][1];
- var xj = vs[j][0], yj = vs[j][1];
-
- var intersect = ((yi > y) != (yj > y))
- && (x < (xj - xi) * (y - yi) / (yj - yi) + xi);
- if (intersect) inside = !inside;
- }
-
- return inside;
-};
diff --git a/erpnext/agriculture/doctype/crop_cycle/crop_cycle.json b/erpnext/agriculture/doctype/crop_cycle/crop_cycle.json
deleted file mode 100644
index a076718..0000000
--- a/erpnext/agriculture/doctype/crop_cycle/crop_cycle.json
+++ /dev/null
@@ -1,904 +0,0 @@
-{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "autoname": "field:title",
- "beta": 0,
- "creation": "2017-11-02 03:09:35.449880",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "title",
- "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": "Title",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 1
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "crop",
- "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": "Crop",
- "length": 0,
- "no_copy": 0,
- "options": "Crop",
- "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
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "description": "",
- "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,
- "label": "Linked Location",
- "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,
- "description": "A link to all the Locations in which the Crop is growing",
- "fieldname": "linked_location",
- "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": "Linked Location",
- "length": 0,
- "no_copy": 0,
- "options": "Linked Location",
- "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": "section_break_3",
- "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,
- "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,
- "depends_on": "eval:!doc.__islocal",
- "fieldname": "project",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 1,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Project",
- "length": 0,
- "no_copy": 0,
- "options": "Project",
- "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": "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
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "description": "This will be day 1 of the crop cycle",
- "fieldname": "start_date",
- "fieldtype": "Date",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Start Date",
- "length": 0,
- "no_copy": 0,
- "options": "",
- "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
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_from": "project.expected_end_date",
- "fieldname": "end_date",
- "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": "End Date",
- "length": 0,
- "no_copy": 0,
- "options": "",
- "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,
- "translatable": 0,
- "unique": 0
- },
- {
- "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,
- "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": "iso_8601_standard",
- "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": "ISO 8601 standard",
- "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": "column_break_5",
- "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
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "cycle_type",
- "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": "Cycle Type",
- "length": 0,
- "no_copy": 0,
- "options": "Yearly\nLess than a year",
- "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": "section_break_12",
- "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,
- "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,
- "description": "The minimum length between each plant in the field for optimum growth",
- "fetch_from": "crop.crop_spacing",
- "fieldname": "crop_spacing",
- "fieldtype": "Float",
- "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": "Crop Spacing",
- "length": 0,
- "no_copy": 0,
- "options": "",
- "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": "crop_spacing_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": "Crop Spacing UOM",
- "length": 0,
- "no_copy": 0,
- "options": "UOM",
- "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": "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
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "description": "The minimum distance between rows of plants for optimum growth",
- "fetch_from": "crop.row_spacing",
- "fieldname": "row_spacing",
- "fieldtype": "Float",
- "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": "Row Spacing",
- "length": 0,
- "no_copy": 0,
- "options": "",
- "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": "row_spacing_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": "Row Spacing UOM",
- "length": 0,
- "no_copy": 0,
- "options": "UOM",
- "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,
- "depends_on": "eval:!doc.__islocal",
- "description": "List of diseases detected on the field. When selected it'll automatically add a list of tasks to deal with the disease ",
- "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": "Detected Diseases",
- "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": "detected_disease",
- "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": "Detected Disease",
- "length": 0,
- "no_copy": 0,
- "options": "Detected Disease",
- "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": 1,
- "collapsible_depends_on": "eval:false",
- "columns": 0,
- "fieldname": "section_break_22",
- "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": "LInked Analysis",
- "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": "soil_texture",
- "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": "Soil Texture",
- "length": 0,
- "no_copy": 0,
- "options": "Linked Soil Texture",
- "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": "soil_analysis",
- "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": "Soil Analysis",
- "length": 0,
- "no_copy": 0,
- "options": "Linked Soil Analysis",
- "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": "plant_analysis",
- "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": "Plant Analysis",
- "length": 0,
- "no_copy": 0,
- "options": "Linked Plant Analysis",
- "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
- }
- ],
- "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-11-04 03:31:47.602312",
- "modified_by": "Administrator",
- "module": "Agriculture",
- "name": "Crop Cycle",
- "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": "Agriculture Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 1
- },
- {
- "amend": 0,
- "cancel": 0,
- "create": 0,
- "delete": 0,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Agriculture User",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 1
- }
- ],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "restrict_to_domain": "Agriculture",
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
-}
\ No newline at end of file
diff --git a/erpnext/agriculture/doctype/crop_cycle/crop_cycle.py b/erpnext/agriculture/doctype/crop_cycle/crop_cycle.py
deleted file mode 100644
index 43c5bbd..0000000
--- a/erpnext/agriculture/doctype/crop_cycle/crop_cycle.py
+++ /dev/null
@@ -1,126 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-import ast
-
-import frappe
-from frappe import _
-from frappe.model.document import Document
-from frappe.utils import add_days
-
-
-class CropCycle(Document):
- def validate(self):
- self.set_missing_values()
-
- def after_insert(self):
- self.create_crop_cycle_project()
- self.create_tasks_for_diseases()
-
- def on_update(self):
- self.create_tasks_for_diseases()
-
- def set_missing_values(self):
- crop = frappe.get_doc('Crop', self.crop)
-
- if not self.crop_spacing_uom:
- self.crop_spacing_uom = crop.crop_spacing_uom
-
- if not self.row_spacing_uom:
- self.row_spacing_uom = crop.row_spacing_uom
-
- def create_crop_cycle_project(self):
- crop = frappe.get_doc('Crop', self.crop)
-
- self.project = self.create_project(crop.period, crop.agriculture_task)
- self.create_task(crop.agriculture_task, self.project, self.start_date)
-
- def create_tasks_for_diseases(self):
- for disease in self.detected_disease:
- if not disease.tasks_created:
- self.import_disease_tasks(disease.disease, disease.start_date)
- disease.tasks_created = True
-
- frappe.msgprint(_("Tasks have been created for managing the {0} disease (on row {1})").format(disease.disease, disease.idx))
-
- def import_disease_tasks(self, disease, start_date):
- disease_doc = frappe.get_doc('Disease', disease)
- self.create_task(disease_doc.treatment_task, self.project, start_date)
-
- def create_project(self, period, crop_tasks):
- project = frappe.get_doc({
- "doctype": "Project",
- "project_name": self.title,
- "expected_start_date": self.start_date,
- "expected_end_date": add_days(self.start_date, period - 1)
- }).insert()
-
- return project.name
-
- def create_task(self, crop_tasks, project_name, start_date):
- for crop_task in crop_tasks:
- frappe.get_doc({
- "doctype": "Task",
- "subject": crop_task.get("task_name"),
- "priority": crop_task.get("priority"),
- "project": project_name,
- "exp_start_date": add_days(start_date, crop_task.get("start_day") - 1),
- "exp_end_date": add_days(start_date, crop_task.get("end_day") - 1)
- }).insert()
-
- @frappe.whitelist()
- def reload_linked_analysis(self):
- linked_doctypes = ['Soil Texture', 'Soil Analysis', 'Plant Analysis']
- required_fields = ['location', 'name', 'collection_datetime']
- output = {}
-
- for doctype in linked_doctypes:
- output[doctype] = frappe.get_all(doctype, fields=required_fields)
-
- output['Location'] = []
-
- for location in self.linked_location:
- output['Location'].append(frappe.get_doc('Location', location.location))
-
- frappe.publish_realtime("List of Linked Docs",
- output, user=frappe.session.user)
-
- @frappe.whitelist()
- def append_to_child(self, obj_to_append):
- for doctype in obj_to_append:
- for doc_name in set(obj_to_append[doctype]):
- self.append(doctype, {doctype: doc_name})
-
- self.save()
-
-
-def get_coordinates(doc):
- return ast.literal_eval(doc.location).get('features')[0].get('geometry').get('coordinates')
-
-
-def get_geometry_type(doc):
- return ast.literal_eval(doc.location).get('features')[0].get('geometry').get('type')
-
-
-def is_in_location(point, vs):
- x, y = point
- inside = False
-
- j = len(vs) - 1
- i = 0
-
- while i < len(vs):
- xi, yi = vs[i]
- xj, yj = vs[j]
-
- intersect = ((yi > y) != (yj > y)) and (
- x < (xj - xi) * (y - yi) / (yj - yi) + xi)
-
- if intersect:
- inside = not inside
-
- i = j
- j += 1
-
- return inside
diff --git a/erpnext/agriculture/doctype/crop_cycle/test_crop_cycle.js b/erpnext/agriculture/doctype/crop_cycle/test_crop_cycle.js
deleted file mode 100644
index 87184da..0000000
--- a/erpnext/agriculture/doctype/crop_cycle/test_crop_cycle.js
+++ /dev/null
@@ -1,34 +0,0 @@
-/* eslint-disable */
-// rename this file from _test_[name] to test_[name] to activate
-// and remove above this line
-
-QUnit.test("test: Crop Cycle", function (assert) {
- let done = assert.async();
-
- // number of asserts
- assert.expect(1);
-
- frappe.run_serially([
- // insert a new Crop Cycle
- () => frappe.tests.make('Crop Cycle', [
- // values to be set
- {title: 'Basil from seed 2017'},
- {detected_disease: [
- [
- {start_date: '2017-11-21'},
- {disease: 'Aphids'}
- ]
- ]},
- {linked_land_unit: [
- [
- {land_unit: 'Basil Farm'}
- ]
- ]},
- {crop: 'Basil from seed'},
- {start_date: '2017-11-11'},
- {cycle_type: 'Less than a year'}
- ]),
- () => assert.equal(cur_frm.doc.name, 'Basil from seed 2017'),
- () => done()
- ]);
-});
diff --git a/erpnext/agriculture/doctype/crop_cycle/test_crop_cycle.py b/erpnext/agriculture/doctype/crop_cycle/test_crop_cycle.py
deleted file mode 100644
index e4765a5..0000000
--- a/erpnext/agriculture/doctype/crop_cycle/test_crop_cycle.py
+++ /dev/null
@@ -1,72 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
-
-import unittest
-
-import frappe
-from frappe.utils import datetime
-
-test_dependencies = ["Crop", "Fertilizer", "Location", "Disease"]
-
-
-class TestCropCycle(unittest.TestCase):
- def test_crop_cycle_creation(self):
- cycle = frappe.get_doc('Crop Cycle', 'Basil from seed 2017')
- self.assertTrue(frappe.db.exists('Crop Cycle', 'Basil from seed 2017'))
-
- # check if the tasks were created
- self.assertEqual(check_task_creation(), True)
- self.assertEqual(check_project_creation(), True)
-
-
-def check_task_creation():
- all_task_dict = {
- "Survey and find the aphid locations": {
- "exp_start_date": datetime.date(2017, 11, 21),
- "exp_end_date": datetime.date(2017, 11, 22)
- },
- "Apply Pesticides": {
- "exp_start_date": datetime.date(2017, 11, 23),
- "exp_end_date": datetime.date(2017, 11, 23)
- },
- "Plough the field": {
- "exp_start_date": datetime.date(2017, 11, 11),
- "exp_end_date": datetime.date(2017, 11, 11)
- },
- "Plant the seeds": {
- "exp_start_date": datetime.date(2017, 11, 12),
- "exp_end_date": datetime.date(2017, 11, 13)
- },
- "Water the field": {
- "exp_start_date": datetime.date(2017, 11, 14),
- "exp_end_date": datetime.date(2017, 11, 14)
- },
- "First harvest": {
- "exp_start_date": datetime.date(2017, 11, 18),
- "exp_end_date": datetime.date(2017, 11, 18)
- },
- "Add the fertilizer": {
- "exp_start_date": datetime.date(2017, 11, 20),
- "exp_end_date": datetime.date(2017, 11, 22)
- },
- "Final cut": {
- "exp_start_date": datetime.date(2017, 11, 25),
- "exp_end_date": datetime.date(2017, 11, 25)
- }
- }
-
- all_tasks = frappe.get_all('Task')
-
- for task in all_tasks:
- sample_task = frappe.get_doc('Task', task.name)
-
- if sample_task.subject in list(all_task_dict):
- if sample_task.exp_start_date != all_task_dict[sample_task.subject]['exp_start_date'] or sample_task.exp_end_date != all_task_dict[sample_task.subject]['exp_end_date']:
- return False
- all_task_dict.pop(sample_task.subject)
-
- return True if not all_task_dict else False
-
-
-def check_project_creation():
- return True if frappe.db.exists('Project', {'project_name': 'Basil from seed 2017'}) else False
diff --git a/erpnext/agriculture/doctype/crop_cycle/test_records.json b/erpnext/agriculture/doctype/crop_cycle/test_records.json
deleted file mode 100644
index 5c79f10..0000000
--- a/erpnext/agriculture/doctype/crop_cycle/test_records.json
+++ /dev/null
@@ -1,15 +0,0 @@
-[
- {
- "doctype": "Crop Cycle",
- "title": "Basil from seed 2017",
- "linked_location": [{
- "location": "Basil Farm"
- }],
- "crop": "Basil from seed",
- "start_date": "2017-11-11",
- "detected_disease": [{
- "disease": "Aphids",
- "start_date": "2017-11-21"
- }]
- }
-]
\ No newline at end of file
diff --git a/erpnext/agriculture/doctype/detected_disease/detected_disease.json b/erpnext/agriculture/doctype/detected_disease/detected_disease.json
deleted file mode 100644
index f670cd3..0000000
--- a/erpnext/agriculture/doctype/detected_disease/detected_disease.json
+++ /dev/null
@@ -1,142 +0,0 @@
-{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2017-11-20 17:31:30.772779",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "disease",
- "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": "Disease",
- "length": 0,
- "no_copy": 0,
- "options": "Disease",
- "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
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "start_date",
- "fieldtype": "Date",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Start Date",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "tasks_created",
- "fieldtype": "Check",
- "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": "Tasks Created",
- "length": 0,
- "no_copy": 1,
- "options": "",
- "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,
- "translatable": 0,
- "unique": 0
- }
- ],
- "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-11-04 03:27:47.463994",
- "modified_by": "Administrator",
- "module": "Agriculture",
- "name": "Detected Disease",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "restrict_to_domain": "Agriculture",
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
-}
\ No newline at end of file
diff --git a/erpnext/agriculture/doctype/detected_disease/detected_disease.py b/erpnext/agriculture/doctype/detected_disease/detected_disease.py
deleted file mode 100644
index e507add..0000000
--- a/erpnext/agriculture/doctype/detected_disease/detected_disease.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-from frappe.model.document import Document
-
-
-class DetectedDisease(Document):
- pass
diff --git a/erpnext/agriculture/doctype/disease/__init__.py b/erpnext/agriculture/doctype/disease/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/erpnext/agriculture/doctype/disease/__init__.py
+++ /dev/null
diff --git a/erpnext/agriculture/doctype/disease/disease.js b/erpnext/agriculture/doctype/disease/disease.js
deleted file mode 100644
index f6b678c..0000000
--- a/erpnext/agriculture/doctype/disease/disease.js
+++ /dev/null
@@ -1,8 +0,0 @@
-// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('Disease', {
- refresh: function(frm) {
-
- }
-});
diff --git a/erpnext/agriculture/doctype/disease/disease.json b/erpnext/agriculture/doctype/disease/disease.json
deleted file mode 100644
index 16b735a..0000000
--- a/erpnext/agriculture/doctype/disease/disease.json
+++ /dev/null
@@ -1,308 +0,0 @@
-{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "autoname": "field:common_name",
- "beta": 0,
- "creation": "2017-11-20 17:16:54.496355",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "common_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": "Common 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": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 1
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "scientific_name",
- "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": "Scientific 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
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "section_break_3",
- "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,
- "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": "treatment_task",
- "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": "Treatment Task",
- "length": 0,
- "no_copy": 0,
- "options": "Agriculture Task",
- "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": "treatment_period",
- "fieldtype": "Int",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Treatment Period",
- "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": "section_break_2",
- "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": "Treatment Task",
- "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": "description",
- "fieldtype": "Long Text",
- "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": "Description",
- "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
- }
- ],
- "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-11-04 03:27:25.076490",
- "modified_by": "Administrator",
- "module": "Agriculture",
- "name": "Disease",
- "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": "Agriculture Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 1
- },
- {
- "amend": 0,
- "cancel": 0,
- "create": 0,
- "delete": 0,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Agriculture User",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 1
- }
- ],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "restrict_to_domain": "Agriculture",
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
-}
\ No newline at end of file
diff --git a/erpnext/agriculture/doctype/disease/disease.py b/erpnext/agriculture/doctype/disease/disease.py
deleted file mode 100644
index 30ab298..0000000
--- a/erpnext/agriculture/doctype/disease/disease.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# Copyright (c) 2017, 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 Disease(Document):
- def validate(self):
- max_period = 0
- for task in self.treatment_task:
- # validate start_day is not > end_day
- if task.start_day > task.end_day:
- 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
diff --git a/erpnext/agriculture/doctype/disease/test_disease.js b/erpnext/agriculture/doctype/disease/test_disease.js
deleted file mode 100644
index 33f60c4..0000000
--- a/erpnext/agriculture/doctype/disease/test_disease.js
+++ /dev/null
@@ -1,38 +0,0 @@
-/* eslint-disable */
-// rename this file from _test_[name] to test_[name] to activate
-// and remove above this line
-
-QUnit.test("test: Disease", function (assert) {
- let done = assert.async();
-
- // number of asserts
- assert.expect(1);
-
- frappe.run_serially([
- // insert a new Disease
- () => frappe.tests.make('Disease', [
- // values to be set
- {common_name: 'Aphids'},
- {scientific_name: 'Aphidoidea'},
- {treatment_task: [
- [
- {task_name: "Survey and find the aphid locations"},
- {start_day: 1},
- {end_day: 2},
- {holiday_management: "Ignore holidays"}
- ],
- [
- {task_name: "Apply Pesticides"},
- {start_day: 3},
- {end_day: 3},
- {holiday_management: "Ignore holidays"}
- ]
- ]}
- ]),
- () => {
- assert.equal(cur_frm.doc.treatment_period, 3);
- },
- () => done()
- ]);
-
-});
diff --git a/erpnext/agriculture/doctype/disease/test_disease.py b/erpnext/agriculture/doctype/disease/test_disease.py
deleted file mode 100644
index 6a6f1e7..0000000
--- a/erpnext/agriculture/doctype/disease/test_disease.py
+++ /dev/null
@@ -1,12 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
-
-import unittest
-
-import frappe
-
-
-class TestDisease(unittest.TestCase):
- def test_treatment_period(self):
- disease = frappe.get_doc('Disease', 'Aphids')
- self.assertEqual(disease.treatment_period, 3)
diff --git a/erpnext/agriculture/doctype/disease/test_records.json b/erpnext/agriculture/doctype/disease/test_records.json
deleted file mode 100644
index e91a611..0000000
--- a/erpnext/agriculture/doctype/disease/test_records.json
+++ /dev/null
@@ -1,18 +0,0 @@
-[
- {
- "doctype": "Disease",
- "common_name": "Aphids",
- "scientific_name": "Aphidoidea",
- "treatment_task": [{
- "task_name": "Survey and find the aphid locations",
- "start_day": 1,
- "end_day": 2,
- "holiday_management": "Ignore holidays"
- }, {
- "task_name": "Apply Pesticides",
- "start_day": 3,
- "end_day": 3,
- "holiday_management": "Ignore holidays"
- }]
- }
-]
\ No newline at end of file
diff --git a/erpnext/agriculture/doctype/fertilizer/__init__.py b/erpnext/agriculture/doctype/fertilizer/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/erpnext/agriculture/doctype/fertilizer/__init__.py
+++ /dev/null
diff --git a/erpnext/agriculture/doctype/fertilizer/fertilizer.js b/erpnext/agriculture/doctype/fertilizer/fertilizer.js
deleted file mode 100644
index 357e089..0000000
--- a/erpnext/agriculture/doctype/fertilizer/fertilizer.js
+++ /dev/null
@@ -1,8 +0,0 @@
-// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('Fertilizer', {
- onload: (frm) => {
- if (frm.doc.fertilizer_contents == undefined) frm.call('load_contents');
- }
-});
diff --git a/erpnext/agriculture/doctype/fertilizer/fertilizer.json b/erpnext/agriculture/doctype/fertilizer/fertilizer.json
deleted file mode 100644
index 6a18773..0000000
--- a/erpnext/agriculture/doctype/fertilizer/fertilizer.json
+++ /dev/null
@@ -1,307 +0,0 @@
-{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "autoname": "field:fertilizer_name",
- "beta": 0,
- "creation": "2017-10-17 18:17:06.175062",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "fertilizer_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": "Fertilizer 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": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 1
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "item",
- "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",
- "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,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "section_break_2",
- "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,
- "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": "density",
- "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": "Density (if liquid)",
- "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": "section_break_4",
- "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,
- "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": "section_break_28",
- "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": "Fertilizer Contents",
- "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": "fertilizer_contents",
- "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,
- "length": 0,
- "no_copy": 0,
- "options": "Fertilizer Content",
- "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
- }
- ],
- "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-11-04 03:26:29.211792",
- "modified_by": "Administrator",
- "module": "Agriculture",
- "name": "Fertilizer",
- "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": "Agriculture Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 1
- },
- {
- "amend": 0,
- "cancel": 0,
- "create": 0,
- "delete": 0,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Agriculture User",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 1
- }
- ],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "restrict_to_domain": "Agriculture",
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
-}
\ No newline at end of file
diff --git a/erpnext/agriculture/doctype/fertilizer/fertilizer.py b/erpnext/agriculture/doctype/fertilizer/fertilizer.py
deleted file mode 100644
index 2408302..0000000
--- a/erpnext/agriculture/doctype/fertilizer/fertilizer.py
+++ /dev/null
@@ -1,14 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-import frappe
-from frappe.model.document import Document
-
-
-class Fertilizer(Document):
- @frappe.whitelist()
- 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)})
diff --git a/erpnext/agriculture/doctype/fertilizer/test_fertilizer.js b/erpnext/agriculture/doctype/fertilizer/test_fertilizer.js
deleted file mode 100644
index 5dd7313..0000000
--- a/erpnext/agriculture/doctype/fertilizer/test_fertilizer.js
+++ /dev/null
@@ -1,31 +0,0 @@
-/* eslint-disable */
-// rename this file from _test_[name] to test_[name] to activate
-// and remove above this line
-
-QUnit.test("test: Fertilizer", 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
- {item_code: 'Urea'},
- {item_name: 'Urea'},
- {item_group: 'Fertilizer'}
- ]),
- // insert a new Fertilizer
- () => frappe.tests.make('Fertilizer', [
- // values to be set
- {fertilizer_name: 'Urea'},
- {item: 'Urea'}
- ]),
- () => {
- assert.equal(cur_frm.doc.name, 'Urea');
- },
- () => done()
- ]);
-
-});
diff --git a/erpnext/agriculture/doctype/fertilizer/test_fertilizer.py b/erpnext/agriculture/doctype/fertilizer/test_fertilizer.py
deleted file mode 100644
index c8630ef..0000000
--- a/erpnext/agriculture/doctype/fertilizer/test_fertilizer.py
+++ /dev/null
@@ -1,11 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
-
-import unittest
-
-import frappe
-
-
-class TestFertilizer(unittest.TestCase):
- def test_fertilizer_creation(self):
- self.assertEqual(frappe.db.exists('Fertilizer', 'Urea'), 'Urea')
diff --git a/erpnext/agriculture/doctype/fertilizer/test_records.json b/erpnext/agriculture/doctype/fertilizer/test_records.json
deleted file mode 100644
index ba735cd..0000000
--- a/erpnext/agriculture/doctype/fertilizer/test_records.json
+++ /dev/null
@@ -1,13 +0,0 @@
-[
- {
- "doctype": "Item",
- "item_code": "Urea",
- "item_name": "Urea",
- "item_group": "Fertilizer"
- },
- {
- "doctype": "Fertilizer",
- "fertilizer_name": "Urea",
- "item": "Urea"
- }
-]
\ No newline at end of file
diff --git a/erpnext/agriculture/doctype/fertilizer_content/__init__.py b/erpnext/agriculture/doctype/fertilizer_content/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/erpnext/agriculture/doctype/fertilizer_content/__init__.py
+++ /dev/null
diff --git a/erpnext/agriculture/doctype/fertilizer_content/fertilizer_content.json b/erpnext/agriculture/doctype/fertilizer_content/fertilizer_content.json
deleted file mode 100644
index bf222ab..0000000
--- a/erpnext/agriculture/doctype/fertilizer_content/fertilizer_content.json
+++ /dev/null
@@ -1,103 +0,0 @@
-{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2017-12-05 16:54:17.071914",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "title",
- "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": "Title",
- "length": 0,
- "no_copy": 0,
- "options": "Agriculture Analysis Criteria",
- "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
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "value",
- "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": "Value",
- "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
- }
- ],
- "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-12-05 19:20:38.892231",
- "modified_by": "Administrator",
- "module": "Agriculture",
- "name": "Fertilizer Content",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "restrict_to_domain": "Agriculture",
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0
-}
\ No newline at end of file
diff --git a/erpnext/agriculture/doctype/fertilizer_content/fertilizer_content.py b/erpnext/agriculture/doctype/fertilizer_content/fertilizer_content.py
deleted file mode 100644
index 967c3e0..0000000
--- a/erpnext/agriculture/doctype/fertilizer_content/fertilizer_content.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-from frappe.model.document import Document
-
-
-class FertilizerContent(Document):
- pass
diff --git a/erpnext/agriculture/doctype/linked_location/__init__.py b/erpnext/agriculture/doctype/linked_location/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/erpnext/agriculture/doctype/linked_location/__init__.py
+++ /dev/null
diff --git a/erpnext/agriculture/doctype/linked_location/linked_location.json b/erpnext/agriculture/doctype/linked_location/linked_location.json
deleted file mode 100644
index a14ae3d..0000000
--- a/erpnext/agriculture/doctype/linked_location/linked_location.json
+++ /dev/null
@@ -1,77 +0,0 @@
-{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2017-11-22 14:34:59.461273",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "location",
- "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": "Location",
- "length": 0,
- "no_copy": 0,
- "options": "Location",
- "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
- }
- ],
- "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-11-04 03:27:58.120962",
- "modified_by": "Administrator",
- "module": "Agriculture",
- "name": "Linked Location",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "restrict_to_domain": "Agriculture",
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
-}
\ No newline at end of file
diff --git a/erpnext/agriculture/doctype/linked_location/linked_location.py b/erpnext/agriculture/doctype/linked_location/linked_location.py
deleted file mode 100644
index e1257f3..0000000
--- a/erpnext/agriculture/doctype/linked_location/linked_location.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-from frappe.model.document import Document
-
-
-class LinkedLocation(Document):
- pass
diff --git a/erpnext/agriculture/doctype/linked_plant_analysis/linked_plant_analysis.json b/erpnext/agriculture/doctype/linked_plant_analysis/linked_plant_analysis.json
deleted file mode 100644
index 57d2aab..0000000
--- a/erpnext/agriculture/doctype/linked_plant_analysis/linked_plant_analysis.json
+++ /dev/null
@@ -1,77 +0,0 @@
-{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2017-11-22 15:04:25.180446",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "plant_analysis",
- "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": "Plant Analysis",
- "length": 0,
- "no_copy": 0,
- "options": "Plant Analysis",
- "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
- }
- ],
- "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-11-04 03:25:15.359130",
- "modified_by": "Administrator",
- "module": "Agriculture",
- "name": "Linked Plant Analysis",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "restrict_to_domain": "Agriculture",
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
-}
\ No newline at end of file
diff --git a/erpnext/agriculture/doctype/linked_plant_analysis/linked_plant_analysis.py b/erpnext/agriculture/doctype/linked_plant_analysis/linked_plant_analysis.py
deleted file mode 100644
index 0bc04af..0000000
--- a/erpnext/agriculture/doctype/linked_plant_analysis/linked_plant_analysis.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-from frappe.model.document import Document
-
-
-class LinkedPlantAnalysis(Document):
- pass
diff --git a/erpnext/agriculture/doctype/linked_soil_analysis/__init__.py b/erpnext/agriculture/doctype/linked_soil_analysis/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/erpnext/agriculture/doctype/linked_soil_analysis/__init__.py
+++ /dev/null
diff --git a/erpnext/agriculture/doctype/linked_soil_analysis/linked_soil_analysis.json b/erpnext/agriculture/doctype/linked_soil_analysis/linked_soil_analysis.json
deleted file mode 100644
index 38e5030..0000000
--- a/erpnext/agriculture/doctype/linked_soil_analysis/linked_soil_analysis.json
+++ /dev/null
@@ -1,77 +0,0 @@
-{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2017-11-22 15:00:37.259063",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "soil_analysis",
- "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": "Soil Analysis",
- "length": 0,
- "no_copy": 0,
- "options": "Soil Analysis",
- "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
- }
- ],
- "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-11-04 03:25:27.670079",
- "modified_by": "Administrator",
- "module": "Agriculture",
- "name": "Linked Soil Analysis",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "restrict_to_domain": "Agriculture",
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
-}
\ No newline at end of file
diff --git a/erpnext/agriculture/doctype/linked_soil_analysis/linked_soil_analysis.py b/erpnext/agriculture/doctype/linked_soil_analysis/linked_soil_analysis.py
deleted file mode 100644
index 0d29055..0000000
--- a/erpnext/agriculture/doctype/linked_soil_analysis/linked_soil_analysis.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-from frappe.model.document import Document
-
-
-class LinkedSoilAnalysis(Document):
- pass
diff --git a/erpnext/agriculture/doctype/linked_soil_texture/__init__.py b/erpnext/agriculture/doctype/linked_soil_texture/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/erpnext/agriculture/doctype/linked_soil_texture/__init__.py
+++ /dev/null
diff --git a/erpnext/agriculture/doctype/linked_soil_texture/linked_soil_texture.json b/erpnext/agriculture/doctype/linked_soil_texture/linked_soil_texture.json
deleted file mode 100644
index 80682b0..0000000
--- a/erpnext/agriculture/doctype/linked_soil_texture/linked_soil_texture.json
+++ /dev/null
@@ -1,77 +0,0 @@
-{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2017-11-22 14:58:52.818040",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "soil_texture",
- "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": "Soil Texture",
- "length": 0,
- "no_copy": 0,
- "options": "Soil Texture",
- "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
- }
- ],
- "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-11-04 03:26:17.877616",
- "modified_by": "Administrator",
- "module": "Agriculture",
- "name": "Linked Soil Texture",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "restrict_to_domain": "Agriculture",
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
-}
\ No newline at end of file
diff --git a/erpnext/agriculture/doctype/linked_soil_texture/linked_soil_texture.py b/erpnext/agriculture/doctype/linked_soil_texture/linked_soil_texture.py
deleted file mode 100644
index 1438853..0000000
--- a/erpnext/agriculture/doctype/linked_soil_texture/linked_soil_texture.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-from frappe.model.document import Document
-
-
-class LinkedSoilTexture(Document):
- pass
diff --git a/erpnext/agriculture/doctype/plant_analysis/__init__.py b/erpnext/agriculture/doctype/plant_analysis/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/erpnext/agriculture/doctype/plant_analysis/__init__.py
+++ /dev/null
diff --git a/erpnext/agriculture/doctype/plant_analysis/plant_analysis.js b/erpnext/agriculture/doctype/plant_analysis/plant_analysis.js
deleted file mode 100644
index 3914f83..0000000
--- a/erpnext/agriculture/doctype/plant_analysis/plant_analysis.js
+++ /dev/null
@@ -1,17 +0,0 @@
-// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('Plant Analysis', {
- onload: (frm) => {
- if (frm.doc.plant_analysis_criteria == undefined) frm.call('load_contents');
- },
- refresh: (frm) => {
- let map_tools = ["a.leaflet-draw-draw-polyline",
- "a.leaflet-draw-draw-polygon",
- "a.leaflet-draw-draw-rectangle",
- "a.leaflet-draw-draw-circle",
- "a.leaflet-draw-draw-circlemarker"];
-
- map_tools.forEach((element) => $(element).hide());
- }
-});
diff --git a/erpnext/agriculture/doctype/plant_analysis/plant_analysis.json b/erpnext/agriculture/doctype/plant_analysis/plant_analysis.json
deleted file mode 100644
index ceb1a5b..0000000
--- a/erpnext/agriculture/doctype/plant_analysis/plant_analysis.json
+++ /dev/null
@@ -1,372 +0,0 @@
-{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "autoname": "AG-PLA-.YYYY.-.#####",
- "beta": 0,
- "creation": "2017-10-18 12:45:13.575986",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "crop",
- "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": "Crop",
- "length": 0,
- "no_copy": 0,
- "options": "Crop",
- "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
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 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,
- "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": "location",
- "fieldtype": "Geolocation",
- "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": "Location",
- "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": "column_break_2",
- "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
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "collection_datetime",
- "fieldtype": "Datetime",
- "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": "Collection Datetime",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "laboratory_testing_datetime",
- "fieldtype": "Datetime",
- "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": "Laboratory Testing Datetime",
- "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": "result_datetime",
- "fieldtype": "Datetime",
- "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": "Result Datetime",
- "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": "section_break_2",
- "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": "Plant Analysis Criterias",
- "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": "plant_analysis_criteria",
- "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": "",
- "length": 0,
- "no_copy": 0,
- "options": "Plant Analysis Criteria",
- "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
- }
- ],
- "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-11-04 03:28:48.087828",
- "modified_by": "Administrator",
- "module": "Agriculture",
- "name": "Plant Analysis",
- "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": "Agriculture Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 1
- },
- {
- "amend": 0,
- "cancel": 0,
- "create": 0,
- "delete": 0,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Agriculture User",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 1
- }
- ],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "restrict_to_domain": "Agriculture",
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
-}
\ No newline at end of file
diff --git a/erpnext/agriculture/doctype/plant_analysis/plant_analysis.py b/erpnext/agriculture/doctype/plant_analysis/plant_analysis.py
deleted file mode 100644
index 9a939cd..0000000
--- a/erpnext/agriculture/doctype/plant_analysis/plant_analysis.py
+++ /dev/null
@@ -1,14 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-import frappe
-from frappe.model.document import Document
-
-
-class PlantAnalysis(Document):
- @frappe.whitelist()
- 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)})
diff --git a/erpnext/agriculture/doctype/plant_analysis/test_plant_analysis.py b/erpnext/agriculture/doctype/plant_analysis/test_plant_analysis.py
deleted file mode 100644
index cee241f..0000000
--- a/erpnext/agriculture/doctype/plant_analysis/test_plant_analysis.py
+++ /dev/null
@@ -1,8 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
-
-import unittest
-
-
-class TestPlantAnalysis(unittest.TestCase):
- pass
diff --git a/erpnext/agriculture/doctype/plant_analysis_criteria/__init__.py b/erpnext/agriculture/doctype/plant_analysis_criteria/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/erpnext/agriculture/doctype/plant_analysis_criteria/__init__.py
+++ /dev/null
diff --git a/erpnext/agriculture/doctype/plant_analysis_criteria/plant_analysis_criteria.json b/erpnext/agriculture/doctype/plant_analysis_criteria/plant_analysis_criteria.json
deleted file mode 100644
index eefc5ee..0000000
--- a/erpnext/agriculture/doctype/plant_analysis_criteria/plant_analysis_criteria.json
+++ /dev/null
@@ -1,173 +0,0 @@
-{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2017-12-05 19:23:52.481348",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "title",
- "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": "Title",
- "length": 0,
- "no_copy": 0,
- "options": "Agriculture Analysis Criteria",
- "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
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "value",
- "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": "Value",
- "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": "minimum_permissible_value",
- "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": "Minimum Permissible Value",
- "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": "maximum_permissible_value",
- "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": "Maximum Permissible Value",
- "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
- }
- ],
- "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-11-04 03:25:43.714882",
- "modified_by": "Administrator",
- "module": "Agriculture",
- "name": "Plant Analysis Criteria",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "restrict_to_domain": "Agriculture",
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
-}
\ No newline at end of file
diff --git a/erpnext/agriculture/doctype/plant_analysis_criteria/plant_analysis_criteria.py b/erpnext/agriculture/doctype/plant_analysis_criteria/plant_analysis_criteria.py
deleted file mode 100644
index 7e6571c..0000000
--- a/erpnext/agriculture/doctype/plant_analysis_criteria/plant_analysis_criteria.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-from frappe.model.document import Document
-
-
-class PlantAnalysisCriteria(Document):
- pass
diff --git a/erpnext/agriculture/doctype/soil_analysis/__init__.py b/erpnext/agriculture/doctype/soil_analysis/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/erpnext/agriculture/doctype/soil_analysis/__init__.py
+++ /dev/null
diff --git a/erpnext/agriculture/doctype/soil_analysis/soil_analysis.js b/erpnext/agriculture/doctype/soil_analysis/soil_analysis.js
deleted file mode 100644
index 12829be..0000000
--- a/erpnext/agriculture/doctype/soil_analysis/soil_analysis.js
+++ /dev/null
@@ -1,17 +0,0 @@
-// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('Soil Analysis', {
- onload: (frm) => {
- if (frm.doc.soil_analysis_criteria == undefined) frm.call('load_contents');
- },
- refresh: (frm) => {
- let map_tools = ["a.leaflet-draw-draw-polyline",
- "a.leaflet-draw-draw-polygon",
- "a.leaflet-draw-draw-rectangle",
- "a.leaflet-draw-draw-circle",
- "a.leaflet-draw-draw-circlemarker"];
-
- map_tools.forEach((element) => $(element).hide());
- }
-});
diff --git a/erpnext/agriculture/doctype/soil_analysis/soil_analysis.json b/erpnext/agriculture/doctype/soil_analysis/soil_analysis.json
deleted file mode 100644
index 59680fa..0000000
--- a/erpnext/agriculture/doctype/soil_analysis/soil_analysis.json
+++ /dev/null
@@ -1,593 +0,0 @@
-{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "autoname": "AG-ANA-.YY.-.MM.-.#####",
- "beta": 0,
- "creation": "2017-10-17 19:12:16.728395",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "location",
- "fieldtype": "Geolocation",
- "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": "Location",
- "length": 0,
- "no_copy": 0,
- "options": "",
- "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": "column_break_2",
- "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
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "collection_datetime",
- "fieldtype": "Datetime",
- "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": "Collection Datetime",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "laboratory_testing_datetime",
- "fieldtype": "Datetime",
- "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": "Laboratory Testing Datetime",
- "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": "result_datetime",
- "fieldtype": "Datetime",
- "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": "Result Datetime",
- "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": "section_break_3",
- "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,
- "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": "ca_per_k",
- "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": "Ca/K",
- "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,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "ca_per_mg",
- "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": "Ca/Mg",
- "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,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "mg_per_k",
- "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": "Mg/K",
- "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,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_31",
- "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
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "ca_mg_per_k",
- "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": "(Ca+Mg)/K",
- "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,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "ca_per_k_ca_mg",
- "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": "Ca/(K+Ca+Mg)",
- "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,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "section_break_28",
- "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,
- "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": "invoice_number",
- "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": "Invoice Number",
- "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": "soil_analysis_criterias",
- "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": "Soil Analysis Criterias",
- "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": "soil_analysis_criteria",
- "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,
- "length": 0,
- "no_copy": 0,
- "options": "Soil Analysis Criteria",
- "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
- }
- ],
- "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-11-04 03:28:58.403760",
- "modified_by": "Administrator",
- "module": "Agriculture",
- "name": "Soil Analysis",
- "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": "Agriculture Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 1
- },
- {
- "amend": 0,
- "cancel": 0,
- "create": 0,
- "delete": 0,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Agriculture User",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 1
- }
- ],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "restrict_to_domain": "Agriculture",
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
-}
\ No newline at end of file
diff --git a/erpnext/agriculture/doctype/soil_analysis/soil_analysis.py b/erpnext/agriculture/doctype/soil_analysis/soil_analysis.py
deleted file mode 100644
index 03667fb..0000000
--- a/erpnext/agriculture/doctype/soil_analysis/soil_analysis.py
+++ /dev/null
@@ -1,14 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-import frappe
-from frappe.model.document import Document
-
-
-class SoilAnalysis(Document):
- @frappe.whitelist()
- 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)})
diff --git a/erpnext/agriculture/doctype/soil_analysis/test_soil_analysis.py b/erpnext/agriculture/doctype/soil_analysis/test_soil_analysis.py
deleted file mode 100644
index bb99363..0000000
--- a/erpnext/agriculture/doctype/soil_analysis/test_soil_analysis.py
+++ /dev/null
@@ -1,8 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
-
-import unittest
-
-
-class TestSoilAnalysis(unittest.TestCase):
- pass
diff --git a/erpnext/agriculture/doctype/soil_analysis_criteria/__init__.py b/erpnext/agriculture/doctype/soil_analysis_criteria/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/erpnext/agriculture/doctype/soil_analysis_criteria/__init__.py
+++ /dev/null
diff --git a/erpnext/agriculture/doctype/soil_analysis_criteria/soil_analysis_criteria.json b/erpnext/agriculture/doctype/soil_analysis_criteria/soil_analysis_criteria.json
deleted file mode 100644
index 860e48a..0000000
--- a/erpnext/agriculture/doctype/soil_analysis_criteria/soil_analysis_criteria.json
+++ /dev/null
@@ -1,173 +0,0 @@
-{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2017-12-05 19:36:05.300770",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "title",
- "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": "Title",
- "length": 0,
- "no_copy": 0,
- "options": "Agriculture Analysis Criteria",
- "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
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "value",
- "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": "Value",
- "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": "minimum_permissible_value",
- "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": "Minimum Permissible Value",
- "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": "maximum_permissible_value",
- "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": "Maximum Permissible Value",
- "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
- }
- ],
- "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-11-04 03:25:54.359008",
- "modified_by": "Administrator",
- "module": "Agriculture",
- "name": "Soil Analysis Criteria",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "restrict_to_domain": "Agriculture",
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
-}
\ No newline at end of file
diff --git a/erpnext/agriculture/doctype/soil_analysis_criteria/soil_analysis_criteria.py b/erpnext/agriculture/doctype/soil_analysis_criteria/soil_analysis_criteria.py
deleted file mode 100644
index f501820..0000000
--- a/erpnext/agriculture/doctype/soil_analysis_criteria/soil_analysis_criteria.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-from frappe.model.document import Document
-
-
-class SoilAnalysisCriteria(Document):
- pass
diff --git a/erpnext/agriculture/doctype/soil_texture/__init__.py b/erpnext/agriculture/doctype/soil_texture/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/erpnext/agriculture/doctype/soil_texture/__init__.py
+++ /dev/null
diff --git a/erpnext/agriculture/doctype/soil_texture/soil_texture.js b/erpnext/agriculture/doctype/soil_texture/soil_texture.js
deleted file mode 100644
index 673284b..0000000
--- a/erpnext/agriculture/doctype/soil_texture/soil_texture.js
+++ /dev/null
@@ -1,59 +0,0 @@
-// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-// For license information, please see license.txt
-
-frappe.provide('agriculture');
-
-frappe.ui.form.on('Soil Texture', {
- refresh: (frm) => {
- let map_tools = ["a.leaflet-draw-draw-polyline",
- "a.leaflet-draw-draw-polygon",
- "a.leaflet-draw-draw-rectangle",
- "a.leaflet-draw-draw-circle",
- "a.leaflet-draw-draw-circlemarker"];
-
- map_tools.forEach((element) => $(element).hide());
- },
- onload: function(frm) {
- if (frm.doc.soil_texture_criteria == undefined) frm.call('load_contents');
- if (frm.doc.ternary_plot) return;
- frm.doc.ternary_plot = new agriculture.TernaryPlot({
- parent: frm.get_field("ternary_plot").$wrapper,
- clay: frm.doc.clay_composition,
- sand: frm.doc.sand_composition,
- silt: frm.doc.silt_composition,
- });
- },
- soil_type: (frm) => {
- let composition_types = ['clay_composition', 'sand_composition', 'silt_composition'];
- composition_types.forEach((composition_type) => {
- frm.doc[composition_type] = 0;
- frm.refresh_field(composition_type);
- });
- },
- clay_composition: function(frm) {
- frm.call("update_soil_edit", {
- soil_type: 'clay_composition'
- }, () => {
- refresh_ternary_plot(frm, this);
- });
- },
- sand_composition: function(frm) {
- frm.call("update_soil_edit", {
- soil_type: 'sand_composition'
- }, () => {
- refresh_ternary_plot(frm, this);
- });
- },
- silt_composition: function(frm) {
- frm.call("update_soil_edit", {
- soil_type: 'silt_composition'
- }, () => {
- refresh_ternary_plot(frm, this);
- });
- }
-});
-
-let refresh_ternary_plot = (frm, me) => {
- me.ternary_plot.remove_blip();
- me.ternary_plot.mark_blip({clay: frm.doc.clay_composition, sand: frm.doc.sand_composition, silt: frm.doc.silt_composition});
-};
diff --git a/erpnext/agriculture/doctype/soil_texture/soil_texture.json b/erpnext/agriculture/doctype/soil_texture/soil_texture.json
deleted file mode 100644
index f78c262..0000000
--- a/erpnext/agriculture/doctype/soil_texture/soil_texture.json
+++ /dev/null
@@ -1,533 +0,0 @@
-{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "autoname": "AG-TEX-.YYYY.-.#####",
- "beta": 0,
- "creation": "2017-10-18 13:06:47.506762",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "location",
- "fieldtype": "Geolocation",
- "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": "Location",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_2",
- "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
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "collection_datetime",
- "fieldtype": "Datetime",
- "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": "Collection Datetime",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "laboratory_testing_datetime",
- "fieldtype": "Datetime",
- "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": "Laboratory Testing Datetime",
- "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": "result_datetime",
- "fieldtype": "Datetime",
- "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": "Result Datetime",
- "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": "section_break_4",
- "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,
- "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": "soil_type",
- "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": "Soil Type",
- "length": 0,
- "no_copy": 0,
- "options": "Select\nSand\nLoamy Sand\nSandy Loam\nLoam\nSilt Loam\nSilt\nSandy Clay Loam\nClay Loam\nSilty Clay Loam\nSandy Clay\nSilty Clay\nClay",
- "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,
- "default": "0",
- "fieldname": "clay_composition",
- "fieldtype": "Percent",
- "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": "Clay Composition (%)",
- "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,
- "default": "0",
- "fieldname": "sand_composition",
- "fieldtype": "Percent",
- "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": "Sand Composition (%)",
- "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,
- "default": "0",
- "fieldname": "silt_composition",
- "fieldtype": "Percent",
- "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": "Silt Composition (%)",
- "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": "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,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "ternary_plot",
- "fieldtype": "HTML",
- "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": "Ternary Plot",
- "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": "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": "Soil Texture Criteria",
- "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": "soil_texture_criteria",
- "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,
- "length": 0,
- "no_copy": 0,
- "options": "Soil Texture Criteria",
- "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
- }
- ],
- "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-11-04 03:29:18.221173",
- "modified_by": "Administrator",
- "module": "Agriculture",
- "name": "Soil Texture",
- "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": "Agriculture Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 1
- },
- {
- "amend": 0,
- "cancel": 0,
- "create": 0,
- "delete": 0,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Agriculture User",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 1
- }
- ],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "restrict_to_domain": "Agriculture",
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
-}
\ No newline at end of file
diff --git a/erpnext/agriculture/doctype/soil_texture/soil_texture.py b/erpnext/agriculture/doctype/soil_texture/soil_texture.py
deleted file mode 100644
index b1fc9a0..0000000
--- a/erpnext/agriculture/doctype/soil_texture/soil_texture.py
+++ /dev/null
@@ -1,71 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-import frappe
-from frappe import _
-from frappe.model.document import Document
-from frappe.utils import cint, flt
-
-
-class SoilTexture(Document):
- soil_edit_order = [2, 1, 0]
- soil_types = ['clay_composition', 'sand_composition', 'silt_composition']
-
- @frappe.whitelist()
- def load_contents(self):
- docs = frappe.get_all("Agriculture Analysis Criteria", filters={'linked_doctype':'Soil Texture'})
- for doc in docs:
- self.append('soil_texture_criteria', {'title': str(doc.name)})
-
- def validate(self):
- self.update_soil_edit('sand_composition')
- for soil_type in self.soil_types:
- if self.get(soil_type) > 100 or self.get(soil_type) < 0:
- frappe.throw(_("{0} should be a value between 0 and 100").format(soil_type))
- if sum(self.get(soil_type) for soil_type in self.soil_types) != 100:
- frappe.throw(_('Soil compositions do not add up to 100'))
-
- @frappe.whitelist()
- def update_soil_edit(self, soil_type):
- self.soil_edit_order[self.soil_types.index(soil_type)] = max(self.soil_edit_order)+1
- self.soil_type = self.get_soil_type()
-
- def get_soil_type(self):
- # update the last edited soil type
- if sum(self.soil_edit_order) < 5: return
- last_edit_index = self.soil_edit_order.index(min(self.soil_edit_order))
-
- # set composition of the last edited soil
- self.set(self.soil_types[last_edit_index],
- 100 - sum(cint(self.get(soil_type)) for soil_type in self.soil_types) + cint(self.get(self.soil_types[last_edit_index])))
-
- # calculate soil type
- c, sa, si = flt(self.clay_composition), flt(self.sand_composition), flt(self.silt_composition)
-
- if si + (1.5 * c) < 15:
- return 'Sand'
- elif si + 1.5 * c >= 15 and si + 2 * c < 30:
- return 'Loamy Sand'
- elif ((c >= 7 and c < 20) or (sa > 52) and ((si + 2*c) >= 30) or (c < 7 and si < 50 and (si+2*c) >= 30)):
- return 'Sandy Loam'
- elif ((c >= 7 and c < 27) and (si >= 28 and si < 50) and (sa <= 52)):
- return 'Loam'
- elif ((si >= 50 and (c >= 12 and c < 27)) or ((si >= 50 and si < 80) and c < 12)):
- return 'Silt Loam'
- elif (si >= 80 and c < 12):
- return 'Silt'
- elif ((c >= 20 and c < 35) and (si < 28) and (sa > 45)):
- return 'Sandy Clay Loam'
- elif ((c >= 27 and c < 40) and (sa > 20 and sa <= 45)):
- return 'Clay Loam'
- elif ((c >= 27 and c < 40) and (sa <= 20)):
- return 'Silty Clay Loam'
- elif (c >= 35 and sa > 45):
- return 'Sandy Clay'
- elif (c >= 40 and si >= 40):
- return 'Silty Clay'
- elif (c >= 40 and sa <= 45 and si < 40):
- return 'Clay'
- else:
- return 'Select'
diff --git a/erpnext/agriculture/doctype/soil_texture/test_records.json b/erpnext/agriculture/doctype/soil_texture/test_records.json
deleted file mode 100644
index dcac7ad..0000000
--- a/erpnext/agriculture/doctype/soil_texture/test_records.json
+++ /dev/null
@@ -1,9 +0,0 @@
-[
- {
- "doctype": "Soil Texture",
- "location": "{\"type\":\"FeatureCollection\",\"features\":[{\"type\":\"Feature\",\"properties\":{},\"geometry\":{\"type\":\"Point\",\"coordinates\":[72.861242,19.079153]}}]}",
- "collection_datetime": "2017-11-08",
- "clay_composition": 20,
- "sand_composition": 30
- }
-]
\ No newline at end of file
diff --git a/erpnext/agriculture/doctype/soil_texture/test_soil_texture.js b/erpnext/agriculture/doctype/soil_texture/test_soil_texture.js
deleted file mode 100644
index d93f852..0000000
--- a/erpnext/agriculture/doctype/soil_texture/test_soil_texture.js
+++ /dev/null
@@ -1,26 +0,0 @@
-/* eslint-disable */
-// rename this file from _test_[name] to test_[name] to activate
-// and remove above this line
-
-QUnit.test("test: Soil Texture", function (assert) {
- let done = assert.async();
-
- // number of asserts
- assert.expect(2);
-
- frappe.run_serially([
- // insert a new Soil Texture
- () => frappe.tests.make('Soil Texture', [
- // values to be set
- {location: '{"type":"FeatureCollection","features":[{"type":"Feature","properties":{},"geometry":{"type":"Point","coordinates":[72.882185,19.076395]}}]}'},
- {collection_datetime: '2017-11-08'},
- {clay_composition: 20},
- {sand_composition: 30}
- ]),
- () => {
- assert.equal(cur_frm.doc.silt_composition, 50);
- assert.equal(cur_frm.doc.soil_type, 'Silt Loam');
- },
- () => done()
- ]);
-});
diff --git a/erpnext/agriculture/doctype/soil_texture/test_soil_texture.py b/erpnext/agriculture/doctype/soil_texture/test_soil_texture.py
deleted file mode 100644
index 4549767..0000000
--- a/erpnext/agriculture/doctype/soil_texture/test_soil_texture.py
+++ /dev/null
@@ -1,14 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
-
-import unittest
-
-import frappe
-
-
-class TestSoilTexture(unittest.TestCase):
- def test_texture_selection(self):
- 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')
diff --git a/erpnext/agriculture/doctype/soil_texture_criteria/__init__.py b/erpnext/agriculture/doctype/soil_texture_criteria/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/erpnext/agriculture/doctype/soil_texture_criteria/__init__.py
+++ /dev/null
diff --git a/erpnext/agriculture/doctype/soil_texture_criteria/soil_texture_criteria.json b/erpnext/agriculture/doctype/soil_texture_criteria/soil_texture_criteria.json
deleted file mode 100644
index 0cd72b0..0000000
--- a/erpnext/agriculture/doctype/soil_texture_criteria/soil_texture_criteria.json
+++ /dev/null
@@ -1,173 +0,0 @@
-{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2017-12-05 23:45:17.419610",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "title",
- "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": "Title",
- "length": 0,
- "no_copy": 0,
- "options": "Agriculture Analysis Criteria",
- "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
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "value",
- "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": "Value",
- "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": "minimum_permissible_value",
- "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": "Minimum Permissible Value",
- "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": "maximum_permissible_value",
- "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": "Maximum Permissible Value",
- "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
- }
- ],
- "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-11-04 03:26:46.178377",
- "modified_by": "Administrator",
- "module": "Agriculture",
- "name": "Soil Texture Criteria",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "restrict_to_domain": "Agriculture",
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
-}
\ No newline at end of file
diff --git a/erpnext/agriculture/doctype/soil_texture_criteria/soil_texture_criteria.py b/erpnext/agriculture/doctype/soil_texture_criteria/soil_texture_criteria.py
deleted file mode 100644
index 92a0cf9..0000000
--- a/erpnext/agriculture/doctype/soil_texture_criteria/soil_texture_criteria.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-from frappe.model.document import Document
-
-
-class SoilTextureCriteria(Document):
- pass
diff --git a/erpnext/agriculture/doctype/water_analysis/__init__.py b/erpnext/agriculture/doctype/water_analysis/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/erpnext/agriculture/doctype/water_analysis/__init__.py
+++ /dev/null
diff --git a/erpnext/agriculture/doctype/water_analysis/test_water_analysis.js b/erpnext/agriculture/doctype/water_analysis/test_water_analysis.js
deleted file mode 100644
index bb01cb3..0000000
--- a/erpnext/agriculture/doctype/water_analysis/test_water_analysis.js
+++ /dev/null
@@ -1,25 +0,0 @@
-/* eslint-disable */
-// rename this file from _test_[name] to test_[name] to activate
-// and remove above this line
-
-QUnit.test("test: Water Analysis", function (assert) {
- let done = assert.async();
-
- // number of asserts
- assert.expect(1);
-
- frappe.run_serially([
- // insert a new Water Analysis
- () => frappe.tests.make('Water Analysis', [
- // values to be set
- {location: '{"type":"FeatureCollection","features":[{"type":"Feature","properties":{},"geometry":{"type":"Point","coordinates":[72.882185,19.076395]}}]}'},
- {collection_datetime: '2017-11-08 18:43:57'},
- {laboratory_testing_datetime: '2017-11-10 18:43:57'}
- ]),
- () => {
- assert.equal(cur_frm.doc.result_datetime, '2017-11-10 18:43:57');
- },
- () => done()
- ]);
-
-});
diff --git a/erpnext/agriculture/doctype/water_analysis/test_water_analysis.py b/erpnext/agriculture/doctype/water_analysis/test_water_analysis.py
deleted file mode 100644
index ae144cc..0000000
--- a/erpnext/agriculture/doctype/water_analysis/test_water_analysis.py
+++ /dev/null
@@ -1,8 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
-
-import unittest
-
-
-class TestWaterAnalysis(unittest.TestCase):
- pass
diff --git a/erpnext/agriculture/doctype/water_analysis/water_analysis.js b/erpnext/agriculture/doctype/water_analysis/water_analysis.js
deleted file mode 100644
index 13fe3ad..0000000
--- a/erpnext/agriculture/doctype/water_analysis/water_analysis.js
+++ /dev/null
@@ -1,18 +0,0 @@
-// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('Water Analysis', {
- onload: (frm) => {
- if (frm.doc.water_analysis_criteria == undefined) frm.call('load_contents');
- },
- refresh: (frm) => {
- let map_tools = ["a.leaflet-draw-draw-polyline",
- "a.leaflet-draw-draw-polygon",
- "a.leaflet-draw-draw-rectangle",
- "a.leaflet-draw-draw-circle",
- "a.leaflet-draw-draw-circlemarker"];
-
- map_tools.forEach((element) => $(element).hide());
- },
- laboratory_testing_datetime: (frm) => frm.call("update_lab_result_date")
-});
diff --git a/erpnext/agriculture/doctype/water_analysis/water_analysis.json b/erpnext/agriculture/doctype/water_analysis/water_analysis.json
deleted file mode 100644
index f990fef..0000000
--- a/erpnext/agriculture/doctype/water_analysis/water_analysis.json
+++ /dev/null
@@ -1,594 +0,0 @@
-{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "autoname": "HR-WAT-.YYYY.-.#####",
- "beta": 0,
- "creation": "2017-10-17 18:51:19.946950",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "location",
- "fieldtype": "Geolocation",
- "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": "Location",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_2",
- "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": "collection_datetime",
- "fieldtype": "Datetime",
- "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": "Collection Datetime",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "laboratory_testing_datetime",
- "fieldtype": "Datetime",
- "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": "Laboratory Testing Datetime",
- "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": "result_datetime",
- "fieldtype": "Datetime",
- "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": "Result Datetime",
- "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": "section_break_4",
- "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,
- "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": "type_of_sample",
- "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": "Type of Sample",
- "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": "container",
- "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": "Container",
- "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": "origin",
- "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": "Origin",
- "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": "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
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "collection_temperature",
- "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": "Collection Temperature ",
- "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": "storage_temperature",
- "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": "Storage Temperature",
- "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": "appearance",
- "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": "Appearance",
- "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": "person_responsible",
- "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": "Person Responsible",
- "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": "column_break_29",
- "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": "Water Analysis Criteria",
- "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": "water_analysis_criteria",
- "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,
- "length": 0,
- "no_copy": 0,
- "options": "Water Analysis Criteria",
- "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
- }
- ],
- "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-11-04 03:29:08.325644",
- "modified_by": "Administrator",
- "module": "Agriculture",
- "name": "Water Analysis",
- "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": "Agriculture Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 1
- },
- {
- "amend": 0,
- "cancel": 0,
- "create": 0,
- "delete": 0,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Agriculture User",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 1
- }
- ],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "restrict_to_domain": "Agriculture",
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
-}
\ No newline at end of file
diff --git a/erpnext/agriculture/doctype/water_analysis/water_analysis.py b/erpnext/agriculture/doctype/water_analysis/water_analysis.py
deleted file mode 100644
index 434acec..0000000
--- a/erpnext/agriculture/doctype/water_analysis/water_analysis.py
+++ /dev/null
@@ -1,26 +0,0 @@
-# Copyright (c) 2017, 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 WaterAnalysis(Document):
- @frappe.whitelist()
- def load_contents(self):
- docs = frappe.get_all("Agriculture Analysis Criteria", filters={'linked_doctype':'Water Analysis'})
- for doc in docs:
- self.append('water_analysis_criteria', {'title': str(doc.name)})
-
- @frappe.whitelist()
- def update_lab_result_date(self):
- if not self.result_datetime:
- self.result_datetime = self.laboratory_testing_datetime
-
- def validate(self):
- 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'))
diff --git a/erpnext/agriculture/doctype/water_analysis_criteria/__init__.py b/erpnext/agriculture/doctype/water_analysis_criteria/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/erpnext/agriculture/doctype/water_analysis_criteria/__init__.py
+++ /dev/null
diff --git a/erpnext/agriculture/doctype/water_analysis_criteria/water_analysis_criteria.json b/erpnext/agriculture/doctype/water_analysis_criteria/water_analysis_criteria.json
deleted file mode 100644
index be9f1be..0000000
--- a/erpnext/agriculture/doctype/water_analysis_criteria/water_analysis_criteria.json
+++ /dev/null
@@ -1,173 +0,0 @@
-{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2017-12-05 23:36:22.723558",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "title",
- "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": "Title",
- "length": 0,
- "no_copy": 0,
- "options": "Agriculture Analysis Criteria",
- "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
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "value",
- "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": "Value",
- "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": "minimum_permissible_value",
- "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": "Minimum Permissible Value",
- "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": "maximum_permissible_value",
- "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": "Maximum Permissible Value",
- "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
- }
- ],
- "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-11-04 03:26:07.026834",
- "modified_by": "Administrator",
- "module": "Agriculture",
- "name": "Water Analysis Criteria",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "restrict_to_domain": "Agriculture",
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
-}
\ No newline at end of file
diff --git a/erpnext/agriculture/doctype/water_analysis_criteria/water_analysis_criteria.py b/erpnext/agriculture/doctype/water_analysis_criteria/water_analysis_criteria.py
deleted file mode 100644
index 225c4f6..0000000
--- a/erpnext/agriculture/doctype/water_analysis_criteria/water_analysis_criteria.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-from frappe.model.document import Document
-
-
-class WaterAnalysisCriteria(Document):
- pass
diff --git a/erpnext/agriculture/doctype/weather/__init__.py b/erpnext/agriculture/doctype/weather/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/erpnext/agriculture/doctype/weather/__init__.py
+++ /dev/null
diff --git a/erpnext/agriculture/doctype/weather/test_weather.py b/erpnext/agriculture/doctype/weather/test_weather.py
deleted file mode 100644
index 345baa9..0000000
--- a/erpnext/agriculture/doctype/weather/test_weather.py
+++ /dev/null
@@ -1,8 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
-
-import unittest
-
-
-class TestWeather(unittest.TestCase):
- pass
diff --git a/erpnext/agriculture/doctype/weather/weather.js b/erpnext/agriculture/doctype/weather/weather.js
deleted file mode 100644
index dadb1d8..0000000
--- a/erpnext/agriculture/doctype/weather/weather.js
+++ /dev/null
@@ -1,8 +0,0 @@
-// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('Weather', {
- onload: (frm) => {
- if (frm.doc.weather_parameter == undefined) frm.call('load_contents');
- }
-});
diff --git a/erpnext/agriculture/doctype/weather/weather.json b/erpnext/agriculture/doctype/weather/weather.json
deleted file mode 100644
index ebab78a..0000000
--- a/erpnext/agriculture/doctype/weather/weather.json
+++ /dev/null
@@ -1,307 +0,0 @@
-{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "autoname": "format:WEA-{date}-{location}",
- "beta": 0,
- "creation": "2017-10-17 19:01:05.095598",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "location",
- "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": "Location",
- "length": 0,
- "no_copy": 0,
- "options": "Location",
- "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": "column_break_2",
- "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
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "date",
- "fieldtype": "Date",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Date",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "source",
- "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": "Source",
- "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": "section_break_3",
- "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,
- "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": "section_break_9",
- "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": "Weather Parameter",
- "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": "weather_parameter",
- "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,
- "length": 0,
- "no_copy": 0,
- "options": "Weather Parameter",
- "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
- }
- ],
- "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-11-04 03:31:36.839743",
- "modified_by": "Administrator",
- "module": "Agriculture",
- "name": "Weather",
- "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": "Agriculture Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 1
- },
- {
- "amend": 0,
- "cancel": 0,
- "create": 0,
- "delete": 0,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Agriculture User",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 1
- }
- ],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "restrict_to_domain": "Agriculture",
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
-}
\ No newline at end of file
diff --git a/erpnext/agriculture/doctype/weather/weather.py b/erpnext/agriculture/doctype/weather/weather.py
deleted file mode 100644
index 8750709..0000000
--- a/erpnext/agriculture/doctype/weather/weather.py
+++ /dev/null
@@ -1,14 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-import frappe
-from frappe.model.document import Document
-
-
-class Weather(Document):
- @frappe.whitelist()
- def load_contents(self):
- docs = frappe.get_all("Agriculture Analysis Criteria", filters={'linked_doctype':'Weather'})
- for doc in docs:
- self.append('weather_parameter', {'title': str(doc.name)})
diff --git a/erpnext/agriculture/doctype/weather_parameter/weather_parameter.json b/erpnext/agriculture/doctype/weather_parameter/weather_parameter.json
deleted file mode 100644
index 45c4cfc..0000000
--- a/erpnext/agriculture/doctype/weather_parameter/weather_parameter.json
+++ /dev/null
@@ -1,173 +0,0 @@
-{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2017-12-06 00:19:15.967334",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "title",
- "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": "Title",
- "length": 0,
- "no_copy": 0,
- "options": "Agriculture Analysis Criteria",
- "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
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "value",
- "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": "Value",
- "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": "minimum_permissible_value",
- "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": "Minimum Permissible Value",
- "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": "maximum_permissible_value",
- "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": "Maximum Permissible Value",
- "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
- }
- ],
- "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-11-04 03:26:58.794373",
- "modified_by": "Administrator",
- "module": "Agriculture",
- "name": "Weather Parameter",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "restrict_to_domain": "Agriculture",
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
-}
\ No newline at end of file
diff --git a/erpnext/agriculture/doctype/weather_parameter/weather_parameter.py b/erpnext/agriculture/doctype/weather_parameter/weather_parameter.py
deleted file mode 100644
index 7f02ab3..0000000
--- a/erpnext/agriculture/doctype/weather_parameter/weather_parameter.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-from frappe.model.document import Document
-
-
-class WeatherParameter(Document):
- pass
diff --git a/erpnext/agriculture/setup.py b/erpnext/agriculture/setup.py
deleted file mode 100644
index 70931b9..0000000
--- a/erpnext/agriculture/setup.py
+++ /dev/null
@@ -1,429 +0,0 @@
-import frappe
-from frappe import _
-from erpnext.setup.utils import insert_record
-
-def setup_agriculture():
- if frappe.get_all('Agriculture Analysis Criteria'):
- # already setup
- return
- create_agriculture_data()
-
-def create_agriculture_data():
- records = [
- dict(
- doctype='Item Group',
- item_group_name='Fertilizer',
- is_group=0,
- parent_item_group=_('All Item Groups')),
- dict(
- doctype='Item Group',
- item_group_name='Seed',
- is_group=0,
- parent_item_group=_('All Item Groups')),
- dict(
- doctype='Item Group',
- item_group_name='By-product',
- is_group=0,
- parent_item_group=_('All Item Groups')),
- dict(
- doctype='Item Group',
- item_group_name='Produce',
- is_group=0,
- parent_item_group=_('All Item Groups')),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Nitrogen Content',
- standard=1,
- linked_doctype='Fertilizer'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Phosphorous Content',
- standard=1,
- linked_doctype='Fertilizer'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Potassium Content',
- standard=1,
- linked_doctype='Fertilizer'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Calcium Content',
- standard=1,
- linked_doctype='Fertilizer'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Sulphur Content',
- standard=1,
- linked_doctype='Fertilizer'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Magnesium Content',
- standard=1,
- linked_doctype='Fertilizer'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Iron Content',
- standard=1,
- linked_doctype='Fertilizer'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Copper Content',
- standard=1,
- linked_doctype='Fertilizer'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Zinc Content',
- standard=1,
- linked_doctype='Fertilizer'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Boron Content',
- standard=1,
- linked_doctype='Fertilizer'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Manganese Content',
- standard=1,
- linked_doctype='Fertilizer'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Chlorine Content',
- standard=1,
- linked_doctype='Fertilizer'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Molybdenum Content',
- standard=1,
- linked_doctype='Fertilizer'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Sodium Content',
- standard=1,
- linked_doctype='Fertilizer'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Humic Acid',
- standard=1,
- linked_doctype='Fertilizer'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Fulvic Acid',
- standard=1,
- linked_doctype='Fertilizer'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Inert',
- standard=1,
- linked_doctype='Fertilizer'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Others',
- standard=1,
- linked_doctype='Fertilizer'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Nitrogen',
- standard=1,
- linked_doctype='Plant Analysis'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Phosphorous',
- standard=1,
- linked_doctype='Plant Analysis'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Potassium',
- standard=1,
- linked_doctype='Plant Analysis'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Calcium',
- standard=1,
- linked_doctype='Plant Analysis'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Magnesium',
- standard=1,
- linked_doctype='Plant Analysis'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Sulphur',
- standard=1,
- linked_doctype='Plant Analysis'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Boron',
- standard=1,
- linked_doctype='Plant Analysis'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Copper',
- standard=1,
- linked_doctype='Plant Analysis'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Iron',
- standard=1,
- linked_doctype='Plant Analysis'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Manganese',
- standard=1,
- linked_doctype='Plant Analysis'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Zinc',
- standard=1,
- linked_doctype='Plant Analysis'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Depth (in cm)',
- standard=1,
- linked_doctype='Soil Analysis'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Soil pH',
- standard=1,
- linked_doctype='Soil Analysis'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Salt Concentration (%)',
- standard=1,
- linked_doctype='Soil Analysis'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Organic Matter (%)',
- standard=1,
- linked_doctype='Soil Analysis'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='CEC (Cation Exchange Capacity) (MAQ/100mL)',
- standard=1,
- linked_doctype='Soil Analysis'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Potassium Saturation (%)',
- standard=1,
- linked_doctype='Soil Analysis'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Calcium Saturation (%)',
- standard=1,
- linked_doctype='Soil Analysis'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Manganese Saturation (%)',
- standard=1,
- linked_doctype='Soil Analysis'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Nirtogen (ppm)',
- standard=1,
- linked_doctype='Soil Analysis'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Phosphorous (ppm)',
- standard=1,
- linked_doctype='Soil Analysis'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Potassium (ppm)',
- standard=1,
- linked_doctype='Soil Analysis'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Calcium (ppm)',
- standard=1,
- linked_doctype='Soil Analysis'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Magnesium (ppm)',
- standard=1,
- linked_doctype='Soil Analysis'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Sulphur (ppm)',
- standard=1,
- linked_doctype='Soil Analysis'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Copper (ppm)',
- standard=1,
- linked_doctype='Soil Analysis'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Iron (ppm)',
- standard=1,
- linked_doctype='Soil Analysis'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Manganese (ppm)',
- standard=1,
- linked_doctype='Soil Analysis'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Zinc (ppm)',
- standard=1,
- linked_doctype='Soil Analysis'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Aluminium (ppm)',
- standard=1,
- linked_doctype='Soil Analysis'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Water pH',
- standard=1,
- linked_doctype='Water Analysis'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Conductivity (mS/cm)',
- standard=1,
- linked_doctype='Water Analysis'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Hardness (mg/CaCO3)',
- standard=1,
- linked_doctype='Water Analysis'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Turbidity (NTU)',
- standard=1,
- linked_doctype='Water Analysis'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Odor',
- standard=1,
- linked_doctype='Water Analysis'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Color',
- standard=1,
- linked_doctype='Water Analysis'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Nitrate (mg/L)',
- standard=1,
- linked_doctype='Water Analysis'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Nirtite (mg/L)',
- standard=1,
- linked_doctype='Water Analysis'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Calcium (mg/L)',
- standard=1,
- linked_doctype='Water Analysis'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Magnesium (mg/L)',
- standard=1,
- linked_doctype='Water Analysis'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Sulphate (mg/L)',
- standard=1,
- linked_doctype='Water Analysis'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Boron (mg/L)',
- standard=1,
- linked_doctype='Water Analysis'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Copper (mg/L)',
- standard=1,
- linked_doctype='Water Analysis'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Iron (mg/L)',
- standard=1,
- linked_doctype='Water Analysis'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Manganese (mg/L)',
- standard=1,
- linked_doctype='Water Analysis'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Zinc (mg/L)',
- standard=1,
- linked_doctype='Water Analysis'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Chlorine (mg/L)',
- standard=1,
- linked_doctype='Water Analysis'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Bulk Density',
- standard=1,
- linked_doctype='Soil Texture'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Field Capacity',
- standard=1,
- linked_doctype='Soil Texture'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Wilting Point',
- standard=1,
- linked_doctype='Soil Texture'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Hydraulic Conductivity',
- standard=1,
- linked_doctype='Soil Texture'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Organic Matter',
- standard=1,
- linked_doctype='Soil Texture'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Temperature High',
- standard=1,
- linked_doctype='Weather'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Temperature Low',
- standard=1,
- linked_doctype='Weather'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Temperature Average',
- standard=1,
- linked_doctype='Weather'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Dew Point',
- standard=1,
- linked_doctype='Weather'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Precipitation Received',
- standard=1,
- linked_doctype='Weather'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Humidity',
- standard=1,
- linked_doctype='Weather'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Pressure',
- standard=1,
- linked_doctype='Weather'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Insolation/ PAR (Photosynthetically Active Radiation)',
- standard=1,
- linked_doctype='Weather'),
- dict(
- doctype='Agriculture Analysis Criteria',
- title='Degree Days',
- standard=1,
- linked_doctype='Weather')
- ]
- insert_record(records)
diff --git a/erpnext/agriculture/workspace/agriculture/agriculture.json b/erpnext/agriculture/workspace/agriculture/agriculture.json
deleted file mode 100644
index 6714de6..0000000
--- a/erpnext/agriculture/workspace/agriculture/agriculture.json
+++ /dev/null
@@ -1,171 +0,0 @@
-{
- "charts": [],
- "content": "[{\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Crops & Lands\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Analytics\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Diseases & Fertilizers\", \"col\": 4}}]",
- "creation": "2020-03-02 17:23:34.339274",
- "docstatus": 0,
- "doctype": "Workspace",
- "for_user": "",
- "hide_custom": 0,
- "icon": "agriculture",
- "idx": 0,
- "label": "Agriculture",
- "links": [
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Crops & Lands",
- "link_count": 0,
- "onboard": 0,
- "type": "Card Break"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Crop",
- "link_count": 0,
- "link_to": "Crop",
- "link_type": "DocType",
- "onboard": 1,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Crop Cycle",
- "link_count": 0,
- "link_to": "Crop Cycle",
- "link_type": "DocType",
- "onboard": 1,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Location",
- "link_count": 0,
- "link_to": "Location",
- "link_type": "DocType",
- "onboard": 1,
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Analytics",
- "link_count": 0,
- "onboard": 0,
- "type": "Card Break"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Plant Analysis",
- "link_count": 0,
- "link_to": "Plant Analysis",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Soil Analysis",
- "link_count": 0,
- "link_to": "Soil Analysis",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Water Analysis",
- "link_count": 0,
- "link_to": "Water Analysis",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Soil Texture",
- "link_count": 0,
- "link_to": "Soil Texture",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Weather",
- "link_count": 0,
- "link_to": "Weather",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Agriculture Analysis Criteria",
- "link_count": 0,
- "link_to": "Agriculture Analysis Criteria",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Diseases & Fertilizers",
- "link_count": 0,
- "onboard": 0,
- "type": "Card Break"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Disease",
- "link_count": 0,
- "link_to": "Disease",
- "link_type": "DocType",
- "onboard": 1,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Fertilizer",
- "link_count": 0,
- "link_to": "Fertilizer",
- "link_type": "DocType",
- "onboard": 1,
- "type": "Link"
- }
- ],
- "modified": "2021-08-05 12:15:54.595198",
- "modified_by": "Administrator",
- "module": "Agriculture",
- "name": "Agriculture",
- "owner": "Administrator",
- "parent_page": "",
- "public": 1,
- "restrict_to_domain": "Agriculture",
- "roles": [],
- "sequence_id": 3,
- "shortcuts": [],
- "title": "Agriculture"
-}
\ No newline at end of file
diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js
index da5778e..153f5c5 100644
--- a/erpnext/assets/doctype/asset/asset.js
+++ b/erpnext/assets/doctype/asset/asset.js
@@ -80,20 +80,20 @@
if (frm.doc.docstatus==1) {
if (in_list(["Submitted", "Partially Depreciated", "Fully Depreciated"], frm.doc.status)) {
- frm.add_custom_button("Transfer Asset", function() {
+ frm.add_custom_button(__("Transfer Asset"), function() {
erpnext.asset.transfer_asset(frm);
}, __("Manage"));
- frm.add_custom_button("Scrap Asset", function() {
+ frm.add_custom_button(__("Scrap Asset"), function() {
erpnext.asset.scrap_asset(frm);
}, __("Manage"));
- frm.add_custom_button("Sell Asset", function() {
+ 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() {
+ frm.add_custom_button(__("Restore Asset"), function() {
erpnext.asset.restore_asset(frm);
}, __("Manage"));
}
@@ -110,7 +110,7 @@
if (frm.doc.status != 'Fully Depreciated') {
frm.add_custom_button(__("Adjust Asset Value"), function() {
- frm.trigger("create_asset_adjustment");
+ frm.trigger("create_asset_value_adjustment");
}, __("Manage"));
}
@@ -121,7 +121,7 @@
}
if (frm.doc.purchase_receipt || !frm.doc.is_existing_asset) {
- frm.add_custom_button("View 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,
@@ -322,14 +322,14 @@
});
},
- create_asset_adjustment: function(frm) {
+ create_asset_value_adjustment: function(frm) {
frappe.call({
args: {
"asset": frm.doc.name,
"asset_category": frm.doc.asset_category,
"company": frm.doc.company
},
- method: "erpnext.assets.doctype.asset.asset.create_asset_adjustment",
+ method: "erpnext.assets.doctype.asset.asset.create_asset_value_adjustment",
freeze: 1,
callback: function(r) {
var doclist = frappe.model.sync(r.message);
diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py
index c0c437f..a18b03a 100644
--- a/erpnext/assets/doctype/asset/asset.py
+++ b/erpnext/assets/doctype/asset/asset.py
@@ -185,84 +185,84 @@
if not self.available_for_use_date:
return
- for d in self.get('finance_books'):
- self.validate_asset_finance_books(d)
+ start = self.clear_depreciation_schedule()
- start = self.clear_depreciation_schedule()
+ for finance_book in self.get('finance_books'):
+ self.validate_asset_finance_books(finance_book)
# value_after_depreciation - current Asset value
- if self.docstatus == 1 and d.value_after_depreciation:
- value_after_depreciation = (flt(d.value_after_depreciation) -
- flt(self.opening_accumulated_depreciation))
+ if self.docstatus == 1 and finance_book.value_after_depreciation:
+ value_after_depreciation = flt(finance_book.value_after_depreciation)
else:
value_after_depreciation = (flt(self.gross_purchase_amount) -
flt(self.opening_accumulated_depreciation))
- d.value_after_depreciation = value_after_depreciation
+ finance_book.value_after_depreciation = value_after_depreciation
- number_of_pending_depreciations = cint(d.total_number_of_depreciations) - \
+ number_of_pending_depreciations = cint(finance_book.total_number_of_depreciations) - \
cint(self.number_of_depreciations_booked)
- has_pro_rata = self.check_is_pro_rata(d)
+ has_pro_rata = self.check_is_pro_rata(finance_book)
if has_pro_rata:
number_of_pending_depreciations += 1
skip_row = False
- for n in range(start, number_of_pending_depreciations):
+
+ for n in range(start[finance_book.idx-1], number_of_pending_depreciations):
# If depreciation is already completed (for double declining balance)
if skip_row: continue
- depreciation_amount = get_depreciation_amount(self, value_after_depreciation, d)
+ depreciation_amount = get_depreciation_amount(self, value_after_depreciation, finance_book)
if not has_pro_rata or n < cint(number_of_pending_depreciations) - 1:
- schedule_date = add_months(d.depreciation_start_date,
- n * cint(d.frequency_of_depreciation))
+ schedule_date = add_months(finance_book.depreciation_start_date,
+ n * cint(finance_book.frequency_of_depreciation))
# schedule date will be a year later from start date
# so monthly schedule date is calculated by removing 11 months from it
- monthly_schedule_date = add_months(schedule_date, - d.frequency_of_depreciation + 1)
+ monthly_schedule_date = add_months(schedule_date, - finance_book.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 = self.get_from_date(finance_book.finance_book)
+ depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, depreciation_amount,
from_date, date_of_sale)
if depreciation_amount > 0:
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
+ "depreciation_method": finance_book.depreciation_method,
+ "finance_book": finance_book.finance_book,
+ "finance_book_id": finance_book.idx
})
break
# For first row
- if has_pro_rata and n==0:
- depreciation_amount, days, months = self.get_pro_rata_amt(d, depreciation_amount,
- self.available_for_use_date, d.depreciation_start_date)
+ if has_pro_rata and not self.opening_accumulated_depreciation and n==0:
+ depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, depreciation_amount,
+ self.available_for_use_date, finance_book.depreciation_start_date)
# For first depr schedule date will be the start date
# so monthly schedule date is calculated by removing month difference between use date and start date
- monthly_schedule_date = add_months(d.depreciation_start_date, - months + 1)
+ monthly_schedule_date = add_months(finance_book.depreciation_start_date, - months + 1)
# For last row
elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1:
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))
+ (n + self.number_of_depreciations_booked) * cint(finance_book.frequency_of_depreciation))
depreciation_amount_without_pro_rata = depreciation_amount
- depreciation_amount, days, months = self.get_pro_rata_amt(d,
+ depreciation_amount, days, months = self.get_pro_rata_amt(finance_book,
depreciation_amount, schedule_date, self.to_date)
depreciation_amount = self.get_adjusted_depreciation_amount(depreciation_amount_without_pro_rata,
- depreciation_amount, d.finance_book)
+ depreciation_amount, finance_book.finance_book)
monthly_schedule_date = add_months(schedule_date, 1)
schedule_date = add_days(schedule_date, days)
@@ -273,10 +273,10 @@
self.precision("gross_purchase_amount"))
# Adjust depreciation amount in the last period based on the expected value after useful life
- if d.expected_value_after_useful_life and ((n == cint(number_of_pending_depreciations) - 1
- and value_after_depreciation != d.expected_value_after_useful_life)
- or value_after_depreciation < d.expected_value_after_useful_life):
- depreciation_amount += (value_after_depreciation - d.expected_value_after_useful_life)
+ if finance_book.expected_value_after_useful_life and ((n == cint(number_of_pending_depreciations) - 1
+ and value_after_depreciation != finance_book.expected_value_after_useful_life)
+ or value_after_depreciation < finance_book.expected_value_after_useful_life):
+ depreciation_amount += (value_after_depreciation - finance_book.expected_value_after_useful_life)
skip_row = True
if depreciation_amount > 0:
@@ -286,7 +286,7 @@
# In pro rata case, for first and last depreciation, month range would be different
month_range = months \
if (has_pro_rata and n==0) or (has_pro_rata and n == cint(number_of_pending_depreciations) - 1) \
- else d.frequency_of_depreciation
+ else finance_book.frequency_of_depreciation
for r in range(month_range):
if (has_pro_rata and n == 0):
@@ -312,27 +312,52 @@
self.append("schedules", {
"schedule_date": date,
"depreciation_amount": amount,
- "depreciation_method": d.depreciation_method,
- "finance_book": d.finance_book,
- "finance_book_id": d.idx
+ "depreciation_method": finance_book.depreciation_method,
+ "finance_book": finance_book.finance_book,
+ "finance_book_id": finance_book.idx
})
else:
self.append("schedules", {
"schedule_date": schedule_date,
"depreciation_amount": depreciation_amount,
- "depreciation_method": d.depreciation_method,
- "finance_book": d.finance_book,
- "finance_book_id": d.idx
+ "depreciation_method": finance_book.depreciation_method,
+ "finance_book": finance_book.finance_book,
+ "finance_book_id": finance_book.idx
})
- # used when depreciation schedule needs to be modified due to increase in asset life
+ # depreciation schedules need to be cleared before modification due to increase in asset life/asset sales
+ # JE: Journal Entry, FB: Finance Book
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
+ start = []
+ num_of_depreciations_completed = 0
+ depr_schedule = []
+
+ for schedule in self.get('schedules'):
+
+ # to update start when there are JEs linked with all the schedule rows corresponding to an FB
+ if len(start) == (int(schedule.finance_book_id) - 2):
+ start.append(num_of_depreciations_completed)
+ num_of_depreciations_completed = 0
+
+ # to ensure that start will only be updated once for each FB
+ if len(start) == (int(schedule.finance_book_id) - 1):
+ if schedule.journal_entry:
+ num_of_depreciations_completed += 1
+ depr_schedule.append(schedule)
+ else:
+ start.append(num_of_depreciations_completed)
+ num_of_depreciations_completed = 0
+
+ # to update start when all the schedule rows corresponding to the last FB are linked with JEs
+ if len(start) == (len(self.finance_books) - 1):
+ start.append(num_of_depreciations_completed)
+
+ # when the Depreciation Schedule is being created for the first time
+ if start == []:
+ start = [0] * len(self.finance_books)
+ else:
+ self.schedules = depr_schedule
+
return start
def get_from_date(self, finance_book):
@@ -354,7 +379,12 @@
# 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 not existing asset, from_date = available_for_use_date
+ # otherwise, if number_of_depreciations_booked = 2, available_for_use_date = 01/01/2020 and frequency_of_depreciation = 12
+ # from_date = 01/01/2022
+ from_date = self.get_modified_available_for_use_date(row)
+ days = date_diff(row.depreciation_start_date, from_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)
@@ -364,6 +394,9 @@
return has_pro_rata
+ def get_modified_available_for_use_date(self, row):
+ return add_months(self.available_for_use_date, (self.number_of_depreciations_booked * row.frequency_of_depreciation))
+
def validate_asset_finance_books(self, row):
if flt(row.expected_value_after_useful_life) >= flt(self.gross_purchase_amount):
frappe.throw(_("Row {0}: Expected Value After Useful Life must be less than Gross Purchase Amount")
@@ -402,10 +435,11 @@
# to ensure that final accumulated depreciation amount is accurate
def get_adjusted_depreciation_amount(self, depreciation_amount_without_pro_rata, depreciation_amount_for_last_row, finance_book):
- depreciation_amount_for_first_row = self.get_depreciation_amount_for_first_row(finance_book)
+ if not self.opening_accumulated_depreciation:
+ depreciation_amount_for_first_row = self.get_depreciation_amount_for_first_row(finance_book)
- if depreciation_amount_for_first_row + depreciation_amount_for_last_row != depreciation_amount_without_pro_rata:
- depreciation_amount_for_last_row = depreciation_amount_without_pro_rata - depreciation_amount_for_first_row
+ if depreciation_amount_for_first_row + depreciation_amount_for_last_row != depreciation_amount_without_pro_rata:
+ depreciation_amount_for_last_row = depreciation_amount_without_pro_rata - depreciation_amount_for_first_row
return depreciation_amount_for_last_row
@@ -461,7 +495,6 @@
asset_value_after_full_schedule = flt(
flt(self.gross_purchase_amount) -
- flt(self.opening_accumulated_depreciation) -
flt(accumulated_depreciation_after_full_schedule), self.precision('gross_purchase_amount'))
if (row.expected_value_after_useful_life and
@@ -723,14 +756,14 @@
return asset_repair
@frappe.whitelist()
-def create_asset_adjustment(asset, asset_category, company):
- asset_maintenance = frappe.get_doc("Asset Value Adjustment")
- asset_maintenance.update({
+def create_asset_value_adjustment(asset, asset_category, company):
+ asset_value_adjustment = frappe.new_doc("Asset Value Adjustment")
+ asset_value_adjustment.update({
"asset": asset,
"company": company,
"asset_category": asset_category
})
- return asset_maintenance
+ return asset_value_adjustment
@frappe.whitelist()
def transfer_asset(args):
@@ -850,13 +883,11 @@
@erpnext.allow_regional
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"):
# if the Depreciation Schedule is being prepared for the first time
if not asset.flags.increase_in_asset_life:
- depreciation_amount = (flt(asset.gross_purchase_amount) - flt(asset.opening_accumulated_depreciation) -
- flt(row.expected_value_after_useful_life)) / depreciation_left
+ depreciation_amount = (flt(asset.gross_purchase_amount) -
+ flt(row.expected_value_after_useful_life)) / flt(row.total_number_of_depreciations)
# if the Depreciation Schedule is being modified after Asset Repair
else:
diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py
index ca10b1d..874fb63 100644
--- a/erpnext/assets/doctype/asset/depreciation.py
+++ b/erpnext/assets/doctype/asset/depreciation.py
@@ -57,8 +57,10 @@
je.finance_book = d.finance_book
je.remark = "Depreciation Entry against {0} worth {1}".format(asset_name, d.depreciation_amount)
+ credit_account, debit_account = get_credit_and_debit_accounts(accumulated_depreciation_account, depreciation_expense_account)
+
credit_entry = {
- "account": accumulated_depreciation_account,
+ "account": credit_account,
"credit_in_account_currency": d.depreciation_amount,
"reference_type": "Asset",
"reference_name": asset.name,
@@ -66,7 +68,7 @@
}
debit_entry = {
- "account": depreciation_expense_account,
+ "account": debit_account,
"debit_in_account_currency": d.depreciation_amount,
"reference_type": "Asset",
"reference_name": asset.name,
@@ -132,6 +134,20 @@
return fixed_asset_account, accumulated_depreciation_account, depreciation_expense_account
+def get_credit_and_debit_accounts(accumulated_depreciation_account, depreciation_expense_account):
+ root_type = frappe.get_value("Account", depreciation_expense_account, "root_type")
+
+ if root_type == "Expense":
+ credit_account = accumulated_depreciation_account
+ debit_account = depreciation_expense_account
+ elif root_type == "Income":
+ credit_account = depreciation_expense_account
+ debit_account = accumulated_depreciation_account
+ else:
+ frappe.throw(_("Depreciation Expense Account should be an Income or Expense Account."))
+
+ return credit_account, debit_account
+
@frappe.whitelist()
def scrap_asset(asset_name):
asset = frappe.get_doc("Asset", asset_name)
diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py
index d1d4527..44c4ce5 100644
--- a/erpnext/assets/doctype/asset/test_asset.py
+++ b/erpnext/assets/doctype/asset/test_asset.py
@@ -409,19 +409,18 @@
calculate_depreciation = 1,
available_for_use_date = "2030-06-06",
is_existing_asset = 1,
- number_of_depreciations_booked = 1,
- opening_accumulated_depreciation = 40000,
+ number_of_depreciations_booked = 2,
+ opening_accumulated_depreciation = 47095.89,
expected_value_after_useful_life = 10000,
- depreciation_start_date = "2030-12-31",
+ depreciation_start_date = "2032-12-31",
total_number_of_depreciations = 3,
frequency_of_depreciation = 12
)
self.assertEqual(asset.status, "Draft")
expected_schedules = [
- ["2030-12-31", 14246.58, 54246.58],
- ["2031-12-31", 25000.00, 79246.58],
- ["2032-06-06", 10753.42, 90000.00]
+ ["2032-12-31", 30000.0, 77095.89],
+ ["2033-06-06", 12904.11, 90000.0]
]
schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), d.accumulated_depreciation_amount]
for d in asset.get("schedules")]
@@ -869,6 +868,72 @@
self.assertFalse(asset.schedules[1].journal_entry)
self.assertFalse(asset.schedules[2].journal_entry)
+ def test_depr_entry_posting_when_depr_expense_account_is_an_expense_account(self):
+ """Tests if the Depreciation Expense Account gets debited and the Accumulated Depreciation Account gets credited when the former's an Expense Account."""
+
+ asset = create_asset(
+ item_code = "Macbook Pro",
+ calculate_depreciation = 1,
+ available_for_use_date = "2019-12-31",
+ depreciation_start_date = "2020-12-31",
+ frequency_of_depreciation = 12,
+ total_number_of_depreciations = 3,
+ expected_value_after_useful_life = 10000,
+ submit = 1
+ )
+
+ post_depreciation_entries(date="2021-06-01")
+ asset.load_from_db()
+
+ je = frappe.get_doc("Journal Entry", asset.schedules[0].journal_entry)
+ accounting_entries = [{"account": entry.account, "debit": entry.debit, "credit": entry.credit} for entry in je.accounts]
+
+ for entry in accounting_entries:
+ if entry["account"] == "_Test Depreciations - _TC":
+ self.assertTrue(entry["debit"])
+ self.assertFalse(entry["credit"])
+ else:
+ self.assertTrue(entry["credit"])
+ self.assertFalse(entry["debit"])
+
+ def test_depr_entry_posting_when_depr_expense_account_is_an_income_account(self):
+ """Tests if the Depreciation Expense Account gets credited and the Accumulated Depreciation Account gets debited when the former's an Income Account."""
+
+ depr_expense_account = frappe.get_doc("Account", "_Test Depreciations - _TC")
+ depr_expense_account.root_type = "Income"
+ depr_expense_account.parent_account = "Income - _TC"
+ depr_expense_account.save()
+
+ asset = create_asset(
+ item_code = "Macbook Pro",
+ calculate_depreciation = 1,
+ available_for_use_date = "2019-12-31",
+ depreciation_start_date = "2020-12-31",
+ frequency_of_depreciation = 12,
+ total_number_of_depreciations = 3,
+ expected_value_after_useful_life = 10000,
+ submit = 1
+ )
+
+ post_depreciation_entries(date="2021-06-01")
+ asset.load_from_db()
+
+ je = frappe.get_doc("Journal Entry", asset.schedules[0].journal_entry)
+ accounting_entries = [{"account": entry.account, "debit": entry.debit, "credit": entry.credit} for entry in je.accounts]
+
+ for entry in accounting_entries:
+ if entry["account"] == "_Test Depreciations - _TC":
+ self.assertTrue(entry["credit"])
+ self.assertFalse(entry["debit"])
+ else:
+ self.assertTrue(entry["debit"])
+ self.assertFalse(entry["credit"])
+
+ # resetting
+ depr_expense_account.root_type = "Expense"
+ depr_expense_account.parent_account = "Expenses - _TC"
+ depr_expense_account.save()
+
def test_clear_depreciation_schedule(self):
"""Tests if clear_depreciation_schedule() works as expected."""
@@ -890,6 +955,82 @@
self.assertEqual(len(asset.schedules), 1)
+ def test_clear_depreciation_schedule_for_multiple_finance_books(self):
+ asset = create_asset(
+ item_code = "Macbook Pro",
+ available_for_use_date = "2019-12-31",
+ do_not_save = 1
+ )
+
+ asset.calculate_depreciation = 1
+ asset.append("finance_books", {
+ "depreciation_method": "Straight Line",
+ "frequency_of_depreciation": 1,
+ "total_number_of_depreciations": 3,
+ "expected_value_after_useful_life": 10000,
+ "depreciation_start_date": "2020-01-31"
+ })
+ asset.append("finance_books", {
+ "depreciation_method": "Straight Line",
+ "frequency_of_depreciation": 1,
+ "total_number_of_depreciations": 6,
+ "expected_value_after_useful_life": 10000,
+ "depreciation_start_date": "2020-01-31"
+ })
+ asset.append("finance_books", {
+ "depreciation_method": "Straight Line",
+ "frequency_of_depreciation": 12,
+ "total_number_of_depreciations": 3,
+ "expected_value_after_useful_life": 10000,
+ "depreciation_start_date": "2020-12-31"
+ })
+ asset.submit()
+
+ post_depreciation_entries(date="2020-04-01")
+ asset.load_from_db()
+
+ asset.clear_depreciation_schedule()
+
+ self.assertEqual(len(asset.schedules), 6)
+
+ for schedule in asset.schedules:
+ if schedule.idx <= 3:
+ self.assertEqual(schedule.finance_book_id, "1")
+ else:
+ self.assertEqual(schedule.finance_book_id, "2")
+
+ def test_depreciation_schedules_are_set_up_for_multiple_finance_books(self):
+ asset = create_asset(
+ item_code = "Macbook Pro",
+ available_for_use_date = "2019-12-31",
+ do_not_save = 1
+ )
+
+ asset.calculate_depreciation = 1
+ asset.append("finance_books", {
+ "depreciation_method": "Straight Line",
+ "frequency_of_depreciation": 12,
+ "total_number_of_depreciations": 3,
+ "expected_value_after_useful_life": 10000,
+ "depreciation_start_date": "2020-12-31"
+ })
+ asset.append("finance_books", {
+ "depreciation_method": "Straight Line",
+ "frequency_of_depreciation": 12,
+ "total_number_of_depreciations": 6,
+ "expected_value_after_useful_life": 10000,
+ "depreciation_start_date": "2020-12-31"
+ })
+ asset.save()
+
+ self.assertEqual(len(asset.schedules), 9)
+
+ for schedule in asset.schedules:
+ if schedule.idx <= 3:
+ self.assertEqual(schedule.finance_book_id, 1)
+ else:
+ self.assertEqual(schedule.finance_book_id, 2)
+
def test_depreciation_entry_cancellation(self):
asset = create_asset(
item_code = "Macbook Pro",
diff --git a/erpnext/assets/doctype/asset_category/asset_category.js b/erpnext/assets/doctype/asset_category/asset_category.js
index 51ce157..c702687 100644
--- a/erpnext/assets/doctype/asset_category/asset_category.js
+++ b/erpnext/assets/doctype/asset_category/asset_category.js
@@ -33,7 +33,7 @@
var d = locals[cdt][cdn];
return {
"filters": {
- "root_type": "Expense",
+ "root_type": ["in", ["Expense", "Income"]],
"is_group": 0,
"company": d.company_name
}
diff --git a/erpnext/assets/doctype/asset_category/asset_category.py b/erpnext/assets/doctype/asset_category/asset_category.py
index e2f3ca3..bd573bf 100644
--- a/erpnext/assets/doctype/asset_category/asset_category.py
+++ b/erpnext/assets/doctype/asset_category/asset_category.py
@@ -42,10 +42,10 @@
def validate_account_types(self):
account_type_map = {
- 'fixed_asset_account': { 'account_type': 'Fixed Asset' },
- 'accumulated_depreciation_account': { 'account_type': 'Accumulated Depreciation' },
- 'depreciation_expense_account': { 'root_type': 'Expense' },
- 'capital_work_in_progress_account': { 'account_type': 'Capital Work in Progress' }
+ 'fixed_asset_account': {'account_type': ['Fixed Asset']},
+ 'accumulated_depreciation_account': {'account_type': ['Accumulated Depreciation']},
+ 'depreciation_expense_account': {'root_type': ['Expense', 'Income']},
+ 'capital_work_in_progress_account': {'account_type': ['Capital Work in Progress']}
}
for d in self.accounts:
for fieldname in account_type_map.keys():
@@ -53,11 +53,11 @@
selected_account = d.get(fieldname)
key_to_match = next(iter(account_type_map.get(fieldname))) # acount_type or root_type
selected_key_type = frappe.db.get_value('Account', selected_account, key_to_match)
- expected_key_type = account_type_map[fieldname][key_to_match]
+ expected_key_types = account_type_map[fieldname][key_to_match]
- if selected_key_type != expected_key_type:
+ if selected_key_type not in expected_key_types:
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)),
+ .format(d.idx, frappe.unscrub(key_to_match), frappe.bold(selected_account), frappe.bold(expected_key_types)),
title=_("Invalid Account"))
def valide_cwip_account(self):
diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js
index 18a56d3..d554d52 100644
--- a/erpnext/assets/doctype/asset_repair/asset_repair.js
+++ b/erpnext/assets/doctype/asset_repair/asset_repair.js
@@ -60,6 +60,10 @@
if (frm.doc.repair_status == "Completed") {
frm.set_value('completion_date', frappe.datetime.now_datetime());
}
+ },
+
+ stock_items_on_form_rendered() {
+ erpnext.setup_serial_or_batch_no();
}
});
diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py
index d780c18..36848e9 100644
--- a/erpnext/assets/doctype/asset_repair/asset_repair.py
+++ b/erpnext/assets/doctype/asset_repair/asset_repair.py
@@ -118,9 +118,10 @@
for stock_item in self.get('stock_items'):
stock_entry.append('items', {
"s_warehouse": self.warehouse,
- "item_code": stock_item.item,
+ "item_code": stock_item.item_code,
"qty": stock_item.consumed_quantity,
- "basic_rate": stock_item.valuation_rate
+ "basic_rate": stock_item.valuation_rate,
+ "serial_no": stock_item.serial_no
})
stock_entry.insert()
diff --git a/erpnext/assets/doctype/asset_repair/test_asset_repair.py b/erpnext/assets/doctype/asset_repair/test_asset_repair.py
index 81b4f6c..7c0d057 100644
--- a/erpnext/assets/doctype/asset_repair/test_asset_repair.py
+++ b/erpnext/assets/doctype/asset_repair/test_asset_repair.py
@@ -11,12 +11,15 @@
create_asset_data,
set_depreciation_settings_in_company,
)
+from erpnext.stock.doctype.item.test_item import create_item
class TestAssetRepair(unittest.TestCase):
- def setUp(self):
+ @classmethod
+ def setUpClass(cls):
set_depreciation_settings_in_company()
create_asset_data()
+ create_item("_Test Stock Item")
frappe.db.sql("delete from `tabTax Rule`")
def test_update_status(self):
@@ -70,9 +73,28 @@
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].item_code, asset_repair.stock_items[0].item_code)
self.assertEqual(stock_entry.items[0].qty, asset_repair.stock_items[0].consumed_quantity)
+ def test_serialized_item_consumption(self):
+ from erpnext.stock.doctype.serial_no.serial_no import SerialNoRequiredError
+ from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
+
+ stock_entry = make_serialized_item()
+ serial_nos = stock_entry.get("items")[0].serial_no
+ serial_no = serial_nos.split("\n")[0]
+
+ # should not raise any error
+ create_asset_repair(stock_consumption = 1, item_code = stock_entry.get("items")[0].item_code,
+ warehouse = "_Test Warehouse - _TC", serial_no = serial_no, submit = 1)
+
+ # should raise error
+ asset_repair = create_asset_repair(stock_consumption = 1, warehouse = "_Test Warehouse - _TC",
+ item_code = stock_entry.get("items")[0].item_code)
+
+ asset_repair.repair_status = "Completed"
+ self.assertRaises(SerialNoRequiredError, asset_repair.submit)
+
def test_increase_in_asset_value_due_to_stock_consumption(self):
asset = create_asset(calculate_depreciation = 1, submit=1)
initial_asset_value = get_asset_value(asset)
@@ -137,11 +159,12 @@
if args.stock_consumption:
asset_repair.stock_consumption = 1
- asset_repair.warehouse = create_warehouse("Test Warehouse", company = asset.company)
+ asset_repair.warehouse = args.warehouse or create_warehouse("Test Warehouse", company = asset.company)
asset_repair.append("stock_items", {
- "item": args.item or args.item_code or "_Test Item",
+ "item_code": args.item_code or "_Test Stock Item",
"valuation_rate": args.rate if args.get("rate") is not None else 100,
- "consumed_quantity": args.qty or 1
+ "consumed_quantity": args.qty or 1,
+ "serial_no": args.serial_no
})
asset_repair.insert(ignore_if_duplicate=True)
@@ -158,7 +181,7 @@
})
stock_entry.append('items', {
"t_warehouse": asset_repair.warehouse,
- "item_code": asset_repair.stock_items[0].item,
+ "item_code": asset_repair.stock_items[0].item_code,
"qty": asset_repair.stock_items[0].consumed_quantity
})
stock_entry.submit()
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
index 528f0ec..f63add1 100644
--- 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
@@ -5,20 +5,14 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
- "item",
+ "item_code",
"valuation_rate",
"consumed_quantity",
- "total_value"
+ "total_value",
+ "serial_no"
],
"fields": [
{
- "fieldname": "item",
- "fieldtype": "Link",
- "in_list_view": 1,
- "label": "Item",
- "options": "Item"
- },
- {
"fetch_from": "item.valuation_rate",
"fieldname": "valuation_rate",
"fieldtype": "Currency",
@@ -38,12 +32,24 @@
"in_list_view": 1,
"label": "Total Value",
"read_only": 1
+ },
+ {
+ "fieldname": "serial_no",
+ "fieldtype": "Small Text",
+ "label": "Serial No"
+ },
+ {
+ "fieldname": "item_code",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Item",
+ "options": "Item"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-05-12 03:19:55.006300",
+ "modified": "2021-11-11 18:23:00.492483",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Repair Consumed Item",
diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.js b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.js
index 79c8861..36f510b 100644
--- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.js
+++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.js
@@ -14,6 +14,14 @@
}
}
});
+ frm.set_query('asset', function() {
+ return {
+ filters: {
+ calculate_depreciation: 1,
+ docstatus: 1
+ }
+ };
+ });
},
onload: function(frm) {
diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py
index b93f474..0b646ed 100644
--- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py
+++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py
@@ -10,7 +10,11 @@
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_checks_for_pl_and_bs_accounts,
)
+from erpnext.assets.doctype.asset.asset import get_depreciation_amount
from erpnext.assets.doctype.asset.depreciation import get_depreciation_accounts
+from erpnext.regional.india.utils import (
+ get_depreciation_amount as get_depreciation_amount_for_india,
+)
class AssetValueAdjustment(Document):
@@ -90,6 +94,7 @@
def reschedule_depreciations(self, asset_value):
asset = frappe.get_doc('Asset', self.asset)
+ country = frappe.get_value('Company', self.company, 'country')
for d in asset.finance_books:
d.value_after_depreciation = asset_value
@@ -111,8 +116,10 @@
depreciation_amount = days * rate_per_day
from_date = data.schedule_date
else:
- depreciation_amount = asset.get_depreciation_amount(value_after_depreciation,
- no_of_depreciations, d)
+ if country == "India":
+ depreciation_amount = get_depreciation_amount_for_india(asset, value_after_depreciation, d)
+ else:
+ depreciation_amount = get_depreciation_amount(asset, value_after_depreciation, d)
if depreciation_amount:
value_after_depreciation -= flt(depreciation_amount)
diff --git a/erpnext/assets/module_onboarding/assets/assets.json b/erpnext/assets/module_onboarding/assets/assets.json
index e6df88b..796245d 100644
--- a/erpnext/assets/module_onboarding/assets/assets.json
+++ b/erpnext/assets/module_onboarding/assets/assets.json
@@ -13,7 +13,7 @@
"documentation_url": "https://docs.erpnext.com/docs/user/manual/en/asset",
"idx": 0,
"is_complete": 0,
- "modified": "2021-08-24 17:50:41.573281",
+ "modified": "2021-12-02 11:24:37.963746",
"modified_by": "Administrator",
"module": "Assets",
"name": "Assets",
diff --git a/erpnext/assets/onboarding_step/asset_category/asset_category.json b/erpnext/assets/onboarding_step/asset_category/asset_category.json
index 033e866..58f322e 100644
--- a/erpnext/assets/onboarding_step/asset_category/asset_category.json
+++ b/erpnext/assets/onboarding_step/asset_category/asset_category.json
@@ -9,7 +9,7 @@
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
- "modified": "2021-08-24 12:49:37.665239",
+ "modified": "2021-11-23 10:02:03.242127",
"modified_by": "Administrator",
"name": "Asset Category",
"owner": "Administrator",
diff --git a/erpnext/assets/onboarding_step/asset_item/asset_item.json b/erpnext/assets/onboarding_step/asset_item/asset_item.json
index 8a174c5..13e3e2e 100644
--- a/erpnext/assets/onboarding_step/asset_item/asset_item.json
+++ b/erpnext/assets/onboarding_step/asset_item/asset_item.json
@@ -1,21 +1,22 @@
{
- "action": "Show Form Tour",
+ "action": "Create Entry",
"action_label": "Let's create a new Asset item",
"creation": "2021-08-13 14:27:07.277167",
"description": "# Asset Item\n\nAsset items are created based on Asset Category. You can create one or multiple items against once Asset Category. The sales and purchase transaction for Asset is done via Asset Item. ",
"docstatus": 0,
"doctype": "Onboarding Step",
+ "form_tour": "Item",
"idx": 0,
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
- "modified": "2021-08-16 13:59:18.362233",
+ "modified": "2021-12-02 11:23:48.158504",
"modified_by": "Administrator",
"name": "Asset Item",
"owner": "Administrator",
"reference_document": "Item",
- "show_form_tour": 0,
- "show_full_form": 0,
+ "show_form_tour": 1,
+ "show_full_form": 1,
"title": "Create an Asset Item",
"validate_action": 1
}
\ No newline at end of file
diff --git a/erpnext/assets/onboarding_step/asset_purchase/asset_purchase.json b/erpnext/assets/onboarding_step/asset_purchase/asset_purchase.json
index 54611ed..69fa337 100644
--- a/erpnext/assets/onboarding_step/asset_purchase/asset_purchase.json
+++ b/erpnext/assets/onboarding_step/asset_purchase/asset_purchase.json
@@ -9,7 +9,7 @@
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
- "modified": "2021-08-24 17:26:57.180637",
+ "modified": "2021-11-23 10:02:03.235498",
"modified_by": "Administrator",
"name": "Asset Purchase",
"owner": "Administrator",
diff --git a/erpnext/assets/onboarding_step/fixed_asset_accounts/fixed_asset_accounts.json b/erpnext/assets/onboarding_step/fixed_asset_accounts/fixed_asset_accounts.json
index cebee7a..2fc6c46 100644
--- a/erpnext/assets/onboarding_step/fixed_asset_accounts/fixed_asset_accounts.json
+++ b/erpnext/assets/onboarding_step/fixed_asset_accounts/fixed_asset_accounts.json
@@ -9,7 +9,7 @@
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
- "modified": "2021-08-24 17:46:37.646174",
+ "modified": "2021-11-23 10:02:03.229566",
"modified_by": "Administrator",
"name": "Fixed Asset Accounts",
"owner": "Administrator",
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py
index 5eab21b..1b5f35e 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.py
@@ -72,6 +72,7 @@
self.create_raw_materials_supplied("supplied_items")
self.set_received_qty_for_drop_ship_items()
validate_inter_company_party(self.doctype, self.supplier, self.company, self.inter_company_order_reference)
+ self.reset_default_field_value("set_warehouse", "items", "warehouse")
def validate_with_previous_doc(self):
super(PurchaseOrder, self).validate_with_previous_doc({
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order_dashboard.py b/erpnext/buying/doctype/purchase_order/purchase_order_dashboard.py
index 0163595..d288f88 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order_dashboard.py
+++ b/erpnext/buying/doctype/purchase_order/purchase_order_dashboard.py
@@ -7,6 +7,7 @@
'non_standard_fieldnames': {
'Journal Entry': 'reference_name',
'Payment Entry': 'reference_name',
+ 'Payment Request': 'reference_name',
'Auto Repeat': 'reference_document'
},
'internal_links': {
@@ -21,7 +22,7 @@
},
{
'label': _('Payment'),
- 'items': ['Payment Entry', 'Journal Entry']
+ 'items': ['Payment Entry', 'Journal Entry', 'Payment Request']
},
{
'label': _('Reference'),
diff --git a/erpnext/buying/doctype/purchase_order/tests/test_purchase_order.js b/erpnext/buying/doctype/purchase_order/tests/test_purchase_order.js
deleted file mode 100644
index 012b061..0000000
--- a/erpnext/buying/doctype/purchase_order/tests/test_purchase_order.js
+++ /dev/null
@@ -1,80 +0,0 @@
-QUnit.module('Buying');
-
-QUnit.test("test: purchase order", function(assert) {
- assert.expect(16);
- let done = assert.async();
-
- frappe.run_serially([
- () => {
- return frappe.tests.make('Purchase Order', [
- {supplier: 'Test Supplier'},
- {is_subcontracted: 'No'},
- {currency: 'INR'},
- {items: [
- [
- {"item_code": 'Test Product 4'},
- {"schedule_date": frappe.datetime.add_days(frappe.datetime.now_date(), 2)},
- {"expected_delivery_date": frappe.datetime.add_days(frappe.datetime.now_date(), 5)},
- {"qty": 5},
- {"uom": 'Unit'},
- {"rate": 100},
- {"warehouse": 'Stores - '+frappe.get_abbr(frappe.defaults.get_default("Company"))}
- ],
- [
- {"item_code": 'Test Product 1'},
- {"schedule_date": frappe.datetime.add_days(frappe.datetime.now_date(), 1)},
- {"expected_delivery_date": frappe.datetime.add_days(frappe.datetime.now_date(), 5)},
- {"qty": 2},
- {"uom": 'Unit'},
- {"rate": 100},
- {"warehouse": 'Stores - '+frappe.get_abbr(frappe.defaults.get_default("Company"))}
- ]
- ]},
-
- {tc_name: 'Test Term 1'},
- {terms: 'This is a term.'}
- ]);
- },
-
- () => {
- // Get supplier details
- assert.ok(cur_frm.doc.supplier_name == 'Test Supplier', "Supplier name correct");
- assert.ok(cur_frm.doc.schedule_date == frappe.datetime.add_days(frappe.datetime.now_date(), 1), "Schedule Date correct");
- assert.ok(cur_frm.doc.contact_email == 'test@supplier.com', "Contact email correct");
- // Get item details
- assert.ok(cur_frm.doc.items[0].item_name == 'Test Product 4', "Item name correct");
- assert.ok(cur_frm.doc.items[0].description == 'Test Product 4', "Description correct");
- assert.ok(cur_frm.doc.items[0].qty == 5, "Quantity correct");
- assert.ok(cur_frm.doc.items[0].schedule_date == frappe.datetime.add_days(frappe.datetime.now_date(), 2), "Schedule Date correct");
-
- assert.ok(cur_frm.doc.items[1].item_name == 'Test Product 1', "Item name correct");
- assert.ok(cur_frm.doc.items[1].description == 'Test Product 1', "Description correct");
- assert.ok(cur_frm.doc.items[1].qty == 2, "Quantity correct");
- assert.ok(cur_frm.doc.items[1].schedule_date == cur_frm.doc.schedule_date, "Schedule Date correct");
- // Calculate total
- assert.ok(cur_frm.doc.total == 700, "Total correct");
- // Get terms
- assert.ok(cur_frm.doc.terms == 'This is a term.', "Terms correct");
- },
-
- () => cur_frm.print_doc(),
- () => frappe.timeout(2),
- () => {
- assert.ok($('.btn-print-print').is(':visible'), "Print Format Available");
- assert.ok($('div > div:nth-child(5) > div > div > table > tbody > tr > td:nth-child(4) > div').text().includes('Test Product 4'), "Print Preview Works");
- },
-
- () => cur_frm.print_doc(),
- () => frappe.timeout(1),
-
- () => frappe.tests.click_button('Submit'),
- () => frappe.tests.click_button('Yes'),
- () => frappe.timeout(1),
-
- () => {
- assert.ok(cur_frm.doc.status == 'To Receive and Bill', "Submitted successfully");
- },
-
- () => done()
- ]);
-});
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
deleted file mode 100644
index bc3d767..0000000
--- a/erpnext/buying/doctype/purchase_order/tests/test_purchase_order_get_items.js
+++ /dev/null
@@ -1,61 +0,0 @@
-QUnit.module('Buying');
-
-QUnit.test("test: purchase order with get items", function(assert) {
- assert.expect(4);
- let done = assert.async();
-
- frappe.run_serially([
- () => {
- return frappe.tests.make('Purchase Order', [
- {supplier: 'Test Supplier'},
- {is_subcontracted: 'No'},
- {buying_price_list: 'Test-Buying-USD'},
- {currency: 'USD'},
- {items: [
- [
- {"item_code": 'Test Product 4'},
- {"qty": 5},
- {"schedule_date": frappe.datetime.add_days(frappe.datetime.now_date(), 1)},
- {"expected_delivery_date": frappe.datetime.add_days(frappe.datetime.now_date(), 5)},
- {"warehouse": 'Stores - '+frappe.get_abbr(frappe.defaults.get_default("Company"))}
- ]
- ]}
- ]);
- },
-
- () => {
- assert.ok(cur_frm.doc.supplier_name == 'Test Supplier', "Supplier name correct");
- },
-
- () => frappe.timeout(0.3),
- () => frappe.click_button('Get items from'),
- () => frappe.timeout(0.3),
-
- () => frappe.click_link('Product Bundle'),
- () => frappe.timeout(0.5),
-
- () => cur_dialog.set_value('product_bundle', 'Computer'),
- () => frappe.click_button('Get Items'),
- () => frappe.timeout(1),
-
- // Check if items are fetched from Product Bundle
- () => {
- assert.ok(cur_frm.doc.items[1].item_name == 'CPU', "Product bundle item 1 correct");
- assert.ok(cur_frm.doc.items[2].item_name == 'Screen', "Product bundle item 2 correct");
- assert.ok(cur_frm.doc.items[3].item_name == 'Keyboard', "Product bundle item 3 correct");
- },
-
- () => cur_frm.doc.items[1].warehouse = 'Stores - '+frappe.get_abbr(frappe.defaults.get_default("Company")),
- () => cur_frm.doc.items[2].warehouse = 'Stores - '+frappe.get_abbr(frappe.defaults.get_default("Company")),
- () => cur_frm.doc.items[3].warehouse = 'Stores - '+frappe.get_abbr(frappe.defaults.get_default("Company")),
-
- () => cur_frm.save(),
- () => frappe.timeout(1),
-
- () => frappe.tests.click_button('Submit'),
- () => frappe.tests.click_button('Yes'),
- () => frappe.timeout(0.3),
-
- () => done()
- ]);
-});
diff --git a/erpnext/buying/doctype/purchase_order/tests/test_purchase_order_receipt.js b/erpnext/buying/doctype/purchase_order/tests/test_purchase_order_receipt.js
deleted file mode 100644
index daf8d6c..0000000
--- a/erpnext/buying/doctype/purchase_order/tests/test_purchase_order_receipt.js
+++ /dev/null
@@ -1,74 +0,0 @@
-QUnit.module('Buying');
-
-QUnit.test("test: purchase order receipt", function(assert) {
- assert.expect(5);
- let done = assert.async();
-
- frappe.run_serially([
- () => {
- return frappe.tests.make('Purchase Order', [
- {supplier: 'Test Supplier'},
- {is_subcontracted: 'No'},
- {buying_price_list: 'Test-Buying-USD'},
- {currency: 'USD'},
- {items: [
- [
- {"item_code": 'Test Product 1'},
- {"schedule_date": frappe.datetime.add_days(frappe.datetime.now_date(), 1)},
- {"expected_delivery_date": frappe.datetime.add_days(frappe.datetime.now_date(), 5)},
- {"qty": 5},
- {"uom": 'Unit'},
- {"rate": 100},
- {"warehouse": 'Stores - '+frappe.get_abbr(frappe.defaults.get_default("Company"))}
- ]
- ]},
- ]);
- },
-
- () => {
-
- // Check supplier and item details
- assert.ok(cur_frm.doc.supplier_name == 'Test Supplier', "Supplier name correct");
- assert.ok(cur_frm.doc.items[0].item_name == 'Test Product 1', "Item name correct");
- assert.ok(cur_frm.doc.items[0].description == 'Test Product 1', "Description correct");
- assert.ok(cur_frm.doc.items[0].qty == 5, "Quantity correct");
-
- },
-
- () => frappe.timeout(1),
-
- () => frappe.tests.click_button('Submit'),
- () => frappe.tests.click_button('Yes'),
-
- () => frappe.timeout(1.5),
- () => frappe.click_button('Close'),
- () => frappe.timeout(0.3),
-
- // Make Purchase Receipt
- () => frappe.click_button('Make'),
- () => frappe.timeout(0.3),
-
- () => frappe.click_link('Receipt'),
- () => frappe.timeout(2),
-
- () => cur_frm.save(),
-
- // Save and submit Purchase Receipt
- () => frappe.timeout(1),
- () => frappe.tests.click_button('Submit'),
- () => frappe.tests.click_button('Yes'),
- () => frappe.timeout(1),
-
- // View Purchase order in Stock Ledger
- () => frappe.click_button('View'),
- () => frappe.timeout(0.3),
-
- () => frappe.click_link('Stock Ledger'),
- () => frappe.timeout(2),
- () => {
- assert.ok($('div.slick-cell.l2.r2 > a').text().includes('Test Product 1')
- && $('div.slick-cell.l9.r9 > div').text().includes(5), "Stock ledger entry correct");
- },
- () => done()
- ]);
-});
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
deleted file mode 100644
index 83eb295..0000000
--- a/erpnext/buying/doctype/purchase_order/tests/test_purchase_order_with_discount_on_grand_total.js
+++ /dev/null
@@ -1,47 +0,0 @@
-QUnit.module('Buying');
-
-QUnit.test("test: purchase order with discount on grand total", function(assert) {
- assert.expect(4);
- let done = assert.async();
-
- frappe.run_serially([
- () => {
- return frappe.tests.make('Purchase Order', [
- {supplier: 'Test Supplier'},
- {is_subcontracted: 'No'},
- {buying_price_list: 'Test-Buying-EUR'},
- {currency: 'EUR'},
- {items: [
- [
- {"item_code": 'Test Product 4'},
- {"qty": 5},
- {"uom": 'Unit'},
- {"rate": 500 },
- {"schedule_date": frappe.datetime.add_days(frappe.datetime.now_date(), 1)},
- {"expected_delivery_date": frappe.datetime.add_days(frappe.datetime.now_date(), 5)},
- {"warehouse": 'Stores - '+frappe.get_abbr(frappe.defaults.get_default("Company"))}
- ]
- ]},
- {apply_discount_on: 'Grand Total'},
- {additional_discount_percentage: 10}
- ]);
- },
-
- () => frappe.timeout(1),
-
- () => {
- assert.ok(cur_frm.doc.supplier_name == 'Test Supplier', "Supplier name correct");
- assert.ok(cur_frm.doc.items[0].rate == 500, "Rate correct");
- // Calculate total
- assert.ok(cur_frm.doc.total == 2500, "Total correct");
- // Calculate grand total after discount
- assert.ok(cur_frm.doc.grand_total == 2250, "Grand total correct");
- },
-
- () => frappe.tests.click_button('Submit'),
- () => frappe.tests.click_button('Yes'),
- () => frappe.timeout(0.3),
-
- () => done()
- ]);
-});
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
deleted file mode 100644
index a729dd9..0000000
--- a/erpnext/buying/doctype/purchase_order/tests/test_purchase_order_with_item_wise_discount.js
+++ /dev/null
@@ -1,44 +0,0 @@
-QUnit.module('Buying');
-
-QUnit.test("test: purchase order with item wise discount", function(assert) {
- assert.expect(4);
- let done = assert.async();
-
- frappe.run_serially([
- () => {
- return frappe.tests.make('Purchase Order', [
- {supplier: 'Test Supplier'},
- {is_subcontracted: 'No'},
- {buying_price_list: 'Test-Buying-EUR'},
- {currency: 'EUR'},
- {items: [
- [
- {"item_code": 'Test Product 4'},
- {"qty": 5},
- {"uom": 'Unit'},
- {"schedule_date": frappe.datetime.add_days(frappe.datetime.now_date(), 1)},
- {"expected_delivery_date": frappe.datetime.add_days(frappe.datetime.now_date(), 5)},
- {"warehouse": 'Stores - '+frappe.get_abbr(frappe.defaults.get_default("Company"))},
- {"discount_percentage": 20}
- ]
- ]}
- ]);
- },
-
- () => frappe.timeout(1),
-
- () => {
- assert.ok(cur_frm.doc.supplier_name == 'Test Supplier', "Supplier name correct");
- assert.ok(cur_frm.doc.items[0].discount_percentage == 20, "Discount correct");
- // Calculate totals after discount
- assert.ok(cur_frm.doc.total == 2000, "Total correct");
- assert.ok(cur_frm.doc.grand_total == 2000, "Grand total correct");
- },
-
- () => frappe.tests.click_button('Submit'),
- () => frappe.tests.click_button('Yes'),
- () => frappe.timeout(0.3),
-
- () => done()
- ]);
-});
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
deleted file mode 100644
index b605e76..0000000
--- a/erpnext/buying/doctype/purchase_order/tests/test_purchase_order_with_multi_uom.js
+++ /dev/null
@@ -1,39 +0,0 @@
-QUnit.module('Buying');
-
-QUnit.test("test: purchase order with multi UOM", function(assert) {
- assert.expect(3);
- let done = assert.async();
-
- frappe.run_serially([
- () => {
- return frappe.tests.make('Purchase Order', [
- {supplier: 'Test Supplier'},
- {is_subcontracted: 'No'},
- {items: [
- [
- {"item_code": 'Test Product 4'},
- {"qty": 5},
- {"uom": 'Unit'},
- {"rate": 100},
- {"schedule_date": frappe.datetime.add_days(frappe.datetime.now_date(), 1)},
- {"expected_delivery_date": frappe.datetime.add_days(frappe.datetime.now_date(), 5)},
- {"warehouse": 'Stores - '+frappe.get_abbr(frappe.defaults.get_default("Company"))}
- ]
- ]}
- ]);
- },
-
- () => {
- assert.ok(cur_frm.doc.supplier_name == 'Test Supplier', "Supplier name correct");
- assert.ok(cur_frm.doc.items[0].item_name == 'Test Product 4', "Item name correct");
- assert.ok(cur_frm.doc.items[0].uom == 'Unit', "Multi UOM correct");
- },
-
- () => frappe.timeout(1),
- () => frappe.tests.click_button('Submit'),
- () => frappe.tests.click_button('Yes'),
- () => frappe.timeout(0.3),
-
- () => done()
- ]);
-});
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
deleted file mode 100644
index c258756..0000000
--- a/erpnext/buying/doctype/purchase_order/tests/test_purchase_order_with_shipping_rule.js
+++ /dev/null
@@ -1,43 +0,0 @@
-QUnit.module('Buying');
-
-QUnit.test("test: purchase order with shipping rule", function(assert) {
- assert.expect(3);
- let done = assert.async();
-
- frappe.run_serially([
- () => {
- return frappe.tests.make('Purchase Order', [
- {supplier: 'Test Supplier'},
- {is_subcontracted: 'No'},
- {buying_price_list: 'Test-Buying-USD'},
- {currency: 'USD'},
- {"schedule_date": frappe.datetime.add_days(frappe.datetime.now_date(), 1)},
- {items: [
- [
- {"item_code": 'Test Product 4'},
- {"qty": 5},
- {"uom": 'Unit'},
- {"rate": 500 },
- {"schedule_date": frappe.datetime.add_days(frappe.datetime.now_date(), 1)},
- {"expected_delivery_date": frappe.datetime.add_days(frappe.datetime.now_date(), 5)},
- {"warehouse": 'Stores - '+frappe.get_abbr(frappe.defaults.get_default("Company"))}
- ]
- ]},
-
- {shipping_rule:'Two Day Shipping'}
- ]);
- },
-
- () => {
- // Check grand total
- assert.ok(cur_frm.doc.total_taxes_and_charges == 200, "Taxes and charges correct");
- assert.ok(cur_frm.doc.grand_total == 2700, "Grand total correct");
- },
-
- () => frappe.timeout(0.3),
- () => frappe.tests.click_button('Submit'),
- () => frappe.tests.click_button('Yes'),
- () => frappe.timeout(0.3),
- () => done()
- ]);
-});
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
deleted file mode 100644
index ccc383f..0000000
--- a/erpnext/buying/doctype/purchase_order/tests/test_purchase_order_with_taxes_and_charges.js
+++ /dev/null
@@ -1,44 +0,0 @@
-QUnit.module('Buying');
-
-QUnit.test("test: purchase order with taxes and charges", function(assert) {
- assert.expect(3);
- let done = assert.async();
-
- frappe.run_serially([
- () => {
- return frappe.tests.make('Purchase Order', [
- {supplier: 'Test Supplier'},
- {is_subcontracted: 'No'},
- {buying_price_list: 'Test-Buying-USD'},
- {currency: 'USD'},
- {"schedule_date": frappe.datetime.add_days(frappe.datetime.now_date(), 1)},
- {items: [
- [
- {"item_code": 'Test Product 4'},
- {"qty": 5},
- {"uom": 'Unit'},
- {"rate": 500 },
- {"schedule_date": frappe.datetime.add_days(frappe.datetime.now_date(), 1)},
- {"expected_delivery_date": frappe.datetime.add_days(frappe.datetime.now_date(), 5)},
- {"warehouse": 'Stores - '+frappe.get_abbr(frappe.defaults.get_default("Company"))}
- ]
- ]},
-
- {taxes_and_charges: 'TEST In State GST - FT'}
- ]);
- },
-
- () => {
- // Check taxes and calculate grand total
- assert.ok(cur_frm.doc.taxes[1].account_head=='SGST - '+frappe.get_abbr(frappe.defaults.get_default('Company')), "Account Head abbr correct");
- assert.ok(cur_frm.doc.total_taxes_and_charges == 225, "Taxes and charges correct");
- assert.ok(cur_frm.doc.grand_total == 2725, "Grand total correct");
- },
-
- () => frappe.timeout(0.3),
- () => frappe.tests.click_button('Submit'),
- () => frappe.tests.click_button('Yes'),
- () => frappe.timeout(0.3),
- () => done()
- ]);
-});
diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js
index bde00cb..e7049fd 100644
--- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js
+++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js
@@ -124,6 +124,14 @@
dialog.show()
},
+ schedule_date(frm) {
+ if(frm.doc.schedule_date){
+ frm.doc.items.forEach((item) => {
+ item.schedule_date = frm.doc.schedule_date;
+ })
+ }
+ refresh_field("items");
+ },
preview: (frm) => {
let dialog = new frappe.ui.Dialog({
title: __('Preview Email'),
@@ -184,7 +192,13 @@
dialog.show();
}
})
-
+frappe.ui.form.on("Request for Quotation Item", {
+ items_add(frm, cdt, cdn) {
+ if (frm.doc.schedule_date) {
+ frappe.model.set_value(cdt, cdn, 'schedule_date', frm.doc.schedule_date);
+ }
+ }
+});
frappe.ui.form.on("Request for Quotation Supplier",{
supplier: function(frm, cdt, cdn) {
var d = locals[cdt][cdn]
diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json
index 4ce4100..4993df9 100644
--- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json
+++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json
@@ -12,6 +12,7 @@
"vendor",
"column_break1",
"transaction_date",
+ "schedule_date",
"status",
"amended_from",
"suppliers_section",
@@ -246,16 +247,22 @@
"fieldname": "sec_break_email_2",
"fieldtype": "Section Break",
"hide_border": 1
+ },
+ {
+ "fieldname": "schedule_date",
+ "fieldtype": "Date",
+ "label": "Required Date"
}
],
"icon": "fa fa-shopping-cart",
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2020-11-05 22:04:29.017134",
+ "modified": "2021-11-24 17:47:49.909000",
"modified_by": "Administrator",
"module": "Buying",
"name": "Request for Quotation",
+ "naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
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
deleted file mode 100644
index 75f85f8..0000000
--- a/erpnext/buying/doctype/request_for_quotation/tests/test_request_for_quotation.js
+++ /dev/null
@@ -1,76 +0,0 @@
-QUnit.module('Buying');
-
-QUnit.test("test: request_for_quotation", function(assert) {
- assert.expect(14);
- let done = assert.async();
- let date;
- frappe.run_serially([
- () => {
- date = frappe.datetime.add_days(frappe.datetime.now_date(), 10);
- return frappe.tests.make('Request for Quotation', [
- {transaction_date: date},
- {suppliers: [
- [
- {"supplier": 'Test Supplier'},
- {"email_id": 'test@supplier.com'}
- ]
- ]},
- {items: [
- [
- {"item_code": 'Test Product 4'},
- {"qty": 5},
- {"schedule_date": frappe.datetime.add_days(frappe.datetime.now_date(),20)},
- {"warehouse": 'All Warehouses - '+frappe.get_abbr(frappe.defaults.get_default("Company"))}
- ]
- ]},
- {message_for_supplier: 'Please supply the specified items at the best possible rates'},
- {tc_name: 'Test Term 1'}
- ]);
- },
- () => frappe.timeout(3),
- () => {
- assert.ok(cur_frm.doc.transaction_date == date, "Date correct");
- assert.ok(cur_frm.doc.company == cur_frm.doc.company, "Company correct");
- assert.ok(cur_frm.doc.suppliers[0].supplier_name == 'Test Supplier', "Supplier name correct");
- assert.ok(cur_frm.doc.suppliers[0].contact == 'Contact 3-Test Supplier', "Contact correct");
- assert.ok(cur_frm.doc.suppliers[0].email_id == 'test@supplier.com', "Email id correct");
- assert.ok(cur_frm.doc.items[0].item_name == 'Test Product 4', "Item Name correct");
- assert.ok(cur_frm.doc.items[0].warehouse == 'All Warehouses - '+frappe.get_abbr(frappe.defaults.get_default("Company")), "Warehouse correct");
- assert.ok(cur_frm.doc.message_for_supplier == 'Please supply the specified items at the best possible rates', "Reply correct");
- assert.ok(cur_frm.doc.tc_name == 'Test Term 1', "Term name correct");
- },
- () => frappe.timeout(3),
- () => cur_frm.print_doc(),
- () => frappe.timeout(1),
- () => {
- assert.ok($('.btn-print-print').is(':visible'), "Print Format Available");
- assert.ok($('.section-break+ .section-break .column-break:nth-child(1) .value').text().includes("Test Product 4"), "Print Preview Works");
- },
- () => cur_frm.print_doc(),
- () => frappe.timeout(1),
- () => frappe.click_button('Get items from'),
- () => frappe.timeout(0.3),
- () => frappe.click_link('Material Request'),
- () => frappe.timeout(1),
- () => frappe.click_button('Get Items'),
- () => frappe.timeout(1),
- () => {
- assert.ok(cur_frm.doc.items[1].item_name == 'Test Product 1', "Getting items from material requests work");
- },
- () => cur_frm.save(),
- () => frappe.timeout(1),
- () => frappe.tests.click_button('Submit'),
- () => frappe.tests.click_button('Yes'),
- () => frappe.timeout(1),
- () => {
- assert.ok(cur_frm.doc.docstatus == 1, "Quotation request submitted");
- },
- () => frappe.click_button('Send Supplier Emails'),
- () => frappe.timeout(6),
- () => {
- assert.ok($('div.modal.fade.in > div.modal-dialog > div > div.modal-body.ui-front > div.msgprint').text().includes("Email sent to supplier Test Supplier"), "Send emails working");
- },
- () => frappe.click_button('Close'),
- () => done()
- ]);
-});
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
deleted file mode 100644
index f06c3f3..0000000
--- a/erpnext/buying/doctype/request_for_quotation/tests/test_request_for_quotation_for_status.js
+++ /dev/null
@@ -1,128 +0,0 @@
-QUnit.module('buying');
-
-QUnit.test("Test: Request for Quotation", function (assert) {
- assert.expect(5);
- let done = assert.async();
- let rfq_name = "";
-
- frappe.run_serially([
- // Go to RFQ list
- () => frappe.set_route("List", "Request for Quotation"),
- // Create a new RFQ
- () => frappe.new_doc("Request for Quotation"),
- () => frappe.timeout(1),
- () => cur_frm.set_value("transaction_date", "04-04-2017"),
- () => cur_frm.set_value("company", "For Testing"),
- // Add Suppliers
- () => {
- cur_frm.fields_dict.suppliers.grid.grid_rows[0].toggle_view();
- },
- () => frappe.timeout(1),
- () => {
- cur_frm.fields_dict.suppliers.grid.grid_rows[0].doc.supplier = "_Test Supplier";
- frappe.click_check('Send Email');
- cur_frm.cur_grid.frm.script_manager.trigger('supplier');
- },
- () => frappe.timeout(1),
- () => {
- cur_frm.cur_grid.toggle_view();
- },
- () => frappe.timeout(1),
- () => frappe.click_button('Add Row',0),
- () => frappe.timeout(1),
- () => {
- cur_frm.fields_dict.suppliers.grid.grid_rows[1].toggle_view();
- },
- () => frappe.timeout(1),
- () => {
- cur_frm.fields_dict.suppliers.grid.grid_rows[1].doc.supplier = "_Test Supplier 1";
- frappe.click_check('Send Email');
- cur_frm.cur_grid.frm.script_manager.trigger('supplier');
- },
- () => frappe.timeout(1),
- () => {
- cur_frm.cur_grid.toggle_view();
- },
- () => frappe.timeout(1),
- // Add Item
- () => {
- cur_frm.fields_dict.items.grid.grid_rows[0].toggle_view();
- },
- () => frappe.timeout(1),
- () => {
- cur_frm.fields_dict.items.grid.grid_rows[0].doc.item_code = "_Test Item";
- frappe.set_control('item_code',"_Test Item");
- frappe.set_control('qty',5);
- frappe.set_control('schedule_date', "05-05-2017");
- cur_frm.cur_grid.frm.script_manager.trigger('supplier');
- },
- () => frappe.timeout(2),
- () => {
- cur_frm.cur_grid.toggle_view();
- },
- () => frappe.timeout(2),
- () => {
- cur_frm.fields_dict.items.grid.grid_rows[0].doc.warehouse = "_Test Warehouse - FT";
- },
- () => frappe.click_button('Save'),
- () => frappe.timeout(1),
- () => frappe.click_button('Submit'),
- () => frappe.timeout(1),
- () => frappe.click_button('Yes'),
- () => frappe.timeout(1),
- () => frappe.click_button('Menu'),
- () => frappe.timeout(1),
- () => frappe.click_link('Reload'),
- () => frappe.timeout(1),
- () => {
- assert.equal(cur_frm.doc.docstatus, 1);
- rfq_name = cur_frm.doc.name;
- assert.ok(cur_frm.fields_dict.suppliers.grid.grid_rows[0].doc.quote_status == "Pending");
- assert.ok(cur_frm.fields_dict.suppliers.grid.grid_rows[1].doc.quote_status == "Pending");
- },
- () => {
- cur_frm.fields_dict.suppliers.grid.grid_rows[0].toggle_view();
- },
- () => frappe.timeout(1),
- () => frappe.timeout(1),
- () => {
- cur_frm.cur_grid.toggle_view();
- },
- () => frappe.click_button('Update'),
- () => frappe.timeout(1),
-
- () => frappe.click_button('Supplier Quotation'),
- () => frappe.timeout(1),
- () => frappe.click_link('Make'),
- () => frappe.timeout(1),
- () => {
- frappe.set_control('supplier',"_Test Supplier 1");
- },
- () => frappe.timeout(1),
- () => frappe.click_button('Make Supplier Quotation'),
- () => frappe.timeout(1),
- () => cur_frm.set_value("company", "For Testing"),
- () => cur_frm.fields_dict.items.grid.grid_rows[0].doc.rate = 4.99,
- () => frappe.timeout(1),
- () => frappe.click_button('Save'),
- () => frappe.timeout(1),
- () => frappe.click_button('Submit'),
- () => frappe.timeout(1),
- () => frappe.click_button('Yes'),
- () => frappe.timeout(1),
- () => frappe.set_route("List", "Request for Quotation"),
- () => frappe.timeout(2),
- () => frappe.set_route("List", "Request for Quotation"),
- () => frappe.timeout(2),
- () => frappe.click_link(rfq_name),
- () => frappe.timeout(1),
- () => frappe.click_button('Menu'),
- () => frappe.timeout(1),
- () => frappe.click_link('Reload'),
- () => frappe.timeout(1),
- () => {
- assert.ok(cur_frm.fields_dict.suppliers.grid.grid_rows[1].doc.quote_status == "Received");
- },
- () => done()
- ]);
-});
diff --git a/erpnext/buying/doctype/supplier/supplier.js b/erpnext/buying/doctype/supplier/supplier.js
index 7ee9196..f0899b0 100644
--- a/erpnext/buying/doctype/supplier/supplier.js
+++ b/erpnext/buying/doctype/supplier/supplier.js
@@ -83,6 +83,12 @@
frm.trigger("get_supplier_group_details");
}, __('Actions'));
+ if (cint(frappe.defaults.get_default("enable_common_party_accounting"))) {
+ frm.add_custom_button(__('Link with Customer'), function () {
+ frm.trigger('show_party_link_dialog');
+ }, __('Actions'));
+ }
+
// indicators
erpnext.utils.set_party_dashboard_indicators(frm);
}
@@ -128,5 +134,42 @@
else {
frm.toggle_reqd("represents_company", false);
}
+ },
+ show_party_link_dialog: function(frm) {
+ const dialog = new frappe.ui.Dialog({
+ title: __('Select a Customer'),
+ fields: [{
+ fieldtype: 'Link', label: __('Customer'),
+ options: 'Customer', fieldname: 'customer', reqd: 1
+ }],
+ primary_action: function({ customer }) {
+ frappe.call({
+ method: 'erpnext.accounts.doctype.party_link.party_link.create_party_link',
+ args: {
+ primary_role: 'Supplier',
+ primary_party: frm.doc.name,
+ secondary_party: customer
+ },
+ freeze: true,
+ callback: function() {
+ dialog.hide();
+ frappe.msgprint({
+ message: __('Successfully linked to Customer'),
+ alert: true
+ });
+ },
+ error: function() {
+ dialog.hide();
+ frappe.msgprint({
+ message: __('Linking to Customer Failed. Please try again.'),
+ title: __('Linking Failed'),
+ indicator: 'red'
+ });
+ }
+ });
+ },
+ primary_action_label: __('Create Link')
+ });
+ dialog.show();
}
});
diff --git a/erpnext/buying/doctype/supplier/supplier.py b/erpnext/buying/doctype/supplier/supplier.py
index ef00cb5..4f9ff43 100644
--- a/erpnext/buying/doctype/supplier/supplier.py
+++ b/erpnext/buying/doctype/supplier/supplier.py
@@ -11,7 +11,11 @@
)
from frappe.model.naming import set_name_by_naming_series, set_name_from_naming_options
-from erpnext.accounts.party import get_dashboard_info, validate_party_accounts
+from erpnext.accounts.party import ( # noqa
+ get_dashboard_info,
+ get_timeline_data,
+ validate_party_accounts,
+)
from erpnext.utilities.transaction_base import TransactionBase
diff --git a/erpnext/buying/doctype/supplier/test_supplier.js b/erpnext/buying/doctype/supplier/test_supplier.js
deleted file mode 100644
index eaa4d09..0000000
--- a/erpnext/buying/doctype/supplier/test_supplier.js
+++ /dev/null
@@ -1,77 +0,0 @@
-QUnit.module('Buying');
-
-QUnit.test("test: supplier", function(assert) {
- assert.expect(6);
- let done = assert.async();
- frappe.run_serially([
- () => {
- return frappe.tests.make('Supplier', [
- {supplier_name: 'Test Supplier'},
- {supplier_group: 'Hardware'},
- {country: 'India'},
- {default_currency: 'INR'},
- {accounts: [
- [
- {'company': "For Testing"},
- {'account': "Creditors - FT"}
- ]]
- }
- ]);
- },
- () => frappe.timeout(1),
- () => frappe.click_button('New Address'),
- () => {
- return frappe.tests.set_form_values(cur_frm, [
- {address_title:"Test3"},
- {address_type: "Billing"},
- {address_line1: "Billing Street 3"},
- {city: "Billing City 3"},
- ]);
- },
- () => cur_frm.save(),
- () => frappe.timeout(2),
- () => frappe.click_button('New Address'),
- () => {
- return frappe.tests.set_form_values(cur_frm, [
- {address_title:"Test3"},
- {address_type: "Shipping"},
- {address_line1: "Shipping Street 3"},
- {city: "Shipping City 3"},
- ]);
- },
- () => cur_frm.save(),
- () => frappe.timeout(2),
- () => frappe.click_button('New Address'),
- () => {
- return frappe.tests.set_form_values(cur_frm, [
- {address_title:"Test3"},
- {address_type: "Warehouse"},
- {address_line1: "Warehouse Street 3"},
- {city: "Warehouse City 3"},
- ]);
- },
- () => cur_frm.save(),
- () => frappe.timeout(2),
- () => frappe.click_button('New Contact'),
- () => {
- return frappe.tests.set_form_values(cur_frm, [
- {first_name: "Contact 3"},
- {email_id: "test@supplier.com"}
- ]);
- },
- () => cur_frm.save(),
- () => frappe.timeout(1),
- () => frappe.set_route('Form', 'Supplier', 'Test Supplier'),
- () => frappe.timeout(0.3),
-
- () => {
- assert.ok(cur_frm.doc.supplier_name == 'Test Supplier', "Name correct");
- assert.ok(cur_frm.doc.supplier_group == 'Hardware', "Type correct");
- assert.ok(cur_frm.doc.default_currency == 'INR', "Currency correct");
- assert.ok(cur_frm.doc.accounts[0].account == 'Creditors - '+frappe.get_abbr('For Testing'), " Account Head abbr correct");
- assert.ok($('.address-box:nth-child(3) p').text().includes('Shipping City 3'), "Address correct");
- assert.ok($('.col-sm-6+ .col-sm-6 .h6').text().includes('Contact 3'), "Contact correct");
- },
- () => done()
- ]);
-});
diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json
index 0a51a8e..023c95d 100644
--- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json
+++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json
@@ -17,6 +17,7 @@
"company",
"transaction_date",
"valid_till",
+ "quotation_number",
"amended_from",
"address_section",
"supplier_address",
@@ -797,6 +798,11 @@
"fieldtype": "Date",
"in_list_view": 1,
"label": "Valid Till"
+ },
+ {
+ "fieldname": "quotation_number",
+ "fieldtype": "Data",
+ "label": "Quotation Number"
}
],
"icon": "fa fa-shopping-cart",
@@ -804,10 +810,11 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2021-04-19 00:58:20.995491",
+ "modified": "2021-12-11 06:43:20.924080",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier Quotation",
+ "naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
diff --git a/erpnext/buying/doctype/supplier_quotation/tests/test_supplier_quotation.js b/erpnext/buying/doctype/supplier_quotation/tests/test_supplier_quotation.js
deleted file mode 100644
index 20fb430..0000000
--- a/erpnext/buying/doctype/supplier_quotation/tests/test_supplier_quotation.js
+++ /dev/null
@@ -1,74 +0,0 @@
-QUnit.module('Buying');
-
-QUnit.test("test: supplier quotation", function(assert) {
- assert.expect(11);
- let done = assert.async();
- let date;
-
- frappe.run_serially([
- () => {
- date = frappe.datetime.add_days(frappe.datetime.now_date(), 10);
- return frappe.tests.make('Supplier Quotation', [
- {supplier: 'Test Supplier'},
- {transaction_date: date},
- {currency: 'INR'},
- {items: [
- [
- {"item_code": 'Test Product 4'},
- {"qty": 5},
- {"uom": 'Unit'},
- {"rate": 200},
- {"warehouse": 'All Warehouses - '+frappe.get_abbr(frappe.defaults.get_default("Company"))}
- ]
- ]},
- {apply_discount_on: 'Grand Total'},
- {additional_discount_percentage: 10},
- {tc_name: 'Test Term 1'},
- {terms: 'This is a term'}
- ]);
- },
- () => frappe.timeout(3),
- () => {
- // Get Supplier details
- assert.ok(cur_frm.doc.supplier == 'Test Supplier', "Supplier correct");
- assert.ok(cur_frm.doc.company == cur_frm.doc.company, "Company correct");
- // Get Contact details
- assert.ok(cur_frm.doc.contact_person == 'Contact 3-Test Supplier', "Conatct correct");
- assert.ok(cur_frm.doc.contact_email == 'test@supplier.com', "Email correct");
- // Get uom
- assert.ok(cur_frm.doc.items[0].uom == 'Unit', "Multi uom correct");
- assert.ok(cur_frm.doc.total == 1000, "Total correct");
- // Calculate total after discount
- assert.ok(cur_frm.doc.grand_total == 900, "Grand total correct");
- // Get terms
- assert.ok(cur_frm.doc.tc_name == 'Test Term 1', "Terms correct");
- },
-
- () => cur_frm.print_doc(),
- () => frappe.timeout(2),
- () => {
- assert.ok($('.btn-print-print').is(':visible'), "Print Format Available");
- assert.ok($("table > tbody > tr > td:nth-child(3) > div").text().includes("Test Product 4"), "Print Preview Works As Expected");
- },
- () => cur_frm.print_doc(),
- () => frappe.timeout(1),
- () => frappe.click_button('Get items from'),
- () => frappe.timeout(0.3),
- () => frappe.click_link('Material Request'),
- () => frappe.timeout(0.3),
- () => frappe.click_button('Get Items'),
- () => frappe.timeout(1),
- () => {
- // Get item from Material Requests
- assert.ok(cur_frm.doc.items[1].item_name == 'Test Product 1', "Getting items from material requests work");
- },
-
- () => cur_frm.save(),
- () => frappe.timeout(1),
- () => frappe.tests.click_button('Submit'),
- () => frappe.tests.click_button('Yes'),
- () => frappe.timeout(0.3),
-
- () => done()
- ]);
-});
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
deleted file mode 100644
index 0a51565..0000000
--- a/erpnext/buying/doctype/supplier_quotation/tests/test_supplier_quotation_for_item_wise_discount.js
+++ /dev/null
@@ -1,34 +0,0 @@
-QUnit.module('Buying');
-
-QUnit.test("test: supplier quotation with item wise discount", function(assert){
- assert.expect(2);
- let done = assert.async();
-
- frappe.run_serially([
- () => {
- return frappe.tests.make('Supplier Quotation', [
- {supplier: 'Test Supplier'},
- {company: 'For Testing'},
- {items: [
- [
- {"item_code": 'Test Product 4'},
- {"qty": 5},
- {"uom": 'Unit'},
- {"warehouse": 'All Warehouses - FT'},
- {'discount_percentage': 10},
- ]
- ]}
- ]);
- },
-
- () => {
- assert.ok(cur_frm.doc.total == 900, "Total correct");
- assert.ok(cur_frm.doc.grand_total == 900, "Grand total correct");
- },
-
- () => frappe.tests.click_button('Submit'),
- () => frappe.tests.click_button('Yes'),
- () => frappe.timeout(0.3),
- () => done()
- ]);
-});
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
deleted file mode 100644
index 7ea3e60..0000000
--- a/erpnext/buying/doctype/supplier_quotation/tests/test_supplier_quotation_for_taxes_and_charges.js
+++ /dev/null
@@ -1,37 +0,0 @@
-QUnit.module('Buying');
-
-QUnit.test("test: supplier quotation with taxes and charges", function(assert) {
- assert.expect(3);
- let done = assert.async();
- let supplier_quotation_name;
-
- frappe.run_serially([
- () => {
- return frappe.tests.make('Supplier Quotation', [
- {supplier: 'Test Supplier'},
- {items: [
- [
- {"item_code": 'Test Product 4'},
- {"qty": 5},
- {"rate": 100},
- {"warehouse": 'Stores - '+frappe.get_abbr(frappe.defaults.get_default('Company'))},
- ]
- ]},
- {taxes_and_charges:'TEST In State GST - FT'},
- ]);
- },
- () => {supplier_quotation_name = cur_frm.doc.name;},
- () => {
- assert.ok(cur_frm.doc.taxes[0].account_head=='CGST - '+frappe.get_abbr(frappe.defaults.get_default('Company')), " Account Head abbr correct");
- assert.ok(cur_frm.doc.total_taxes_and_charges == 45, "Taxes and charges correct");
- assert.ok(cur_frm.doc.grand_total == 545, "Grand total correct");
- },
-
- () => cur_frm.save(),
- () => frappe.timeout(0.3),
- () => frappe.tests.click_button('Submit'),
- () => frappe.tests.click_button('Yes'),
- () => frappe.timeout(0.3),
- () => done()
- ]);
-});
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 3190fea..eab9e12 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -7,6 +7,7 @@
import frappe
from frappe import _, throw
from frappe.model.workflow import get_workflow_name, is_transition_condition_satisfied
+from frappe.query_builder.functions import Sum
from frappe.utils import (
add_days,
add_months,
@@ -112,7 +113,7 @@
_('{0} is blocked so this transaction cannot proceed').format(supplier_name), raise_exception=1)
def validate(self):
- if not self.get('is_return'):
+ if not self.get('is_return') and not self.get('is_debit_note'):
self.validate_qty_is_not_zero()
if self.get("_action") and self._action != "update_after_submit":
@@ -145,11 +146,6 @@
self.validate_party()
self.validate_currency()
- if self.doctype == 'Purchase Invoice':
- self.calculate_paid_amount()
- # apply tax withholding only if checked and applicable
- self.set_tax_withholding()
-
if self.doctype in ['Purchase Invoice', 'Sales Invoice']:
pos_check_field = "is_pos" if self.doctype=="Sales Invoice" else "is_paid"
if cint(self.allocate_advances_automatically) and not cint(self.get(pos_check_field)):
@@ -164,6 +160,11 @@
self.set_inter_company_account()
+ if self.doctype == 'Purchase Invoice':
+ self.calculate_paid_amount()
+ # apply tax withholding only if checked and applicable
+ self.set_tax_withholding()
+
validate_regional(self)
if self.doctype != 'Material Request':
@@ -184,6 +185,8 @@
frappe.throw(_("Row #{0}: Service Start Date cannot be greater than Service End Date").format(d.idx))
elif getdate(self.posting_date) > getdate(d.service_end_date):
frappe.throw(_("Row #{0}: Service End Date cannot be before Invoice Posting Date").format(d.idx))
+ elif getdate(self.posting_date) > getdate(d.service_start_date):
+ frappe.throw(_("Row #{0}: Service Start Date cannot be before Invoice Posting Date").format(d.idx))
def validate_invoice_documents_schedule(self):
self.validate_payment_schedule_dates()
@@ -250,7 +253,12 @@
from erpnext.controllers.taxes_and_totals import calculate_taxes_and_totals
calculate_taxes_and_totals(self)
- if self.doctype in ["Quotation", "Sales Order", "Delivery Note", "Sales Invoice"]:
+ if self.doctype in (
+ 'Sales Order',
+ 'Delivery Note',
+ 'Sales Invoice',
+ 'POS Invoice',
+ ):
self.calculate_commission()
self.calculate_contribution()
@@ -524,7 +532,8 @@
'is_opening': self.get("is_opening") or "No",
'party_type': None,
'party': None,
- 'project': self.get("project")
+ 'project': self.get("project"),
+ 'post_net_value': args.get('post_net_value')
})
accounting_dimensions = get_accounting_dimensions()
@@ -805,7 +814,6 @@
from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries
if self.doctype in ["Sales Invoice", "Purchase Invoice"]:
- self.update_allocated_advance_taxes_on_cancel()
if frappe.db.get_single_value('Accounts Settings', 'unlink_payment_on_cancellation_of_invoice'):
unlink_ref_doc_from_payment_entries(self)
@@ -853,29 +861,6 @@
return tax_map
- def update_allocated_advance_taxes_on_cancel(self):
- if self.get('advances'):
- tax_accounts = [d.account_head for d in self.get('taxes')]
- allocated_tax_map = frappe._dict(frappe.get_all('GL Entry', fields=['account', 'sum(credit - debit)'],
- filters={'voucher_no': self.name, 'account': ('in', tax_accounts)},
- group_by='account', as_list=1))
-
- tax_map = self.get_tax_map()
-
- for pe in self.get('advances'):
- if pe.reference_type == 'Payment Entry':
- pe = frappe.get_doc('Payment Entry', pe.reference_name)
- for tax in pe.get('taxes'):
- allocated_amount = tax_map.get(tax.account_head) - allocated_tax_map.get(tax.account_head)
- if allocated_amount > tax.tax_amount:
- allocated_amount = tax.tax_amount
-
- if allocated_amount:
- frappe.db.set_value('Advance Taxes and Charges', tax.name, 'allocated_amount',
- tax.allocated_amount - allocated_amount)
- 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
@@ -959,58 +944,10 @@
}, item=self)
)
- def allocate_advance_taxes(self, gl_entries):
- tax_map = self.get_tax_map()
- for pe in self.get("advances"):
- if pe.reference_type == "Payment Entry" and \
- frappe.db.get_value('Payment Entry', pe.reference_name, 'advance_tax_account'):
- pe = frappe.get_doc("Payment Entry", pe.reference_name)
- for tax in pe.get("taxes"):
- account_currency = get_account_currency(tax.account_head)
-
- if self.doctype == "Purchase Invoice":
- 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
- if tax_map.get(tax.account_head):
- amount = tax_map.get(tax.account_head)
- if amount < unallocated_amount:
- unallocated_amount = amount
-
- gl_entries.append(
- self.get_gl_dict({
- "account": tax.account_head,
- "against": party,
- dr_or_cr: unallocated_amount,
- dr_or_cr + "_in_account_currency": unallocated_amount
- if account_currency==self.company_currency
- else unallocated_amount,
- "cost_center": tax.cost_center
- }, account_currency, item=tax))
-
- gl_entries.append(
- self.get_gl_dict({
- "account": pe.advance_tax_account,
- "against": party,
- rev_dr_cr: unallocated_amount,
- rev_dr_cr + "_in_account_currency": unallocated_amount
- if account_currency==self.company_currency
- else unallocated_amount,
- "cost_center": tax.cost_center
- }, account_currency, item=tax))
-
- frappe.db.set_value("Advance Taxes and Charges", tax.name, "allocated_amount",
- tax.allocated_amount + unallocated_amount)
-
- tax_map[tax.account_head] -= unallocated_amount
def validate_multiple_billing(self, ref_dt, item_ref_dn, based_on, parentfield):
from erpnext.controllers.status_updater import get_allowance_for
+
item_allowance = {}
global_qty_allowance, global_amount_allowance = None, None
@@ -1031,12 +968,7 @@
.format(item.item_code, ref_dt), title=_("Warning"), indicator="orange")
continue
- already_billed = frappe.db.sql("""
- select sum(%s)
- from `tab%s`
- where %s=%s and docstatus=1 and parent != %s
- """ % (based_on, self.doctype + " Item", item_ref_dn, '%s', '%s'),
- (item.get(item_ref_dn), self.name))[0][0]
+ already_billed = self.get_billed_amount_for_item(item, item_ref_dn, based_on)
total_billed_amt = flt(flt(already_billed) + flt(item.get(based_on)),
self.precision(based_on, item))
@@ -1064,6 +996,43 @@
frappe.msgprint(_("Overbilling of {} ignored because you have {} role.")
.format(total_overbilled_amt, role_allowed_to_over_bill), indicator="orange", alert=True)
+ def get_billed_amount_for_item(self, item, item_ref_dn, based_on):
+ '''
+ Returns Sum of Amount of
+ Sales/Purchase Invoice Items
+ that are linked to `item_ref_dn` (`dn_detail` / `pr_detail`)
+ that are submitted OR not submitted but are under current invoice
+ '''
+
+ from frappe.query_builder import Criterion
+ from frappe.query_builder.functions import Sum
+
+ item_doctype = frappe.qb.DocType(item.doctype)
+ based_on_field = frappe.qb.Field(based_on)
+ join_field = frappe.qb.Field(item_ref_dn)
+
+ result = (
+ frappe.qb.from_(item_doctype)
+ .select(Sum(based_on_field))
+ .where(
+ join_field == item.get(item_ref_dn)
+ ).where(
+ Criterion.any([ # select all items from other invoices OR current invoices
+ Criterion.all([ # for selecting items from other invoices
+ item_doctype.docstatus == 1,
+ item_doctype.parent != self.name
+ ]),
+ Criterion.all([ # for selecting items from current invoice, that are linked to same reference
+ item_doctype.docstatus == 0,
+ item_doctype.parent == self.name,
+ item_doctype.name != item.name
+ ])
+ ])
+ )
+ ).run()
+
+ return result[0][0] if result else 0
+
def throw_overbill_exception(self, item, max_allowed_amt):
frappe.throw(_("Cannot overbill for Item {0} in row {1} more than {2}. To allow over-billing, please set allowance in Accounts Settings")
.format(item.item_code, item.idx, max_allowed_amt))
@@ -1718,58 +1687,69 @@
def update_invoice_status():
"""Updates status as Overdue for applicable invoices. Runs daily."""
today = getdate()
-
+ payment_schedule = frappe.qb.DocType("Payment Schedule")
for doctype in ("Sales Invoice", "Purchase Invoice"):
- frappe.db.sql("""
- UPDATE `tab{doctype}` invoice SET invoice.status = 'Overdue'
- WHERE invoice.docstatus = 1
- AND invoice.status REGEXP '^Unpaid|^Partly Paid'
- AND invoice.outstanding_amount > 0
- AND (
- {or_condition}
- (
- (
- CASE
- WHEN invoice.party_account_currency = invoice.currency
- THEN (
- CASE
- WHEN invoice.disable_rounded_total
- THEN invoice.grand_total
- ELSE invoice.rounded_total
- END
- )
- ELSE (
- CASE
- WHEN invoice.disable_rounded_total
- THEN invoice.base_grand_total
- ELSE invoice.base_rounded_total
- END
- )
- END
- ) - invoice.outstanding_amount
- ) < (
- SELECT SUM(
- CASE
- WHEN invoice.party_account_currency = invoice.currency
- THEN ps.payment_amount
- ELSE ps.base_payment_amount
- END
- )
- FROM `tabPayment Schedule` ps
- WHERE ps.parent = invoice.name
- AND ps.due_date < %(today)s
- )
- )
- """.format(
- doctype=doctype,
- or_condition=(
- "invoice.is_pos AND invoice.due_date < %(today)s OR"
- if doctype == "Sales Invoice"
- else ""
- )
- ), {"today": today}
+ invoice = frappe.qb.DocType(doctype)
+
+ consider_base_amount = invoice.party_account_currency != invoice.currency
+ payment_amount = (
+ frappe.qb.terms.Case()
+ .when(consider_base_amount, payment_schedule.base_payment_amount)
+ .else_(payment_schedule.payment_amount)
)
+ payable_amount = (
+ frappe.qb.from_(payment_schedule)
+ .select(Sum(payment_amount))
+ .where(
+ (payment_schedule.parent == invoice.name)
+ & (payment_schedule.due_date < today)
+ )
+ )
+
+ total = (
+ frappe.qb.terms.Case()
+ .when(invoice.disable_rounded_total, invoice.grand_total)
+ .else_(invoice.rounded_total)
+ )
+
+ base_total = (
+ frappe.qb.terms.Case()
+ .when(invoice.disable_rounded_total, invoice.base_grand_total)
+ .else_(invoice.base_rounded_total)
+ )
+
+ total_amount = (
+ frappe.qb.terms.Case()
+ .when(consider_base_amount, base_total)
+ .else_(total)
+ )
+
+ is_overdue = total_amount - invoice.outstanding_amount < payable_amount
+
+ conditions = (
+ (invoice.docstatus == 1)
+ & (invoice.outstanding_amount > 0)
+ & (
+ invoice.status.like("Unpaid%")
+ | invoice.status.like("Partly Paid%")
+ )
+ & (
+ ((invoice.is_pos & invoice.due_date < today) | is_overdue)
+ if doctype == "Sales Invoice"
+ else is_overdue
+ )
+ )
+
+ status = (
+ frappe.qb.terms.Case()
+ .when(invoice.status.like("%Discounted"), "Overdue and Discounted")
+ .else_("Overdue")
+ )
+
+ frappe.qb.update(invoice).set("status", status).where(conditions).run()
+
+
@frappe.whitelist()
def get_payment_terms(terms_template, posting_date=None, grand_total=None, base_grand_total=None, bill_date=None):
if not terms_template:
@@ -2139,6 +2119,11 @@
parent.update_status_updater()
else:
parent.check_credit_limit()
+
+ # reset index of child table
+ for idx, row in enumerate(parent.get(child_docname), start=1):
+ row.idx = idx
+
parent.save()
if parent_doctype == 'Purchase Order':
diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py
index 9965c87..a3d2502 100644
--- a/erpnext/controllers/buying_controller.py
+++ b/erpnext/controllers/buying_controller.py
@@ -3,7 +3,7 @@
import frappe
-from frappe import _, msgprint
+from frappe import ValidationError, _, msgprint
from frappe.contacts.doctype.address.address import get_address_display
from frappe.utils import cint, cstr, flt, getdate
@@ -17,6 +17,9 @@
from erpnext.stock.utils import get_incoming_rate
+class QtyMismatchError(ValidationError):
+ pass
+
class BuyingController(StockController, Subcontracting):
def get_feed(self):
@@ -360,19 +363,15 @@
def validate_accepted_rejected_qty(self):
for d in self.get("items"):
self.validate_negative_quantity(d, ["received_qty","qty", "rejected_qty"])
- if not flt(d.received_qty) and flt(d.qty):
- d.received_qty = flt(d.qty) - flt(d.rejected_qty)
- elif not flt(d.qty) and flt(d.rejected_qty):
- d.qty = flt(d.received_qty) - flt(d.rejected_qty)
+ if not flt(d.received_qty) and (flt(d.qty) or flt(d.rejected_qty)):
+ d.received_qty = flt(d.qty) + flt(d.rejected_qty)
- elif not flt(d.rejected_qty):
- d.rejected_qty = flt(d.received_qty) - flt(d.qty)
-
- val = flt(d.qty) + flt(d.rejected_qty)
# Check Received Qty = Accepted Qty + Rejected Qty
+ val = flt(d.qty) + flt(d.rejected_qty)
if (flt(val, d.precision("received_qty")) != flt(d.received_qty, d.precision("received_qty"))):
- frappe.throw(_("Accepted + Rejected Qty must be equal to Received quantity for Item {0}").format(d.item_code))
+ message = _("Row #{0}: Received Qty must be equal to Accepted + Rejected Qty for Item {1}").format(d.idx, d.item_code)
+ frappe.throw(msg=message, title=_("Mismatch"), exc=QtyMismatchError)
def validate_negative_quantity(self, item_row, field_list):
if self.is_return:
diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py
index 76a7d1a..dc04dab 100644
--- a/erpnext/controllers/queries.py
+++ b/erpnext/controllers/queries.py
@@ -539,6 +539,10 @@
dimension_filters = get_dimension_filter_map()
dimension_filters = dimension_filters.get((filters.get('dimension'),filters.get('account')))
query_filters = []
+ or_filters = []
+ fields = ['name']
+
+ searchfields = frappe.get_meta(doctype).get_search_fields()
meta = frappe.get_meta(doctype)
if meta.is_tree:
@@ -550,8 +554,9 @@
if meta.has_field('company'):
query_filters.append(['company', '=', filters.get('company')])
- if txt:
- query_filters.append([searchfield, 'LIKE', "%%%s%%" % txt])
+ for field in searchfields:
+ or_filters.append([field, 'LIKE', "%%%s%%" % txt])
+ fields.append(field)
if dimension_filters:
if dimension_filters['allow_or_restrict'] == 'Allow':
@@ -566,10 +571,9 @@
query_filters.append(['name', query_selector, dimensions])
- output = frappe.get_list(doctype, filters=query_filters)
- result = [d.name for d in output]
+ output = frappe.get_list(doctype, fields=fields, filters=query_filters, or_filters=or_filters, as_list=1)
- return [(d,) for d in set(result)]
+ return [tuple(d) for d in set(output)]
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py
index dad3ed7..cc773b7 100644
--- a/erpnext/controllers/selling_controller.py
+++ b/erpnext/controllers/selling_controller.py
@@ -120,13 +120,27 @@
self.in_words = money_in_words(amount, self.currency)
def calculate_commission(self):
- if self.meta.get_field("commission_rate"):
- self.round_floats_in(self, ["base_net_total", "commission_rate"])
- if self.commission_rate > 100.0:
- throw(_("Commission rate cannot be greater than 100"))
+ if not self.meta.get_field("commission_rate"):
+ return
- self.total_commission = flt(self.base_net_total * self.commission_rate / 100.0,
- self.precision("total_commission"))
+ self.round_floats_in(
+ self, ("amount_eligible_for_commission", "commission_rate")
+ )
+
+ if not (0 <= self.commission_rate <= 100.0):
+ throw("{} {}".format(
+ _(self.meta.get_label("commission_rate")),
+ _("must be between 0 and 100"),
+ ))
+
+ self.amount_eligible_for_commission = sum(
+ item.base_net_amount for item in self.items if item.grant_commission
+ )
+
+ self.total_commission = flt(
+ self.amount_eligible_for_commission * self.commission_rate / 100.0,
+ self.precision("total_commission")
+ )
def calculate_contribution(self):
if not self.meta.get_field("sales_team"):
@@ -138,7 +152,7 @@
self.round_floats_in(sales_person)
sales_person.allocated_amount = flt(
- self.base_net_total * sales_person.allocated_percentage / 100.0,
+ self.amount_eligible_for_commission * sales_person.allocated_percentage / 100.0,
self.precision("allocated_amount", sales_person))
if sales_person.commission_rate:
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index 08d422d..7073e32 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -17,7 +17,7 @@
from erpnext.accounts.utils import get_fiscal_year
from erpnext.controllers.accounts_controller import AccountsController
from erpnext.stock import get_warehouse_account_map
-from erpnext.stock.stock_ledger import get_valuation_rate
+from erpnext.stock.stock_ledger import get_items_to_be_repost, get_valuation_rate
class QualityInspectionRequiredError(frappe.ValidationError): pass
@@ -134,7 +134,7 @@
"against": expense_account,
"cost_center": item_row.cost_center,
"project": item_row.project or self.get('project'),
- "remarks": self.get("remarks") or "Accounting Entry for Stock",
+ "remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"debit": flt(sle.stock_value_difference, precision),
"is_opening": item_row.get("is_opening") or self.get("is_opening") or "No",
}, warehouse_account[sle.warehouse]["account_currency"], item=item_row))
@@ -143,7 +143,7 @@
"account": expense_account,
"against": warehouse_account[sle.warehouse]["account"],
"cost_center": item_row.cost_center,
- "remarks": self.get("remarks") or "Accounting Entry for Stock",
+ "remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"credit": flt(sle.stock_value_difference, precision),
"project": item_row.get("project") or self.get("project"),
"is_opening": item_row.get("is_opening") or self.get("is_opening") or "No"
@@ -544,7 +544,12 @@
"company": self.company
})
if future_sle_exists(args):
- create_repost_item_valuation_entry(args)
+ item_based_reposting = cint(frappe.db.get_single_value("Stock Reposting Settings", "item_based_reposting"))
+ if item_based_reposting:
+ create_item_wise_repost_entries(voucher_type=self.doctype, voucher_no=self.name)
+ else:
+ create_repost_item_valuation_entry(args)
+
@frappe.whitelist()
def make_quality_inspections(doctype, docname, items):
@@ -676,5 +681,38 @@
repost_entry.company = args.company
repost_entry.allow_zero_rate = args.allow_zero_rate
repost_entry.flags.ignore_links = True
+ repost_entry.flags.ignore_permissions = True
repost_entry.save()
repost_entry.submit()
+
+
+def create_item_wise_repost_entries(voucher_type, voucher_no, allow_zero_rate=False):
+ """Using a voucher create repost item valuation records for all item-warehouse pairs."""
+
+ stock_ledger_entries = get_items_to_be_repost(voucher_type, voucher_no)
+
+ distinct_item_warehouses = set()
+ repost_entries = []
+
+ for sle in stock_ledger_entries:
+ item_wh = (sle.item_code, sle.warehouse)
+ if item_wh in distinct_item_warehouses:
+ continue
+ distinct_item_warehouses.add(item_wh)
+
+ repost_entry = frappe.new_doc("Repost Item Valuation")
+ repost_entry.based_on = "Item and Warehouse"
+ repost_entry.voucher_type = voucher_type
+ repost_entry.voucher_no = voucher_no
+
+ repost_entry.item_code = sle.item_code
+ repost_entry.warehouse = sle.warehouse
+ repost_entry.posting_date = sle.posting_date
+ repost_entry.posting_time = sle.posting_time
+ repost_entry.allow_zero_rate = allow_zero_rate
+ repost_entry.flags.ignore_links = True
+ repost_entry.flags.ignore_permissions = True
+ repost_entry.submit()
+ repost_entries.append(repost_entry)
+
+ return repost_entries
diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py
index 667edab..075e3e3 100644
--- a/erpnext/controllers/taxes_and_totals.py
+++ b/erpnext/controllers/taxes_and_totals.py
@@ -50,6 +50,7 @@
self.initialize_taxes()
self.determine_exclusive_rate()
self.calculate_net_total()
+ self.calculate_shipping_charges()
self.calculate_taxes()
self.manipulate_grand_total_for_inclusive_tax()
self.calculate_totals()
@@ -138,6 +139,8 @@
if not item.qty and self.doc.get("is_return"):
item.amount = flt(-1 * item.rate, item.precision("amount"))
+ elif not item.qty and self.doc.get("is_debit_note"):
+ item.amount = flt(item.rate, item.precision("amount"))
else:
item.amount = flt(item.rate * item.qty, item.precision("amount"))
@@ -258,6 +261,11 @@
self.doc.round_floats_in(self.doc, ["total", "base_total", "net_total", "base_net_total"])
+ def calculate_shipping_charges(self):
+ if hasattr(self.doc, "shipping_rule") and self.doc.shipping_rule:
+ shipping_rule = frappe.get_doc("Shipping Rule", self.doc.shipping_rule)
+ shipping_rule.apply(self.doc)
+
def calculate_taxes(self):
if not self.doc.get('is_consolidated'):
self.doc.rounding_adjustment = 0
@@ -588,13 +596,14 @@
if self.doc.doctype in ["Sales Invoice", "Purchase Invoice"]:
grand_total = self.doc.rounded_total or self.doc.grand_total
+ base_grand_total = self.doc.base_rounded_total or self.doc.base_grand_total
+
if self.doc.party_account_currency == self.doc.currency:
total_amount_to_pay = flt(grand_total - self.doc.total_advance
- flt(self.doc.write_off_amount), self.doc.precision("grand_total"))
else:
- total_amount_to_pay = flt(flt(grand_total *
- self.doc.conversion_rate, self.doc.precision("grand_total")) - self.doc.total_advance
- - flt(self.doc.base_write_off_amount), self.doc.precision("grand_total"))
+ total_amount_to_pay = flt(flt(base_grand_total, self.doc.precision("base_grand_total")) - self.doc.total_advance
+ - flt(self.doc.base_write_off_amount), self.doc.precision("base_grand_total"))
self.doc.round_floats_in(self.doc, ["paid_amount"])
change_amount = 0
diff --git a/erpnext/controllers/tests/test_queries.py b/erpnext/controllers/tests/test_queries.py
index 05541d1..908d78c 100644
--- a/erpnext/controllers/tests/test_queries.py
+++ b/erpnext/controllers/tests/test_queries.py
@@ -1,6 +1,8 @@
import unittest
from functools import partial
+import frappe
+
from erpnext.controllers import queries
@@ -85,3 +87,6 @@
wh = query(filters=[["Bin", "item_code", "=", "_Test Item"]])
self.assertGreaterEqual(len(wh), 1)
+
+ def test_default_uoms(self):
+ self.assertGreaterEqual(frappe.db.count("UOM", {"enabled": 1}), 10)
diff --git a/erpnext/controllers/tests/test_transaction_base.py b/erpnext/controllers/tests/test_transaction_base.py
new file mode 100644
index 0000000..13aa697
--- /dev/null
+++ b/erpnext/controllers/tests/test_transaction_base.py
@@ -0,0 +1,22 @@
+import unittest
+
+import frappe
+
+
+class TestUtils(unittest.TestCase):
+ def test_reset_default_field_value(self):
+ doc = frappe.get_doc({
+ "doctype": "Purchase Receipt",
+ "set_warehouse": "Warehouse 1",
+ })
+
+ # Same values
+ doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}]
+ doc.reset_default_field_value("set_warehouse", "items", "warehouse")
+ self.assertEqual(doc.set_warehouse, "Warehouse 1")
+
+ # Mixed values
+ doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 2"}, {"warehouse": "Warehouse 1"}]
+ doc.reset_default_field_value("set_warehouse", "items", "warehouse")
+ self.assertEqual(doc.set_warehouse, None)
+
diff --git a/erpnext/agriculture/__init__.py b/erpnext/crm/doctype/crm_settings/__init__.py
similarity index 100%
copy from erpnext/agriculture/__init__.py
copy to erpnext/crm/doctype/crm_settings/__init__.py
diff --git a/erpnext/crm/doctype/crm_settings/crm_settings.js b/erpnext/crm/doctype/crm_settings/crm_settings.js
new file mode 100644
index 0000000..c6569d8
--- /dev/null
+++ b/erpnext/crm/doctype/crm_settings/crm_settings.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('CRM Settings', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/erpnext/crm/doctype/crm_settings/crm_settings.json b/erpnext/crm/doctype/crm_settings/crm_settings.json
new file mode 100644
index 0000000..a2a19b9
--- /dev/null
+++ b/erpnext/crm/doctype/crm_settings/crm_settings.json
@@ -0,0 +1,147 @@
+{
+ "actions": [],
+ "creation": "2021-09-09 17:03:22.754446",
+ "description": "Settings for Selling Module",
+ "doctype": "DocType",
+ "document_type": "Other",
+ "engine": "InnoDB",
+ "field_order": [
+ "section_break_5",
+ "campaign_naming_by",
+ "allow_lead_duplication_based_on_emails",
+ "column_break_4",
+ "create_event_on_next_contact_date",
+ "auto_creation_of_contact",
+ "opportunity_section",
+ "close_opportunity_after_days",
+ "column_break_9",
+ "create_event_on_next_contact_date_opportunity",
+ "quotation_section",
+ "default_valid_till",
+ "section_break_13",
+ "carry_forward_communication_and_comments"
+ ],
+ "fields": [
+ {
+ "fieldname": "campaign_naming_by",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "label": "Campaign Naming By",
+ "options": "Campaign Name\nNaming Series"
+ },
+ {
+ "fieldname": "column_break_9",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "default_valid_till",
+ "fieldtype": "Data",
+ "label": "Default Quotation Validity Days"
+ },
+ {
+ "fieldname": "section_break_5",
+ "fieldtype": "Section Break",
+ "label": "Lead"
+ },
+ {
+ "default": "0",
+ "fieldname": "allow_lead_duplication_based_on_emails",
+ "fieldtype": "Check",
+ "label": "Allow Lead Duplication based on Emails"
+ },
+ {
+ "default": "1",
+ "fieldname": "auto_creation_of_contact",
+ "fieldtype": "Check",
+ "label": "Auto Creation of Contact"
+ },
+ {
+ "default": "1",
+ "fieldname": "create_event_on_next_contact_date",
+ "fieldtype": "Check",
+ "label": "Create Event on Next Contact Date"
+ },
+ {
+ "fieldname": "opportunity_section",
+ "fieldtype": "Section Break",
+ "label": "Opportunity"
+ },
+ {
+ "default": "15",
+ "description": "Auto close Opportunity Replied after the no. of days mentioned above",
+ "fieldname": "close_opportunity_after_days",
+ "fieldtype": "Int",
+ "label": "Close Replied Opportunity After Days"
+ },
+ {
+ "default": "1",
+ "fieldname": "create_event_on_next_contact_date_opportunity",
+ "fieldtype": "Check",
+ "label": "Create Event on Next Contact Date"
+ },
+ {
+ "fieldname": "column_break_4",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "quotation_section",
+ "fieldtype": "Section Break",
+ "label": "Quotation"
+ },
+ {
+ "fieldname": "section_break_13",
+ "fieldtype": "Section Break",
+ "label": "Other Settings"
+ },
+ {
+ "default": "0",
+ "description": "All the Comments and Emails will be copied from one document to another newly created document(Lead -> Opportunity -> Quotation) throughout the CRM documents.",
+ "fieldname": "carry_forward_communication_and_comments",
+ "fieldtype": "Check",
+ "label": "Carry Forward Communication and Comments"
+ }
+ ],
+ "icon": "fa fa-cog",
+ "index_web_pages_for_search": 1,
+ "issingle": 1,
+ "links": [],
+ "modified": "2021-12-20 12:51:38.894252",
+ "modified_by": "Administrator",
+ "module": "CRM",
+ "name": "CRM Settings",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "role": "Sales Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "role": "Sales Master Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/crm/doctype/crm_settings/crm_settings.py b/erpnext/crm/doctype/crm_settings/crm_settings.py
new file mode 100644
index 0000000..bde5254
--- /dev/null
+++ b/erpnext/crm/doctype/crm_settings/crm_settings.py
@@ -0,0 +1,9 @@
+# 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 CRMSettings(Document):
+ pass
diff --git a/erpnext/crm/doctype/crm_settings/test_crm_settings.py b/erpnext/crm/doctype/crm_settings/test_crm_settings.py
new file mode 100644
index 0000000..3372c5d
--- /dev/null
+++ b/erpnext/crm/doctype/crm_settings/test_crm_settings.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+# import frappe
+import unittest
+
+
+class TestCRMSettings(unittest.TestCase):
+ pass
diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py
index c590523..c31b068 100644
--- a/erpnext/crm/doctype/lead/lead.py
+++ b/erpnext/crm/doctype/lead/lead.py
@@ -8,9 +8,9 @@
from frappe.email.inbox import link_communication_to_document
from frappe.model.mapper import get_mapped_doc
from frappe.utils import (
- cint,
comma_and,
cstr,
+ get_link_to_form,
getdate,
has_gravatar,
nowdate,
@@ -38,11 +38,7 @@
self.check_email_id_is_unique()
self.validate_email_id()
self.validate_contact_date()
- self._prev = frappe._dict({
- "contact_date": frappe.db.get_value("Lead", self.name, "contact_date") if (not cint(self.is_new())) else None,
- "ends_on": frappe.db.get_value("Lead", self.name, "ends_on") if (not cint(self.is_new())) else None,
- "contact_by": frappe.db.get_value("Lead", self.name, "contact_by") if (not cint(self.is_new())) else None,
- })
+ self.set_prev()
def set_full_name(self):
if self.first_name:
@@ -74,6 +70,16 @@
self.add_calendar_event()
self.update_prospects()
+ def set_prev(self):
+ if self.is_new():
+ self._prev = frappe._dict({
+ "contact_date": None,
+ "ends_on": None,
+ "contact_by": None
+ })
+ else:
+ self._prev = frappe.db.get_value("Lead", self.name, ["contact_date", "ends_on", "contact_by"], as_dict=1)
+
def before_insert(self):
self.contact_doc = self.create_contact()
@@ -91,13 +97,14 @@
self.contact_doc.save()
def add_calendar_event(self, opts=None, force=False):
- super(Lead, self).add_calendar_event({
- "owner": self.lead_owner,
- "starts_on": self.contact_date,
- "ends_on": self.ends_on or "",
- "subject": ('Contact ' + cstr(self.lead_name)),
- "description": ('Contact ' + cstr(self.lead_name)) + (self.contact_by and ('. By : ' + cstr(self.contact_by)) or '')
- }, force)
+ if frappe.db.get_single_value('CRM Settings', 'create_event_on_next_contact_date'):
+ super(Lead, self).add_calendar_event({
+ "owner": self.lead_owner,
+ "starts_on": self.contact_date,
+ "ends_on": self.ends_on or "",
+ "subject": ('Contact ' + cstr(self.lead_name)),
+ "description": ('Contact ' + cstr(self.lead_name)) + (self.contact_by and ('. By : ' + cstr(self.contact_by)) or '')
+ }, force)
def update_prospects(self):
prospects = frappe.get_all('Prospect Lead', filters={'lead': self.name}, fields=['parent'])
@@ -108,12 +115,13 @@
def check_email_id_is_unique(self):
if self.email_id:
# validate email is unique
- duplicate_leads = frappe.get_all("Lead", filters={"email_id": self.email_id, "name": ["!=", self.name]})
- duplicate_leads = [lead.name for lead in duplicate_leads]
+ if not frappe.db.get_single_value('CRM Settings', 'allow_lead_duplication_based_on_emails'):
+ duplicate_leads = frappe.get_all("Lead", filters={"email_id": self.email_id, "name": ["!=", self.name]})
+ duplicate_leads = [frappe.bold(get_link_to_form('Lead', lead.name)) for lead in duplicate_leads]
- if duplicate_leads:
- frappe.throw(_("Email Address must be unique, already exists for {0}")
- .format(comma_and(duplicate_leads)), frappe.DuplicateEntryError)
+ if duplicate_leads:
+ frappe.throw(_("Email Address must be unique, already exists for {0}")
+ .format(comma_and(duplicate_leads)), frappe.DuplicateEntryError)
def on_trash(self):
frappe.db.sql("""update `tabIssue` set lead='' where lead=%s""", self.name)
@@ -172,41 +180,42 @@
self.title = self.company_name or self.lead_name
def create_contact(self):
- if not self.lead_name:
- self.set_full_name()
- self.set_lead_name()
+ if frappe.db.get_single_value('CRM Settings', 'auto_creation_of_contact'):
+ if not self.lead_name:
+ self.set_full_name()
+ self.set_lead_name()
- contact = frappe.new_doc("Contact")
- contact.update({
- "first_name": self.first_name or self.lead_name,
- "last_name": self.last_name,
- "salutation": self.salutation,
- "gender": self.gender,
- "designation": self.designation,
- "company_name": self.company_name,
- })
-
- if self.email_id:
- contact.append("email_ids", {
- "email_id": self.email_id,
- "is_primary": 1
+ contact = frappe.new_doc("Contact")
+ contact.update({
+ "first_name": self.first_name or self.lead_name,
+ "last_name": self.last_name,
+ "salutation": self.salutation,
+ "gender": self.gender,
+ "designation": self.designation,
+ "company_name": self.company_name,
})
- if self.phone:
- contact.append("phone_nos", {
- "phone": self.phone,
- "is_primary_phone": 1
- })
+ if self.email_id:
+ contact.append("email_ids", {
+ "email_id": self.email_id,
+ "is_primary": 1
+ })
- if self.mobile_no:
- contact.append("phone_nos", {
- "phone": self.mobile_no,
- "is_primary_mobile_no":1
- })
+ if self.phone:
+ contact.append("phone_nos", {
+ "phone": self.phone,
+ "is_primary_phone": 1
+ })
- contact.insert(ignore_permissions=True)
+ if self.mobile_no:
+ contact.append("phone_nos", {
+ "phone": self.mobile_no,
+ "is_primary_mobile_no":1
+ })
- return contact
+ contact.insert(ignore_permissions=True)
+
+ return contact
@frappe.whitelist()
def make_customer(source_name, target_doc=None):
diff --git a/erpnext/crm/doctype/lead/test_lead.py b/erpnext/crm/doctype/lead/test_lead.py
index 56bfc8f..3882974 100644
--- a/erpnext/crm/doctype/lead/test_lead.py
+++ b/erpnext/crm/doctype/lead/test_lead.py
@@ -23,6 +23,17 @@
customer.customer_group = "_Test Customer Group"
customer.insert()
+ #check whether lead contact is carried forward to the customer.
+ contact = frappe.db.get_value('Dynamic Link', {
+ "parenttype": "Contact",
+ "link_doctype": "Lead",
+ "link_name": customer.lead_name,
+ }, "parent")
+
+ if contact:
+ contact_doc = frappe.get_doc("Contact", contact)
+ self.assertEqual(contact_doc.has_link(customer.doctype, customer.name), True)
+
def test_make_customer_from_organization(self):
from erpnext.crm.doctype.lead.lead import make_customer
diff --git a/erpnext/crm/doctype/lead/tests/test_lead_individual.js b/erpnext/crm/doctype/lead/tests/test_lead_individual.js
deleted file mode 100644
index 66d3337..0000000
--- a/erpnext/crm/doctype/lead/tests/test_lead_individual.js
+++ /dev/null
@@ -1,43 +0,0 @@
-QUnit.module("sales");
-
-QUnit.test("test: lead", function (assert) {
- assert.expect(4);
- let done = assert.async();
- let lead_name = frappe.utils.get_random(10);
- frappe.run_serially([
- // test lead creation
- () => frappe.set_route("List", "Lead"),
- () => frappe.new_doc("Lead"),
- () => frappe.timeout(1),
- () => cur_frm.set_value("lead_name", lead_name),
- () => cur_frm.save(),
- () => frappe.timeout(1),
- () => {
- assert.ok(cur_frm.doc.lead_name.includes(lead_name),
- 'name correctly set');
- frappe.lead_name = cur_frm.doc.name;
- },
- // create address and contact
- () => frappe.click_link('Address & Contact'),
- () => frappe.click_button('New Address'),
- () => frappe.timeout(1),
- () => frappe.set_control('address_line1', 'Gateway'),
- () => frappe.set_control('city', 'Mumbai'),
- () => cur_frm.save(),
- () => frappe.timeout(3),
- () => assert.equal(frappe.get_route()[1], 'Lead',
- 'back to lead form'),
- () => frappe.click_link('Address & Contact'),
- () => assert.ok($('.address-box').text().includes('Mumbai'),
- 'city is seen in address box'),
-
- // make opportunity
- () => frappe.click_button('Make'),
- () => frappe.click_link('Opportunity'),
- () => frappe.timeout(2),
- () => assert.equal(cur_frm.doc.lead, frappe.lead_name,
- 'lead name correctly mapped'),
-
- () => done()
- ]);
-});
diff --git a/erpnext/crm/doctype/lead/tests/test_lead_organization.js b/erpnext/crm/doctype/lead/tests/test_lead_organization.js
deleted file mode 100644
index 7fb9573..0000000
--- a/erpnext/crm/doctype/lead/tests/test_lead_organization.js
+++ /dev/null
@@ -1,55 +0,0 @@
-QUnit.module("sales");
-
-QUnit.test("test: lead", function (assert) {
- assert.expect(5);
- let done = assert.async();
- let lead_name = frappe.utils.get_random(10);
- frappe.run_serially([
- // test lead creation
- () => frappe.set_route("List", "Lead"),
- () => frappe.new_doc("Lead"),
- () => frappe.timeout(1),
- () => cur_frm.set_value("company_name", lead_name),
- () => cur_frm.save(),
- () => frappe.timeout(1),
- () => {
- assert.ok(cur_frm.doc.lead_name.includes(lead_name),
- 'name correctly set');
- frappe.lead_name = cur_frm.doc.name;
- },
- // create address and contact
- () => frappe.click_link('Address & Contact'),
- () => frappe.click_button('New Address'),
- () => frappe.timeout(1),
- () => frappe.set_control('address_line1', 'Gateway'),
- () => frappe.set_control('city', 'Mumbai'),
- () => cur_frm.save(),
- () => frappe.timeout(3),
- () => assert.equal(frappe.get_route()[1], 'Lead',
- 'back to lead form'),
- () => frappe.click_link('Address & Contact'),
- () => assert.ok($('.address-box').text().includes('Mumbai'),
- 'city is seen in address box'),
-
- () => frappe.click_button('New Contact'),
- () => frappe.timeout(1),
- () => frappe.set_control('first_name', 'John'),
- () => frappe.set_control('last_name', 'Doe'),
- () => cur_frm.save(),
- () => frappe.timeout(3),
- () => frappe.set_route('Form', 'Lead', cur_frm.doc.links[0].link_name),
- () => frappe.timeout(1),
- () => frappe.click_link('Address & Contact'),
- () => assert.ok($('.address-box').text().includes('John'),
- 'contact is seen in contact box'),
-
- // make customer
- () => frappe.click_button('Make'),
- () => frappe.click_link('Customer'),
- () => frappe.timeout(2),
- () => assert.equal(cur_frm.doc.lead_name, frappe.lead_name,
- 'lead name correctly mapped'),
-
- () => done()
- ]);
-});
diff --git a/erpnext/crm/doctype/opportunity/opportunity.json b/erpnext/crm/doctype/opportunity/opportunity.json
index feb6044..dc32d9a 100644
--- a/erpnext/crm/doctype/opportunity/opportunity.json
+++ b/erpnext/crm/doctype/opportunity/opportunity.json
@@ -510,8 +510,7 @@
"icon": "fa fa-info-sign",
"idx": 195,
"links": [],
- "migration_hash": "d87c646ea2579b6900197fd41e6c5c5a",
- "modified": "2021-10-21 11:04:30.151379",
+ "modified": "2021-10-21 12:04:30.151379",
"modified_by": "Administrator",
"module": "CRM",
"name": "Opportunity",
diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py
index 0bef80a..a4fd765 100644
--- a/erpnext/crm/doctype/opportunity/opportunity.py
+++ b/erpnext/crm/doctype/opportunity/opportunity.py
@@ -8,8 +8,10 @@
from frappe import _
from frappe.email.inbox import link_communication_to_document
from frappe.model.mapper import get_mapped_doc
+from frappe.query_builder import DocType
from frappe.utils import cint, cstr, flt, get_fullname
+from erpnext.crm.utils import add_link_in_communication, copy_comments
from erpnext.setup.utils import get_exchange_rate
from erpnext.utilities.transaction_base import TransactionBase
@@ -19,6 +21,11 @@
if self.opportunity_from == "Lead":
frappe.get_doc("Lead", self.party_name).set_status(update=True)
+ if self.opportunity_from in ["Lead", "Prospect"]:
+ if frappe.db.get_single_value("CRM Settings", "carry_forward_communication_and_comments"):
+ copy_comments(self.opportunity_from, self.party_name, self)
+ add_link_in_communication(self.opportunity_from, self.party_name, self)
+
def validate(self):
self._prev = frappe._dict({
"contact_date": frappe.db.get_value("Opportunity", self.name, "contact_date") if \
@@ -28,7 +35,6 @@
})
self.make_new_lead_if_required()
-
self.validate_item_details()
self.validate_uom_is_integer("uom", "qty")
self.validate_cust_name()
@@ -70,21 +76,21 @@
"""Set lead against new opportunity"""
if (not self.get("party_name")) and self.contact_email:
# check if customer is already created agains the self.contact_email
- customer = frappe.db.sql("""select
- distinct `tabDynamic Link`.link_name as customer
- from
- `tabContact`,
- `tabDynamic Link`
- where `tabContact`.email_id='{0}'
- and
- `tabContact`.name=`tabDynamic Link`.parent
- and
- ifnull(`tabDynamic Link`.link_name, '')<>''
- and
- `tabDynamic Link`.link_doctype='Customer'
- """.format(self.contact_email), as_dict=True)
- if customer and customer[0].customer:
- self.party_name = customer[0].customer
+ dynamic_link, contact = DocType("Dynamic Link"), DocType("Contact")
+ customer = frappe.qb.from_(
+ dynamic_link
+ ).join(
+ contact
+ ).on(
+ (contact.name == dynamic_link.parent)
+ & (dynamic_link.link_doctype == "Customer")
+ & (contact.email_id == self.contact_email)
+ ).select(
+ dynamic_link.link_name
+ ).distinct().run(as_dict=True)
+
+ if customer and customer[0].link_name:
+ self.party_name = customer[0].link_name
self.opportunity_from = "Customer"
return
@@ -191,30 +197,31 @@
self.add_calendar_event()
def add_calendar_event(self, opts=None, force=False):
- if not opts:
- opts = frappe._dict()
+ if frappe.db.get_single_value('CRM Settings', 'create_event_on_next_contact_date_opportunity'):
+ if not opts:
+ opts = frappe._dict()
- opts.description = ""
- opts.contact_date = self.contact_date
+ opts.description = ""
+ opts.contact_date = self.contact_date
- if self.party_name and self.opportunity_from == 'Customer':
- if self.contact_person:
- opts.description = 'Contact '+cstr(self.contact_person)
- else:
- opts.description = 'Contact customer '+cstr(self.party_name)
- elif self.party_name and self.opportunity_from == 'Lead':
- if self.contact_display:
- opts.description = 'Contact '+cstr(self.contact_display)
- else:
- opts.description = 'Contact lead '+cstr(self.party_name)
+ if self.party_name and self.opportunity_from == 'Customer':
+ if self.contact_person:
+ opts.description = 'Contact '+cstr(self.contact_person)
+ else:
+ opts.description = 'Contact customer '+cstr(self.party_name)
+ elif self.party_name and self.opportunity_from == 'Lead':
+ if self.contact_display:
+ opts.description = 'Contact '+cstr(self.contact_display)
+ else:
+ opts.description = 'Contact lead '+cstr(self.party_name)
- opts.subject = opts.description
- opts.description += '. By : ' + cstr(self.contact_by)
+ opts.subject = opts.description
+ opts.description += '. By : ' + cstr(self.contact_by)
- if self.to_discuss:
- opts.description += ' To Discuss : ' + cstr(self.to_discuss)
+ if self.to_discuss:
+ opts.description += ' To Discuss : ' + cstr(self.to_discuss)
- super(Opportunity, self).add_calendar_event(opts, force)
+ super(Opportunity, self).add_calendar_event(opts, force)
def validate_item_details(self):
if not self.get('items'):
@@ -363,7 +370,7 @@
def auto_close_opportunity():
""" auto close the `Replied` Opportunities after 7 days """
- auto_close_after_days = frappe.db.get_single_value("Selling Settings", "close_opportunity_after_days") or 15
+ auto_close_after_days = frappe.db.get_single_value("CRM Settings", "close_opportunity_after_days") or 15
opportunities = frappe.db.sql(""" select name from tabOpportunity where status='Replied' and
modified<DATE_SUB(CURDATE(), INTERVAL %s DAY) """, (auto_close_after_days), as_dict=True)
diff --git a/erpnext/crm/doctype/opportunity/test_opportunity.js b/erpnext/crm/doctype/opportunity/test_opportunity.js
deleted file mode 100644
index 45b97dd..0000000
--- a/erpnext/crm/doctype/opportunity/test_opportunity.js
+++ /dev/null
@@ -1,56 +0,0 @@
-QUnit.test("test: opportunity", function (assert) {
- assert.expect(8);
- let done = assert.async();
- frappe.run_serially([
- () => frappe.set_route('List', 'Opportunity'),
- () => frappe.timeout(1),
- () => frappe.click_button('New'),
- () => frappe.timeout(1),
- () => cur_frm.set_value('opportunity_from', 'Customer'),
- () => cur_frm.set_value('customer', 'Test Customer 1'),
-
- // check items
- () => cur_frm.set_value('with_items', 1),
- () => frappe.tests.set_grid_values(cur_frm, 'items', [
- [
- {item_code:'Test Product 1'},
- {qty: 4}
- ]
- ]),
- () => cur_frm.save(),
- () => frappe.timeout(1),
- () => {
- assert.notOk(cur_frm.is_new(), 'saved');
- frappe.opportunity_name = cur_frm.doc.name;
- },
-
- // close and re-open
- () => frappe.click_button('Close'),
- () => frappe.timeout(1),
- () => assert.equal(cur_frm.doc.status, 'Closed',
- 'closed'),
-
- () => frappe.click_button('Reopen'),
- () => assert.equal(cur_frm.doc.status, 'Open',
- 'reopened'),
- () => frappe.timeout(1),
-
- // make quotation
- () => frappe.click_button('Make'),
- () => frappe.click_link('Quotation', 1),
- () => frappe.timeout(2),
- () => {
- assert.equal(frappe.get_route()[1], 'Quotation',
- 'made quotation');
- assert.equal(cur_frm.doc.customer, 'Test Customer 1',
- 'customer set in quotation');
- assert.equal(cur_frm.doc.items[0].item_code, 'Test Product 1',
- 'item set in quotation');
- assert.equal(cur_frm.doc.items[0].qty, 4,
- 'qty set in quotation');
- assert.equal(cur_frm.doc.items[0].prevdoc_docname, frappe.opportunity_name,
- 'opportunity set in quotation');
- },
- () => done()
- ]);
-});
diff --git a/erpnext/crm/doctype/opportunity/test_opportunity.py b/erpnext/crm/doctype/opportunity/test_opportunity.py
index 6e6fed5..db44b6a 100644
--- a/erpnext/crm/doctype/opportunity/test_opportunity.py
+++ b/erpnext/crm/doctype/opportunity/test_opportunity.py
@@ -4,10 +4,12 @@
import unittest
import frappe
-from frappe.utils import random_string, today
+from frappe.utils import now_datetime, random_string, today
from erpnext.crm.doctype.lead.lead import make_customer
+from erpnext.crm.doctype.lead.test_lead import make_lead
from erpnext.crm.doctype.opportunity.opportunity import make_quotation
+from erpnext.crm.utils import get_linked_communication_list
test_records = frappe.get_test_records('Opportunity')
@@ -28,21 +30,11 @@
self.assertEqual(doc.status, "Quotation")
def test_make_new_lead_if_required(self):
- new_lead_email_id = "new{}@example.com".format(random_string(5))
- args = {
- "doctype": "Opportunity",
- "contact_email": new_lead_email_id,
- "opportunity_type": "Sales",
- "with_items": 0,
- "transaction_date": today()
- }
- # new lead should be created against the new.opportunity@example.com
- opp_doc = frappe.get_doc(args).insert(ignore_permissions=True)
+ opp_doc = make_opportunity_from_lead()
self.assertTrue(opp_doc.party_name)
self.assertEqual(opp_doc.opportunity_from, "Lead")
- self.assertEqual(frappe.db.get_value("Lead", opp_doc.party_name, "email_id"),
- new_lead_email_id)
+ self.assertEqual(frappe.db.get_value("Lead", opp_doc.party_name, "email_id"), opp_doc.contact_email)
# create new customer and create new contact against 'new.opportunity@example.com'
customer = make_customer(opp_doc.party_name).insert(ignore_permissions=True)
@@ -54,18 +46,60 @@
"link_name": customer.name
}]
})
- contact.add_email(new_lead_email_id, is_primary=True)
+ contact.add_email(opp_doc.contact_email, is_primary=True)
contact.insert(ignore_permissions=True)
- opp_doc = frappe.get_doc(args).insert(ignore_permissions=True)
- self.assertTrue(opp_doc.party_name)
- self.assertEqual(opp_doc.opportunity_from, "Customer")
- self.assertEqual(opp_doc.party_name, customer.name)
-
def test_opportunity_item(self):
opportunity_doc = make_opportunity(with_items=1, rate=1100, qty=2)
self.assertEqual(opportunity_doc.total, 2200)
+ def test_carry_forward_of_email_and_comments(self):
+ frappe.db.set_value("CRM Settings", "CRM Settings", "carry_forward_communication_and_comments", 1)
+ lead_doc = make_lead()
+ lead_doc.add_comment('Comment', text='Test Comment 1')
+ lead_doc.add_comment('Comment', text='Test Comment 2')
+ create_communication(lead_doc.doctype, lead_doc.name, lead_doc.email_id)
+ create_communication(lead_doc.doctype, lead_doc.name, lead_doc.email_id)
+
+ opp_doc = make_opportunity(opportunity_from="Lead", lead=lead_doc.name)
+ opportunity_comment_count = frappe.db.count("Comment", {"reference_doctype": opp_doc.doctype, "reference_name": opp_doc.name})
+ opportunity_communication_count = len(get_linked_communication_list(opp_doc.doctype, opp_doc.name))
+ self.assertEqual(opportunity_comment_count, 2)
+ self.assertEqual(opportunity_communication_count, 2)
+
+ opp_doc.add_comment('Comment', text='Test Comment 3')
+ opp_doc.add_comment('Comment', text='Test Comment 4')
+ create_communication(opp_doc.doctype, opp_doc.name, opp_doc.contact_email)
+ create_communication(opp_doc.doctype, opp_doc.name, opp_doc.contact_email)
+
+ quotation_doc = make_quotation(opp_doc.name)
+ quotation_doc.append('items', {
+ "item_code": "_Test Item",
+ "qty": 1
+ })
+ quotation_doc.run_method("set_missing_values")
+ quotation_doc.run_method("calculate_taxes_and_totals")
+ quotation_doc.save()
+
+ quotation_comment_count = frappe.db.count("Comment", {"reference_doctype": quotation_doc.doctype, "reference_name": quotation_doc.name, "comment_type": "Comment"})
+ quotation_communication_count = len(get_linked_communication_list(quotation_doc.doctype, quotation_doc.name))
+ self.assertEqual(quotation_comment_count, 4)
+ self.assertEqual(quotation_communication_count, 4)
+
+def make_opportunity_from_lead():
+ new_lead_email_id = "new{}@example.com".format(random_string(5))
+ args = {
+ "doctype": "Opportunity",
+ "contact_email": new_lead_email_id,
+ "opportunity_type": "Sales",
+ "with_items": 0,
+ "transaction_date": today()
+ }
+ # new lead should be created against the new.opportunity@example.com
+ opp_doc = frappe.get_doc(args).insert(ignore_permissions=True)
+
+ return opp_doc
+
def make_opportunity(**args):
args = frappe._dict(args)
@@ -95,3 +129,20 @@
opp_doc.insert()
return opp_doc
+
+def create_communication(reference_doctype, reference_name, sender, sent_or_received=None, creation=None):
+ communication = frappe.get_doc({
+ "doctype": "Communication",
+ "communication_type": "Communication",
+ "communication_medium": "Email",
+ "sent_or_received": sent_or_received or "Sent",
+ "email_status": "Open",
+ "subject": "Test Subject",
+ "sender": sender,
+ "content": "Test",
+ "status": "Linked",
+ "reference_doctype": reference_doctype,
+ "creation": creation or now_datetime(),
+ "reference_name": reference_name
+ })
+ communication.save()
\ No newline at end of file
diff --git a/erpnext/crm/doctype/prospect/prospect.py b/erpnext/crm/doctype/prospect/prospect.py
index 367aa3d..cc4c1d3 100644
--- a/erpnext/crm/doctype/prospect/prospect.py
+++ b/erpnext/crm/doctype/prospect/prospect.py
@@ -6,6 +6,8 @@
from frappe.model.document import Document
from frappe.model.mapper import get_mapped_doc
+from erpnext.crm.utils import add_link_in_communication, copy_comments
+
class Prospect(Document):
def onload(self):
@@ -20,6 +22,12 @@
def on_trash(self):
self.unlink_dynamic_links()
+ def after_insert(self):
+ if frappe.db.get_single_value("CRM Settings", "carry_forward_communication_and_comments"):
+ for row in self.get('prospect_lead'):
+ copy_comments("Lead", row.lead, self)
+ add_link_in_communication("Lead", row.lead, self)
+
def update_lead_details(self):
for row in self.get('prospect_lead'):
lead = frappe.get_value('Lead', row.lead, ['lead_name', 'status', 'email_id', 'mobile_no'], as_dict=True)
diff --git a/erpnext/crm/utils.py b/erpnext/crm/utils.py
index 95b19ec..a4576a2 100644
--- a/erpnext/crm/utils.py
+++ b/erpnext/crm/utils.py
@@ -21,3 +21,30 @@
lead = frappe.get_doc("Lead", contact_lead)
lead.db_set("phone", phone)
lead.db_set("mobile_no", mobile_no)
+
+def copy_comments(doctype, docname, doc):
+ comments = frappe.db.get_values("Comment", filters={"reference_doctype": doctype, "reference_name": docname, "comment_type": "Comment"}, fieldname="*")
+ for comment in comments:
+ comment = frappe.get_doc(comment.update({"doctype":"Comment"}))
+ comment.name = None
+ comment.reference_doctype = doc.doctype
+ comment.reference_name = doc.name
+ comment.insert()
+
+def add_link_in_communication(doctype, docname, doc):
+ communication_list = get_linked_communication_list(doctype, docname)
+
+ for communication in communication_list:
+ communication_doc = frappe.get_doc("Communication", communication)
+ communication_doc.add_link(doc.doctype, doc.name, autosave=True)
+
+def get_linked_communication_list(doctype, docname):
+ communications = frappe.get_all("Communication", filters={"reference_doctype": doctype, "reference_name": docname}, pluck='name')
+ communication_links = frappe.get_all('Communication Link',
+ {
+ "link_doctype": doctype,
+ "link_name": docname,
+ "parent": ("not in", communications)
+ }, pluck="parent")
+
+ return communications + communication_links
diff --git a/erpnext/demo/domains.py b/erpnext/demo/domains.py
index 5fa181d..346787e 100644
--- a/erpnext/demo/domains.py
+++ b/erpnext/demo/domains.py
@@ -14,9 +14,6 @@
'Education': {
'company_name': 'Whitmore College'
},
- 'Agriculture': {
- 'company_name': 'Schrute Farms'
- },
'Non Profit': {
'company_name': 'Erpnext Foundation'
}
diff --git a/erpnext/domains/agriculture.py b/erpnext/domains/agriculture.py
deleted file mode 100644
index e5414a9..0000000
--- a/erpnext/domains/agriculture.py
+++ /dev/null
@@ -1,26 +0,0 @@
-data = {
- 'desktop_icons': [
- 'Agriculture Task',
- 'Crop',
- 'Crop Cycle',
- 'Fertilizer',
- 'Item',
- 'Location',
- 'Disease',
- 'Plant Analysis',
- 'Soil Analysis',
- 'Soil Texture',
- 'Task',
- 'Water Analysis',
- 'Weather'
- ],
- 'restricted_roles': [
- 'Agriculture Manager',
- 'Agriculture User'
- ],
- 'modules': [
- 'Agriculture'
- ],
- 'default_portal_role': 'System Manager',
- 'on_setup': 'erpnext.agriculture.setup.setup_agriculture'
-}
diff --git a/erpnext/education/api.py b/erpnext/education/api.py
index 53d1482..d9013b0 100644
--- a/erpnext/education/api.py
+++ b/erpnext/education/api.py
@@ -125,7 +125,7 @@
:param student: Student.
"""
- guardians = frappe.get_list("Student Guardian", fields=["guardian"] ,
+ guardians = frappe.get_all("Student Guardian", fields=["guardian"] ,
filters={"parent": student})
return guardians
@@ -137,10 +137,10 @@
:param student_group: Student Group.
"""
if include_inactive:
- students = frappe.get_list("Student Group Student", fields=["student", "student_name"] ,
+ students = frappe.get_all("Student Group Student", fields=["student", "student_name"] ,
filters={"parent": student_group}, order_by= "group_roll_number")
else:
- students = frappe.get_list("Student Group Student", fields=["student", "student_name"] ,
+ students = frappe.get_all("Student Group Student", fields=["student", "student_name"] ,
filters={"parent": student_group, "active": 1}, order_by= "group_roll_number")
return students
@@ -164,7 +164,7 @@
:param fee_structure: Fee Structure.
"""
if fee_structure:
- fs = frappe.get_list("Fee Component", fields=["fees_category", "description", "amount"] , filters={"parent": fee_structure}, order_by= "idx")
+ fs = frappe.get_all("Fee Component", fields=["fees_category", "description", "amount"] , filters={"parent": fee_structure}, order_by= "idx")
return fs
@@ -175,7 +175,7 @@
:param program: Program.
:param student_category: Student Category
"""
- fs = frappe.get_list("Program Fee", fields=["academic_term", "fee_structure", "due_date", "amount"] ,
+ fs = frappe.get_all("Program Fee", fields=["academic_term", "fee_structure", "due_date", "amount"] ,
filters={"parent": program, "student_category": student_category }, order_by= "idx")
return fs
@@ -220,7 +220,7 @@
:param Course: Course
"""
- return frappe.get_list("Course Assessment Criteria", \
+ return frappe.get_all("Course Assessment Criteria",
fields=["assessment_criteria", "weightage"], filters={"parent": course}, order_by= "idx")
@@ -253,7 +253,7 @@
:param Assessment Plan: Assessment Plan
"""
- return frappe.get_list("Assessment Plan Criteria", \
+ return frappe.get_all("Assessment Plan Criteria",
fields=["assessment_criteria", "maximum_score", "docstatus"], filters={"parent": assessment_plan}, order_by= "idx")
diff --git a/erpnext/education/doctype/academic_term/test_academic_term.js b/erpnext/education/doctype/academic_term/test_academic_term.js
deleted file mode 100644
index 383b65a..0000000
--- a/erpnext/education/doctype/academic_term/test_academic_term.js
+++ /dev/null
@@ -1,24 +0,0 @@
-// Testing Setup Module in Education
-QUnit.module('education');
-
-QUnit.test('Test: Academic Term', function(assert){
- assert.expect(4);
- let done = assert.async();
- frappe.run_serially([
- () => {
- return frappe.tests.make('Academic Term', [
- {academic_year: '2016-17'},
- {term_name: "Semester 1"},
- {term_start_date: '2016-07-20'},
- {term_end_date:'2017-06-20'},
- ]);
- },
- () => {
- assert.ok(cur_frm.doc.academic_year=='2016-17');
- assert.ok(cur_frm.doc.term_name=='Semester 1');
- assert.ok(cur_frm.doc.term_start_date=='2016-07-20');
- assert.ok(cur_frm.doc.term_end_date=='2017-06-20');
- },
- () => done()
- ]);
-});
diff --git a/erpnext/education/doctype/assessment_criteria/test_assessment_criteria.js b/erpnext/education/doctype/assessment_criteria/test_assessment_criteria.js
deleted file mode 100644
index 724c4da..0000000
--- a/erpnext/education/doctype/assessment_criteria/test_assessment_criteria.js
+++ /dev/null
@@ -1,16 +0,0 @@
-// Education Assessment module
-QUnit.module('education');
-
-QUnit.test('Test: Assessment Criteria', function(assert){
- assert.expect(0);
- let done = assert.async();
- frappe.run_serially([
- () => {
- return frappe.tests.make('Assessment Criteria', [
- {assessment_criteria: 'Pass'},
- {assessment_criteria_group: 'Reservation'}
- ]);
- },
- () => done()
- ]);
-});
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
deleted file mode 100644
index ab27e63..0000000
--- a/erpnext/education/doctype/assessment_criteria_group/test_assessment_criteria_group.js
+++ /dev/null
@@ -1,15 +0,0 @@
-// Education Assessment module
-QUnit.module('education');
-
-QUnit.test('Test: Assessment Criteria Group', function(assert){
- assert.expect(0);
- let done = assert.async();
- frappe.run_serially([
- () => {
- return frappe.tests.make('Assessment Criteria Group', [
- {assessment_criteria_group: 'Reservation'}
- ]);
- },
- () => done()
- ]);
-});
diff --git a/erpnext/education/doctype/assessment_group/test_assessment_group.js b/erpnext/education/doctype/assessment_group/test_assessment_group.js
deleted file mode 100644
index 00e6309..0000000
--- a/erpnext/education/doctype/assessment_group/test_assessment_group.js
+++ /dev/null
@@ -1,65 +0,0 @@
-// Education Assessment module
-QUnit.module('education');
-
-QUnit.test('Test: Assessment Group', function(assert){
- assert.expect(4);
- let done = assert.async();
-
- frappe.run_serially([
- () => frappe.set_route('Tree', 'Assessment Group'),
-
- // Checking adding child without selecting any Node
- () => frappe.tests.click_button('New'),
- () => frappe.timeout(0.2),
- () => {assert.equal($(`.msgprint`).text(), "Select a group node first.", "Error message success");},
- () => frappe.tests.click_button('Close'),
- () => frappe.timeout(0.2),
-
- // Creating child nodes
- () => frappe.tests.click_link('All Assessment Groups'),
- () => frappe.map_group.make('Assessment-group-1'),
- () => frappe.map_group.make('Assessment-group-4', "All Assessment Groups", 1),
- () => frappe.tests.click_link('Assessment-group-4'),
- () => frappe.map_group.make('Assessment-group-5', "Assessment-group-3", 0),
-
- // Checking Edit button
- () => frappe.timeout(0.5),
- () => frappe.tests.click_link('Assessment-group-1'),
- () => frappe.tests.click_button('Edit'),
- () => frappe.timeout(0.5),
- () => {assert.deepEqual(frappe.get_route(), ["Form", "Assessment Group", "Assessment-group-1"], "Edit route checks");},
-
- // Deleting child Node
- () => frappe.set_route('Tree', 'Assessment Group'),
- () => frappe.timeout(0.5),
- () => frappe.tests.click_link('Assessment-group-1'),
- () => frappe.tests.click_button('Delete'),
- () => frappe.timeout(0.5),
- () => frappe.tests.click_button('Yes'),
-
- // Checking Collapse and Expand button
- () => frappe.timeout(2),
- () => frappe.tests.click_link('Assessment-group-4'),
- () => frappe.click_button('Collapse'),
- () => frappe.tests.click_link('All Assessment Groups'),
- () => frappe.click_button('Collapse'),
- () => {assert.ok($('.opened').size() == 0, 'Collapsed');},
- () => frappe.click_button('Expand'),
- () => {assert.ok($('.opened').size() > 0, 'Expanded');},
-
- () => done()
- ]);
-});
-
-frappe.map_group = {
- make:function(assessment_group_name, parent_assessment_group = 'All Assessment Groups', is_group = 0){
- return frappe.run_serially([
- () => frappe.click_button('Add Child'),
- () => frappe.timeout(0.2),
- () => cur_dialog.set_value('is_group', is_group),
- () => cur_dialog.set_value('assessment_group_name', assessment_group_name),
- () => cur_dialog.set_value('parent_assessment_group', parent_assessment_group),
- () => frappe.click_button('Create New'),
- ]);
- }
-};
diff --git a/erpnext/education/doctype/assessment_plan/test_assessment_plan.js b/erpnext/education/doctype/assessment_plan/test_assessment_plan.js
deleted file mode 100644
index b0bff26..0000000
--- a/erpnext/education/doctype/assessment_plan/test_assessment_plan.js
+++ /dev/null
@@ -1,54 +0,0 @@
-// Testing Assessment Module in education
-QUnit.module('education');
-
-QUnit.test('Test: Assessment Plan', function(assert){
- assert.expect(6);
- let done = assert.async();
- let room_name, instructor_name, assessment_name;
-
- frappe.run_serially([
- () => frappe.db.get_value('Room', {'room_name': 'Room 1'}, 'name'),
- (room) => {room_name = room.message.name;}, // Fetching Room name
- () => frappe.db.get_value('Instructor', {'instructor_name': 'Instructor 1'}, 'name'),
- (instructor) => {instructor_name = instructor.message.name;}, // Fetching Instructor name
-
- () => {
- return frappe.tests.make('Assessment Plan', [
- {assessment_name: "Test-Mid-Term"},
- {assessment_group: 'Assessment-group-5'},
- {maximum_assessment_score: 100},
- {student_group: 'test-course-wise-group-2'},
- {course: 'Test_Sub'},
- {grading_scale: 'GTU'},
- {schedule_date: frappe.datetime.nowdate()},
- {room: room_name},
- {examiner: instructor_name},
- {supervisor: instructor_name},
- {from_time: "12:30:00"},
- {to_time: "2:30:00"}
- ]);
- },
-
- () => {
- assessment_name = cur_frm.doc.name; // Storing the name of current Assessment Plan
- assert.equal(cur_frm.doc.assessment_criteria[0].assessment_criteria, 'Pass', 'Assessment Criteria auto-filled correctly');
- assert.equal(cur_frm.doc.assessment_criteria[0].maximum_score, 100, 'Maximum score correctly set');
- }, // Checking if the table was auto-filled upon selecting appropriate fields
-
- () => frappe.timeout(1),
- () => frappe.tests.click_button('Submit'),
- () => frappe.timeout(0.5),
- () => frappe.tests.click_button('Yes'),
- () => frappe.timeout(0.5),
- () => {assert.equal(cur_frm.doc.docstatus, 1, "Assessment Plan submitted successfully");},
-
- () => frappe.click_button('Assessment Result'), // Checking out Assessment Result button option
- () => frappe.timeout(0.5),
- () => {
- assert.deepEqual(frappe.get_route(), ["Form", "Assessment Result Tool"], 'Assessment Result properly linked');
- assert.equal(cur_frm.doc.assessment_plan, assessment_name, 'Assessment correctly set');
- assert.equal(cur_frm.doc.student_group, 'test-course-wise-group-2', 'Course for Assessment correctly set');
- },
- () => done()
- ]);
-});
diff --git a/erpnext/education/doctype/assessment_result/test_assessment_result.js b/erpnext/education/doctype/assessment_result/test_assessment_result.js
deleted file mode 100644
index d4eb4b8..0000000
--- a/erpnext/education/doctype/assessment_result/test_assessment_result.js
+++ /dev/null
@@ -1,73 +0,0 @@
-// Education Assessment module
-QUnit.module('education');
-
-QUnit.test('Test: Assessment Result', function(assert){
- assert.expect(25);
- let done = assert.async();
- let student_list = [];
- let assessment_name;
- let tasks = []
-
- frappe.run_serially([
- // Saving Assessment Plan name
- () => frappe.db.get_value('Assessment Plan', {'assessment_name': 'Test-Mid-Term'}, 'name'),
- (assessment_plan) => {assessment_name = assessment_plan.message.name;},
- // Fetching list of Student for which Result is supposed to be set
- () => frappe.set_route('Form', 'Assessment Plan', assessment_name),
- () => frappe.timeout(1),
- () => frappe.tests.click_button('Assessment Result'),
- () => frappe.timeout(1),
- () => cur_frm.refresh(),
- () => frappe.timeout(1),
- () => {
- $("tbody tr").each( function(i, input){
- student_list.push($(input).data().student);
- });
- },
-
- // Looping through each student in the list and setting up their score
- () => {
- student_list.forEach(index => {
- tasks.push(
- () => frappe.set_route('List', 'Assessment Result', 'List'),
- () => frappe.timeout(0.5),
- () => frappe.tests.click_button('New'),
- () => frappe.timeout(0.5),
- () => cur_frm.set_value('student', index),
- () => cur_frm.set_value('assessment_plan', assessment_name),
- () => frappe.timeout(0.2),
- () => cur_frm.doc.details[0].score = (39 + (15 * student_list.indexOf(index))),
- () => cur_frm.save(),
- () => frappe.timeout(0.5),
-
- () => frappe.db.get_value('Assessment Plan', {'name': 'ASP00001'}, ['grading_scale', 'maximum_assessment_score']),
- (assessment_plan) => {
- assert.equal(cur_frm.doc.grading_scale, assessment_plan.message.grading_scale, 'Grading scale correctly fetched');
- assert.equal(cur_frm.doc.maximum_score, assessment_plan.message.maximum_assessment_score, 'Maximum score correctly fetched');
-
- frappe.call({
- method: "erpnext.education.api.get_grade",
- args: {
- "grading_scale": assessment_plan.message.grading_scale,
- "percentage": cur_frm.doc.total_score
- },
- callback: function(r){
- assert.equal(cur_frm.doc.grade, r.message, "Grade correctly calculated");
- }
- });
- },
-
- () => frappe.tests.click_button('Submit'),
- () => frappe.timeout(0.5),
- () => frappe.tests.click_button('Yes'),
- () => frappe.timeout(0.5),
- () => {assert.equal();},
- () => {assert.equal(cur_frm.doc.docstatus, 1, "Submitted successfully");},
- );
- });
- return frappe.run_serially(tasks);
- },
-
- () => done()
- ]);
-});
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
deleted file mode 100644
index 7ef5c68..0000000
--- a/erpnext/education/doctype/assessment_result_tool/test_assessment_result_tool.js
+++ /dev/null
@@ -1,29 +0,0 @@
-// Education Assessment module
-QUnit.module('education');
-
-QUnit.test('Test: Assessment Result Tool', function(assert){
- assert.expect(1);
- let done = assert.async();
- let i, count = 0, assessment_name;
-
- frappe.run_serially([
- // Saving Assessment Plan name
- () => frappe.db.get_value('Assessment Plan', {'assessment_name': 'Test-Mid-Term'}, 'name'),
- (assessment_plan) => {assessment_name = assessment_plan.message.name;},
-
- () => frappe.set_route('Form', 'Assessment Plan', assessment_name),
- () => frappe.timeout(1),
- () => frappe.tests.click_button('Assessment Result'),
- () => frappe.timeout(1),
- () => cur_frm.refresh(),
- () => frappe.timeout(1),
- () => {
- for(i = 2; i < $('tbody tr').size() * 4; i = (i + 4)){
- if(($(`tbody td:eq("${i}")`) != "") && ($(`tbody td:eq("${i+1}")`) != ""))
- count++;
- }
- assert.equal($('tbody tr').size(), count, 'All grades correctly displayed');
- },
- () => done()
- ]);
-});
diff --git a/erpnext/education/doctype/course/test_course.js b/erpnext/education/doctype/course/test_course.js
deleted file mode 100644
index 2b6860c..0000000
--- a/erpnext/education/doctype/course/test_course.js
+++ /dev/null
@@ -1,36 +0,0 @@
-// Testing Setup Module in education
-QUnit.module('education');
-
-QUnit.test('test course', function(assert) {
- assert.expect(8);
- let done = assert.async();
- frappe.run_serially([
- () => {
- return frappe.tests.make('Course', [
- {course_name: 'Test_Subject'},
- {course_code: 'Test_Sub'},
- {department: 'Test Department'},
- {course_abbreviation: 'Test_Sub'},
- {course_intro: 'Test Subject Intro'},
- {default_grading_scale: 'GTU'},
- {assessment_criteria: [
- [
- {assessment_criteria: 'Pass'},
- {weightage: 100}
- ]
- ]}
- ]);
- },
- () => {
- assert.ok(cur_frm.doc.course_name == 'Test_Subject', 'Course name correctly set');
- assert.ok(cur_frm.doc.course_code == 'Test_Sub', 'Course code correctly set');
- assert.ok(cur_frm.doc.department == 'Test Department', 'Department selected correctly');
- assert.ok(cur_frm.doc.course_abbreviation == 'Test_Sub');
- assert.ok(cur_frm.doc.course_intro == 'Test Subject Intro');
- assert.ok(cur_frm.doc.default_grading_scale == 'GTU', 'Grading scale selected correctly');
- assert.ok(cur_frm.doc.assessment_criteria[0].assessment_criteria == 'Pass', 'Assessment criteria selected correctly');
- assert.ok(cur_frm.doc.assessment_criteria[0].weightage == '100');
- },
- () => done()
- ]);
-});
diff --git a/erpnext/education/doctype/course_schedule/course_schedule.json b/erpnext/education/doctype/course_schedule/course_schedule.json
index 8c6746b..38d9b50 100644
--- a/erpnext/education/doctype/course_schedule/course_schedule.json
+++ b/erpnext/education/doctype/course_schedule/course_schedule.json
@@ -1,520 +1,143 @@
{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
+ "actions": [],
"allow_import": 1,
- "allow_rename": 0,
"autoname": "naming_series:",
- "beta": 0,
"creation": "2015-09-09 16:34:04.960369",
- "custom": 0,
- "docstatus": 0,
"doctype": "DocType",
"document_type": "Document",
- "editable_grid": 0,
"engine": "InnoDB",
+ "field_order": [
+ "student_group",
+ "instructor",
+ "instructor_name",
+ "column_break_2",
+ "naming_series",
+ "course",
+ "color",
+ "section_break_6",
+ "schedule_date",
+ "room",
+ "column_break_9",
+ "from_time",
+ "to_time",
+ "title"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "student_group",
"fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
"in_global_search": 1,
- "in_list_view": 0,
"in_standard_filter": 1,
"label": "Student Group",
- "length": 0,
- "no_copy": 0,
"options": "Student 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": 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,
"fieldname": "instructor",
"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": 1,
"label": "Instructor",
- "length": 0,
- "no_copy": 0,
"options": "Instructor",
- "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_from": "instructor.Instructor_name",
+ "fetch_from": "instructor.instructor_name",
"fieldname": "instructor_name",
"fieldtype": "Read Only",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
"in_global_search": 1,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Instructor Name",
- "length": 0,
- "no_copy": 0,
- "options": "",
- "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,
- "translatable": 0,
- "unique": 0
+ "read_only": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "column_break_2",
- "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,
- "default": "",
"fieldname": "naming_series",
"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": "Naming Series",
- "length": 0,
- "no_copy": 0,
"options": "EDU-CSH-.YYYY.-",
- "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": 1,
- "translatable": 0,
- "unique": 0
+ "set_only_once": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "course",
"fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
"in_global_search": 1,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Course",
- "length": 0,
- "no_copy": 0,
"options": "Course",
- "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,
"fieldname": "color",
"fieldtype": "Color",
- "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": "Color",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 1,
- "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
+ "print_hide": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "section_break_6",
- "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,
- "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": "Today",
"fieldname": "schedule_date",
"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": "Schedule Date",
- "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": "Schedule Date"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "room",
"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": "Room",
- "length": 0,
- "no_copy": 0,
"options": "Room",
- "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,
"fieldname": "column_break_9",
- "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,
"fieldname": "from_time",
"fieldtype": "Time",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
"in_list_view": 1,
- "in_standard_filter": 0,
"label": "From Time",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "reqd": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "to_time",
"fieldtype": "Time",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
"in_list_view": 1,
- "in_standard_filter": 0,
"label": "To Time",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "reqd": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "title",
"fieldtype": "Data",
"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": "Title",
- "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": "Title"
}
],
- "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,
- "menu_index": 0,
- "modified": "2018-08-21 14:44:51.827225",
+ "links": [],
+ "modified": "2021-11-24 11:57:08.164449",
"modified_by": "Administrator",
"module": "Education",
"name": "Course Schedule",
- "name_case": "",
+ "naming_rule": "By \"Naming Series\" field",
"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": "Academics User",
- "set_user_permissions": 0,
"share": 1,
- "submit": 0,
"write": 1
}
],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
"restrict_to_domain": "Education",
- "show_name_in_global_search": 0,
"sort_field": "schedule_date",
"sort_order": "DESC",
- "title_field": "title",
- "track_changes": 0,
- "track_seen": 0,
- "track_views": 0
+ "title_field": "title"
}
\ No newline at end of file
diff --git a/erpnext/education/doctype/course_scheduling_tool/course_scheduling_tool.json b/erpnext/education/doctype/course_scheduling_tool/course_scheduling_tool.json
index 2926fe8..13dfe38 100644
--- a/erpnext/education/doctype/course_scheduling_tool/course_scheduling_tool.json
+++ b/erpnext/education/doctype/course_scheduling_tool/course_scheduling_tool.json
@@ -1,661 +1,168 @@
{
- "allow_copy": 1,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2015-09-23 15:37:38.108475",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "Setup",
- "editable_grid": 0,
- "engine": "InnoDB",
+ "actions": [],
+ "allow_copy": 1,
+ "creation": "2015-09-23 15:37:38.108475",
+ "doctype": "DocType",
+ "document_type": "Setup",
+ "engine": "InnoDB",
+ "field_order": [
+ "student_group",
+ "course",
+ "program",
+ "column_break_3",
+ "academic_year",
+ "academic_term",
+ "section_break_6",
+ "instructor",
+ "instructor_name",
+ "column_break_9",
+ "room",
+ "section_break_7",
+ "course_start_date",
+ "course_end_date",
+ "day",
+ "reschedule",
+ "column_break_15",
+ "from_time",
+ "to_time"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "student_group",
- "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 Group",
- "length": 0,
- "no_copy": 0,
- "options": "Student 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": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "student_group",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Student Group",
+ "options": "Student Group",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "course",
- "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": "Course",
- "length": 0,
- "no_copy": 0,
- "options": "Course",
- "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": "course",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Course",
+ "options": "Course",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "program",
- "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": "Program",
- "length": 0,
- "no_copy": 0,
- "options": "Program",
- "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,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "program",
+ "fieldtype": "Link",
+ "label": "Program",
+ "options": "Program",
+ "read_only": 1
+ },
{
- "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,
- "translatable": 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": "academic_year",
- "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": "Academic Year",
- "length": 0,
- "no_copy": 0,
- "options": "Academic Year",
- "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,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "academic_year",
+ "fieldtype": "Link",
+ "label": "Academic Year",
+ "options": "Academic Year",
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "academic_term",
- "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": "Academic Term",
- "length": 0,
- "no_copy": 0,
- "options": "Academic Term",
- "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,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "academic_term",
+ "fieldtype": "Link",
+ "label": "Academic Term",
+ "options": "Academic Term",
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "section_break_6",
- "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,
- "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": "section_break_6",
+ "fieldtype": "Section Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "instructor",
- "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": "Instructor",
- "length": 0,
- "no_copy": 0,
- "options": "Instructor",
- "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": "instructor",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Instructor",
+ "options": "Instructor",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fetch_from": "instructor.instructor_name",
- "fieldname": "instructor_name",
- "fieldtype": "Read Only",
- "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": "Instructor Name",
- "length": 0,
- "no_copy": 0,
- "options": "",
- "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,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "instructor_name",
+ "fieldtype": "Read Only",
+ "label": "Instructor Name",
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_9",
- "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_9",
+ "fieldtype": "Column Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "room",
- "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": "Room",
- "length": 0,
- "no_copy": 0,
- "options": "Room",
- "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": "room",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Room",
+ "options": "Room",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 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,
- "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": "section_break_7",
+ "fieldtype": "Section Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "",
- "fieldname": "from_time",
- "fieldtype": "Time",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "From Time",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "from_time",
+ "fieldtype": "Time",
+ "label": "From Time",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "",
- "fieldname": "course_start_date",
- "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": "Course Start Date",
- "length": 0,
- "no_copy": 0,
- "options": "",
- "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": "course_start_date",
+ "fieldtype": "Date",
+ "label": "Course Start Date",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "day",
- "fieldtype": "Select",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Day",
- "length": 0,
- "no_copy": 0,
- "options": "\nMonday\nTuesday\nWednesday\nThursday\nFriday\nSaturday\nSunday",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "day",
+ "fieldtype": "Select",
+ "label": "Day",
+ "options": "\nMonday\nTuesday\nWednesday\nThursday\nFriday\nSaturday\nSunday",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "reschedule",
- "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": "Reschedule",
- "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": "reschedule",
+ "fieldtype": "Check",
+ "label": "Reschedule"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_15",
- "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_15",
+ "fieldtype": "Column Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "to_time",
- "fieldtype": "Time",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "To TIme",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "to_time",
+ "fieldtype": "Time",
+ "label": "To TIme",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "",
- "fieldname": "course_end_date",
- "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": "Course End Date",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldname": "course_end_date",
+ "fieldtype": "Date",
+ "label": "Course End Date",
+ "reqd": 1
}
- ],
- "has_web_view": 0,
- "hide_heading": 1,
- "hide_toolbar": 1,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 1,
- "istable": 0,
- "max_attachments": 0,
- "menu_index": 0,
- "modified": "2018-05-16 22:43:29.363798",
- "modified_by": "Administrator",
- "module": "Education",
- "name": "Course Scheduling Tool",
- "name_case": "",
- "owner": "Administrator",
+ ],
+ "hide_toolbar": 1,
+ "issingle": 1,
+ "links": [],
+ "modified": "2021-11-11 09:33:18.874445",
+ "modified_by": "Administrator",
+ "module": "Education",
+ "name": "Course Scheduling Tool",
+ "owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "cancel": 0,
- "create": 1,
- "delete": 0,
- "email": 0,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 0,
- "read": 1,
- "report": 0,
- "role": "Academics User",
- "set_user_permissions": 0,
- "share": 0,
- "submit": 0,
+ "create": 1,
+ "read": 1,
+ "role": "Academics User",
"write": 1
}
- ],
- "quick_entry": 0,
- "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": 1,
- "track_seen": 0
+ ],
+ "restrict_to_domain": "Education",
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/education/doctype/education_settings/test_education_settings.js b/erpnext/education/doctype/education_settings/test_education_settings.js
deleted file mode 100644
index 990b0aa..0000000
--- a/erpnext/education/doctype/education_settings/test_education_settings.js
+++ /dev/null
@@ -1,31 +0,0 @@
-/* eslint-disable */
-// rename this file from _test_[name] to test_[name] to activate
-// and remove above this line
-
-// Testing Setup Module in Education
-QUnit.module('education');
-
-QUnit.test("test: Education Settings", function (assert) {
- let done = assert.async();
-
- assert.expect(3);
-
- frappe.run_serially([
- () => frappe.set_route("List", "Education Settings"),
- () => frappe.timeout(0.4),
- () => {
- return frappe.tests.set_form_values(cur_frm, [
- {current_academic_year: '2016-17'},
- {current_academic_term: '2016-17 (Semester 1)'},
- {attendance_freeze_date: '2016-07-20'}
- ]);
- },
- () => {
- cur_frm.save();
- assert.ok(cur_frm.doc.current_academic_year=="2016-17");
- assert.ok(cur_frm.doc.current_academic_term=="2016-17 (Semester 1)");
- assert.ok(cur_frm.doc.attendance_freeze_date=="2016-07-20");
- },
- () => done()
- ]);
-});
diff --git a/erpnext/education/doctype/fees/test_fees.js b/erpnext/education/doctype/fees/test_fees.js
deleted file mode 100644
index 22e987e..0000000
--- a/erpnext/education/doctype/fees/test_fees.js
+++ /dev/null
@@ -1,31 +0,0 @@
-/* eslint-disable */
-// rename this file from _test_[name] to test_[name] to activate
-// and remove above this line
-
-QUnit.test("test: Fees", function (assert) {
- let done = assert.async();
-
- // number of asserts
- assert.expect(1);
-
- frappe.run_serially('Fees', [
-
- // insert a new Fees
- () => {
- return frappe.tests.make('Fees', [
- {student: 'STUD00001'},
- {due_date: frappe.datetime.get_today()},
- {fee_structure: 'FS00001'}
- ]);
- },
- () => {
- assert.equal(cur_frm.doc.grand_total===cur_frm.doc.outstanding_amount);
- },
- () => frappe.timeout(0.3),
- () => cur_frm.save(),
- () => frappe.tests.click_button('Submit'),
- () => frappe.tests.click_button('Yes'),
- () => done()
- ]);
-
-});
diff --git a/erpnext/education/doctype/grading_scale/test_grading_scale.js b/erpnext/education/doctype/grading_scale/test_grading_scale.js
deleted file mode 100644
index fb56918..0000000
--- a/erpnext/education/doctype/grading_scale/test_grading_scale.js
+++ /dev/null
@@ -1,102 +0,0 @@
-// Education Assessment module
-QUnit.module('education');
-
-QUnit.test('Test: Grading Scale', function(assert){
- assert.expect(3);
- let done = assert.async();
- frappe.run_serially([
- () => {
- return frappe.tests.make('Grading Scale', [
- {grading_scale_name: 'GTU'},
- {description: 'The score will be set according to 100 based system.'},
- {intervals: [
- [
- {grade_code: 'AA'},
- {threshold: '95'},
- {grade_description: 'First Class + Distinction'}
- ],
- [
- {grade_code: 'AB'},
- {threshold: '90'},
- {grade_description: 'First Class'}
- ],
- [
- {grade_code: 'BB'},
- {threshold: '80'},
- {grade_description: 'Distinction'}
- ],
- [
- {grade_code: 'BC'},
- {threshold: '70'},
- {grade_description: 'Second Class'}
- ],
- [
- {grade_code: 'CC'},
- {threshold: '60'},
- {grade_description: 'Third Class'}
- ],
- [
- {grade_code: 'CD'},
- {threshold: '50'},
- {grade_description: 'Average'}
- ],
- [
- {grade_code: 'DD'},
- {threshold: '40'},
- {grade_description: 'Pass'}
- ],
- [
- {grade_code: 'FF'},
- {threshold: '0'},
- {grade_description: 'Fail'}
- ],
- ]}
- ]);
- },
- () => {
- return frappe.tests.make('Grading Scale', [
- {grading_scale_name: 'GTU-2'},
- {description: 'The score will be set according to 100 based system.'},
- {intervals: [
- [
- {grade_code: 'AA'},
- {threshold: '90'},
- {grade_description: 'Distinction'}
- ],
- [
- {grade_code: 'FF'},
- {threshold: '0'},
- {grade_description: 'Fail'}
- ]
- ]}
- ]);
- },
-
- () => {
- let grading_scale = ['GTU', 'GTU-2'];
- let tasks = [];
- grading_scale.forEach(index => {
- tasks.push(
- () => frappe.set_route('Form', 'Grading Scale', index),
- () => frappe.timeout(0.5),
- () => frappe.tests.click_button('Submit'),
- () => frappe.timeout(0.5),
- () => frappe.tests.click_button('Yes'),
- () => {assert.equal(cur_frm.doc.docstatus, 1, 'Submitted successfully');}
- );
- });
- return frappe.run_serially(tasks);
- },
-
- () => frappe.timeout(1),
- () => frappe.set_route('Form', 'Grading Scale','GTU-2'),
- () => frappe.timeout(0.5),
- () => frappe.tests.click_button('Cancel'),
- () => frappe.timeout(0.5),
- () => frappe.tests.click_button('Yes'),
- () => frappe.timeout(0.5),
- () => {assert.equal(cur_frm.doc.docstatus, 2, 'Cancelled successfully');},
-
- () => done()
- ]);
-});
diff --git a/erpnext/education/doctype/guardian/test_guardian.js b/erpnext/education/doctype/guardian/test_guardian.js
deleted file mode 100644
index 1ea6dc2..0000000
--- a/erpnext/education/doctype/guardian/test_guardian.js
+++ /dev/null
@@ -1,34 +0,0 @@
-// Testing Student Module in education
-QUnit.module('education');
-
-QUnit.test('Test: Guardian', function(assert){
- assert.expect(9);
- let done = assert.async();
- frappe.run_serially([
- () => {
- return frappe.tests.make('Guardian', [
- {guardian_name: 'Test Guardian'},
- {email_address: 'guardian@testmail.com'},
- {mobile_number: 9898980000},
- {alternate_number: 8989890000},
- {date_of_birth: '1982-07-22'},
- {education: 'Testing'},
- {occupation: 'Testing'},
- {designation: 'Testing'},
- {work_address: 'Testing address'}
- ]);
- },
- () => {
- assert.ok(cur_frm.doc.guardian_name == 'Test Guardian');
- assert.ok(cur_frm.doc.email_address == 'guardian@testmail.com');
- assert.ok(cur_frm.doc.mobile_number == 9898980000);
- assert.ok(cur_frm.doc.alternate_number == 8989890000);
- assert.ok(cur_frm.doc.date_of_birth == '1982-07-22');
- assert.ok(cur_frm.doc.education == 'Testing');
- assert.ok(cur_frm.doc.occupation == 'Testing');
- assert.ok(cur_frm.doc.designation == 'Testing');
- assert.ok(cur_frm.doc.work_address == 'Testing address');
- },
- () => done()
- ]);
-});
diff --git a/erpnext/education/doctype/instructor/test_instructor.js b/erpnext/education/doctype/instructor/test_instructor.js
deleted file mode 100644
index c584f45..0000000
--- a/erpnext/education/doctype/instructor/test_instructor.js
+++ /dev/null
@@ -1,20 +0,0 @@
-// Testing Setup Module in education
-QUnit.module('education');
-
-QUnit.test('Test: Instructor', function(assert){
- assert.expect(2);
- let done = assert.async();
- frappe.run_serially([
- () => {
- return frappe.tests.make("Instructor", [
- {instructor_name: 'Instructor 1'},
- {department: 'Test Department'}
- ]);
- },
- () => {
- assert.ok(cur_frm.doc.instructor_name == 'Instructor 1');
- assert.ok(cur_frm.doc.department = 'Test Department');
- },
- () => done()
- ]);
-});
diff --git a/erpnext/education/doctype/program/test_program.js b/erpnext/education/doctype/program/test_program.js
deleted file mode 100644
index b9ca41a..0000000
--- a/erpnext/education/doctype/program/test_program.js
+++ /dev/null
@@ -1,34 +0,0 @@
-// Testing Setup Module in education
-QUnit.module('education');
-
-QUnit.test('Test: Program', function(assert){
- assert.expect(6);
- let done = assert.async();
- let fee_structure_code;
- frappe.run_serially([
- () => {
- return frappe.tests.make('Program', [
- {program_name: 'Standard Test'},
- {program_code: 'Standard Test'},
- {department: 'Test Department'},
- {program_abbreviation: 'Standard Test'},
- {courses: [
- [
- {course: 'Test_Sub'},
- {required: true}
- ]
- ]}
- ]);
- },
-
- () => {
- assert.ok(cur_frm.doc.program_name == 'Standard Test');
- assert.ok(cur_frm.doc.program_code == 'Standard Test');
- assert.ok(cur_frm.doc.department == 'Test Department');
- assert.ok(cur_frm.doc.program_abbreviation == 'Standard Test');
- assert.ok(cur_frm.doc.courses[0].course == 'Test_Sub');
- assert.ok(cur_frm.doc.courses[0].required == true);
- },
- () => done()
- ]);
-});
diff --git a/erpnext/education/doctype/room/test_room.js b/erpnext/education/doctype/room/test_room.js
deleted file mode 100644
index fdcbe92..0000000
--- a/erpnext/education/doctype/room/test_room.js
+++ /dev/null
@@ -1,22 +0,0 @@
-// Testing Setup Module in Education
-QUnit.module('education');
-
-QUnit.test('Test: Room', function(assert){
- assert.expect(3);
- let done = assert.async();
- frappe.run_serially([
- () => {
- return frappe.tests.make('Room', [
- {room_name: 'Room 1'},
- {room_number: '1'},
- {seating_capacity: '60'},
- ]);
- },
- () => {
- assert.ok(cur_frm.doc.room_name == 'Room 1');
- assert.ok(cur_frm.doc.room_number = '1');
- assert.ok(cur_frm.doc.seating_capacity = '60');
- },
- () => done()
- ]);
-});
diff --git a/erpnext/education/doctype/student_admission/test_student_admission.js b/erpnext/education/doctype/student_admission/test_student_admission.js
deleted file mode 100644
index e01791a..0000000
--- a/erpnext/education/doctype/student_admission/test_student_admission.js
+++ /dev/null
@@ -1,40 +0,0 @@
-// Testing Admission Module in Education
-QUnit.module('education');
-
-QUnit.test('Test: Student Admission', function(assert) {
- assert.expect(10);
- let done = assert.async();
- frappe.run_serially([
- () => {
- return frappe.tests.make('Student Admission', [
- {academic_year: '2016-17'},
- {admission_start_date: '2016-04-20'},
- {admission_end_date: '2016-05-31'},
- {title: '2016-17 Admissions'},
- {enable_admission_application: 1},
- {introduction: 'Test intro'},
- {program_details: [
- [
- {'program': 'Standard Test'},
- {'application_fee': 1000},
- {'applicant_naming_series': 'AP'},
- ]
- ]}
- ]);
- },
- () => cur_frm.save(),
- () => {
- assert.ok(cur_frm.doc.academic_year == '2016-17');
- assert.ok(cur_frm.doc.admission_start_date == '2016-04-20');
- assert.ok(cur_frm.doc.admission_end_date == '2016-05-31');
- assert.ok(cur_frm.doc.title == '2016-17 Admissions');
- assert.ok(cur_frm.doc.enable_admission_application == 1);
- assert.ok(cur_frm.doc.introduction == 'Test intro');
- assert.ok(cur_frm.doc.program_details[0].program == 'Standard Test', 'Program correctly selected');
- assert.ok(cur_frm.doc.program_details[0].application_fee == 1000);
- assert.ok(cur_frm.doc.program_details[0].applicant_naming_series == 'AP');
- assert.ok(cur_frm.doc.route == 'admissions/2016-17-Admissions', "Route successfully set");
- },
- () => done()
- ]);
-});
diff --git a/erpnext/education/doctype/student_applicant/tests/test_student_applicant.js b/erpnext/education/doctype/student_applicant/tests/test_student_applicant.js
deleted file mode 100644
index fa67977..0000000
--- a/erpnext/education/doctype/student_applicant/tests/test_student_applicant.js
+++ /dev/null
@@ -1,95 +0,0 @@
-// Testing Admission module in Education
-QUnit.module('education');
-
-QUnit.test('Test: Student Applicant', function(assert){
- assert.expect(24);
- let done = assert.async();
- let guradian_auto_code;
- let guardian_name;
- frappe.run_serially([
- () => frappe.set_route('List', 'Guardian'),
- () => frappe.timeout(0.5),
- () => {$(`a:contains("Test Guardian"):visible`)[0].click();},
- () => frappe.timeout(1),
- () => {
- guardian_name = cur_frm.doc.guardian_name;
- guradian_auto_code = frappe.get_route()[2];
- },
- // Testing data entry for Student Applicant
- () => {
- return frappe.tests.make('Student Applicant',[
- {first_name: 'Fname'},
- {middle_name: 'Mname'},
- {last_name: 'Lname'},
- {program: 'Standard Test'},
- {student_admission: '2016-17 Admissions'},
- {academic_year: '2016-17'},
- {date_of_birth: '1995-07-20'},
- {student_email_id: 'test@testmail.com'},
- {gender: 'Male'},
- {student_mobile_number: '9898980000'},
- {blood_group: 'O+'},
- {address_line_1: 'Test appt, Test Society,'},
- {address_line_2: 'Test district, Test city.'},
- {city: 'Test'},
- {state: 'Test'},
- {pincode: '400086'}
- ]);
- },
- // Entry in Guardian child table
- () => $('a:contains("Guardian Details"):visible').click(),
- () => $('.btn:contains("Add Row"):visible').click(),
- () => {
- cur_frm.get_field("guardians").grid.grid_rows[0].doc.guardian = guradian_auto_code;
- cur_frm.get_field("guardians").grid.grid_rows[0].doc.relation = "Father";
- cur_frm.get_field("guardians").grid.grid_rows[0].doc.guardian_name = guardian_name;
- $('a:contains("Guardian Details"):visible').click();
- },
- // Entry in Sibling child table
- () => $('a:contains("Sibling Details"):visible').click(),
- () => $('.btn:contains("Add Row"):visible').click(),
- () => {
- cur_frm.get_field("siblings").grid.grid_rows[0].doc.full_name = "Test Name";
- cur_frm.get_field("siblings").grid.grid_rows[0].doc.gender = "Male";
- cur_frm.get_field("siblings").grid.grid_rows[0].doc.institution = "Test Institution";
- cur_frm.get_field("siblings").grid.grid_rows[0].doc.program = "Test Program";
- cur_frm.get_field("siblings").grid.grid_rows[0].doc.date_of_birth = "1995-07-20";
- $('span.hidden-xs.octicon.octicon-triangle-up').click();
- cur_frm.save();
- },
- () => {
- assert.ok(cur_frm.doc.first_name == 'Fname');
- assert.ok(cur_frm.doc.middle_name == 'Mname');
- assert.ok(cur_frm.doc.last_name == 'Lname');
- assert.ok(cur_frm.doc.program == 'Standard Test', 'Program selected correctly');
- assert.ok(cur_frm.doc.student_admission == '2016-17 Admissions', 'Student Admission entry correctly selected');
- assert.ok(cur_frm.doc.academic_year == '2016-17');
- assert.ok(cur_frm.doc.date_of_birth == '1995-07-20');
- assert.ok(cur_frm.doc.student_email_id == 'test@testmail.com');
- assert.ok(cur_frm.doc.gender == 'Male');
- assert.ok(cur_frm.doc.student_mobile_number == '9898980000');
- assert.ok(cur_frm.doc.blood_group == 'O+');
- assert.ok(cur_frm.doc.address_line_1 == 'Test appt, Test Society,');
- assert.ok(cur_frm.doc.address_line_2 == 'Test district, Test city.');
- assert.ok(cur_frm.doc.city == 'Test');
- assert.ok(cur_frm.doc.state == 'Test');
- assert.ok(cur_frm.doc.pincode == '400086');
- },
- () => frappe.timeout(1),
- () => $('a:contains("Guardian Details"):visible').click(),
- () => {
- assert.ok(cur_frm.get_field("guardians").grid.grid_rows[0].doc.guardian == guradian_auto_code, 'Guardian correctly selected from dropdown');
- assert.ok(cur_frm.get_field("guardians").grid.grid_rows[0].doc.relation == 'Father');
- assert.ok(cur_frm.get_field("guardians").grid.grid_rows[0].doc.guardian_name == guardian_name, 'Guardian name was correctly retrieved');
- },
- () => $('a:contains("Sibling Details"):visible').click(),
- () => {
- assert.ok(cur_frm.get_field("siblings").grid.grid_rows[0].doc.full_name == 'Test Name');
- assert.ok(cur_frm.get_field("siblings").grid.grid_rows[0].doc.gender == 'Male');
- assert.ok(cur_frm.get_field("siblings").grid.grid_rows[0].doc.institution == 'Test Institution');
- assert.ok(cur_frm.get_field("siblings").grid.grid_rows[0].doc.program == 'Test Program');
- assert.ok(cur_frm.get_field("siblings").grid.grid_rows[0].doc.date_of_birth == '1995-07-20');
- },
- () => done()
- ]);
-});
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
deleted file mode 100644
index 03101e4..0000000
--- a/erpnext/education/doctype/student_applicant/tests/test_student_applicant_dummy_data.js
+++ /dev/null
@@ -1,87 +0,0 @@
-QUnit.module('Admission');
-
-QUnit.test('Make Students', function(assert){
- assert.expect(0);
- let done = assert.async();
- let tasks = [];
- let loop = [1,2,3,4];
- let fname;
-
- frappe.run_serially([
- // Making School House to be used in this test and later
- () => frappe.set_route('Form', 'School House/New School House'),
- () => frappe.timeout(0.5),
- () => cur_frm.doc.house_name = 'Test_house',
- () => cur_frm.save(),
-
- // Making Student Applicant entries
- () => {
- loop.forEach(index => {
- tasks.push(() => {
- fname = "Fname" + index;
-
- return frappe.tests.make('Student Applicant', [
- {first_name: fname},
- {middle_name: "Mname"},
- {last_name: "Lname"},
- {program: "Standard Test"},
- {student_admission: "2016-17 Admissions"},
- {date_of_birth: '1995-08-20'},
- {student_email_id: ('test' + (index+3) + '@testmail.com')},
- {gender: 'Male'},
- {student_mobile_number: (9898980000 + index)},
- {blood_group: 'O+'},
- {address_line_1: 'Test appt, Test Society,'},
- {address_line_2: 'Test district, Test city.'},
- {city: 'Test'},
- {state: 'Test'},
- {pincode: '395007'}
- ]);
- });
- });
- return frappe.run_serially(tasks);
- },
-
- // Using Program Enrollment Tool to enroll all dummy student at once
- () => frappe.set_route('Form', 'Program Enrollment Tool'),
- () => {
- cur_frm.set_value("get_students_from", "Student Applicants");
- cur_frm.set_value("academic_year", "2016-17");
- cur_frm.set_value("program", "Standard Test");
- },
- () => frappe.tests.click_button("Get Students"),
- () => frappe.timeout(1),
- () => frappe.tests.click_button("Enroll Students"),
- () => frappe.timeout(1.5),
- () => frappe.tests.click_button("Close"),
-
- // Submitting required data for each enrolled Student
- () => {
- tasks = [];
- loop.forEach(index => {
- tasks.push(
- () => {fname = "Fname" + index + " Mname Lname";},
- () => frappe.set_route('List', 'Program Enrollment/List'),
- () => frappe.timeout(0.6),
- () => frappe.tests.click_link(fname),
- () => frappe.timeout(0.4),
- () => {
- cur_frm.set_value('program', 'Standard Test');
- cur_frm.set_value('student_category', 'Reservation');
- cur_frm.set_value('student_batch_name', 'A');
- cur_frm.set_value('academic_year', '2016-17');
- cur_frm.set_value('academic_term', '2016-17 (Semester 1)');
- cur_frm.set_value('school_house', 'Test_house');
- },
- () => cur_frm.save(),
- () => frappe.timeout(0.5),
- () => frappe.tests.click_button('Submit'),
- () => frappe.tests.click_button('Yes'),
- () => frappe.timeout(0.5)
- );
- });
- return frappe.run_serially(tasks);
- },
- () => done()
- ]);
-});
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
deleted file mode 100644
index daa36e7..0000000
--- a/erpnext/education/doctype/student_applicant/tests/test_student_applicant_options.js
+++ /dev/null
@@ -1,110 +0,0 @@
-// Testing Admission module in Education
-QUnit.module('education');
-
-QUnit.test('test student applicant', function(assert){
- assert.expect(11);
- let done = assert.async();
- let testing_status;
- frappe.run_serially([
- () => frappe.set_route('List', 'Student Applicant'),
- () => frappe.timeout(0.5),
- () => {$(`a:contains("Fname Mname Lname"):visible`)[0].click();},
-
- // Checking different options
- // 1. Moving forward with Submit
- () => frappe.timeout(0.5),
- () => frappe.tests.click_button('Submit'),
- () => frappe.tests.click_button('Yes'),
- () => frappe.timeout(0.5),
- () => {
- testing_status = $('span.indicator.orange').text();
- assert.ok(testing_status.indexOf('Submit this document to confirm') == -1); // checking if submit has been successfull
- },
-
- // 2. Cancelling the Submit request
- () => frappe.timeout(0.5),
- () => frappe.tests.click_button('Cancel'),
- () => frappe.tests.click_button('Yes'),
- () => frappe.timeout(0.5),
- () => {
- testing_status = $('h1.editable-title').text();
- assert.ok(testing_status.indexOf('Cancelled') != -1); // checking if cancel request has been successfull
- },
-
- // 3. Checking Amend option
- () => frappe.timeout(0.5),
- () => frappe.tests.click_button('Amend'),
- () => cur_frm.doc.student_email_id = "test2@testmail.com", // updating email id since same id again is not allowed
- () => cur_frm.save(),
- () => frappe.timeout(0.5),
- () => frappe.tests.click_button('Submit'),
- () => frappe.tests.click_button('Yes'), // Submitting again after amend
- () => {
- testing_status = $('span.indicator.orange').text();
- assert.ok(testing_status.indexOf('Submit this document to confirm') == -1); // checking if submit has been successfull after amend
- },
-
- // Checking different Application status option
- () => {
- testing_status = $('h1.editable-title').text();
- assert.ok(testing_status.indexOf('Applied') != -1); // checking if Applied has been successfull
- },
- () => cur_frm.set_value('application_status', "Rejected"), // Rejected Status
- () => frappe.tests.click_button('Update'),
- () => {
- testing_status = $('h1.editable-title').text();
- assert.ok(testing_status.indexOf('Rejected') != -1); // checking if Rejected has been successfull
- },
- () => cur_frm.set_value('application_status', "Admitted"), // Admitted Status
- () => frappe.tests.click_button('Update'),
- () => {
- testing_status = $('h1.editable-title').text();
- assert.ok(testing_status.indexOf('Admitted') != -1); // checking if Admitted has been successfull
- },
- () => cur_frm.set_value('application_status', "Approved"), // Approved Status
- () => frappe.tests.click_button('Update'),
- () => {
- testing_status = $('h1.editable-title').text();
- assert.ok(testing_status.indexOf('Approved') != -1); // checking if Approved has been successfull
- },
-
- // Clicking on Enroll button should add the applicant's entry in Student doctype, and take you to Program Enrollment page
- () => frappe.timeout(0.5),
- () => frappe.tests.click_button('Enroll'),
- () => frappe.timeout(0.5),
- () => {
- assert.ok(frappe.get_route()[0] == 'Form'); // Checking if the current page is Program Enrollment page or not
- assert.ok(frappe.get_route()[1] == 'Program Enrollment');
- },
-
- // Routing to Student List to check if the Applicant's entry has been made or not
- () => frappe.timeout(0.5),
- () => frappe.set_route('List', 'Student'),
- () => frappe.timeout(0.5),
- () => {$(`a:contains("Fname Mname Lname"):visible`)[0].click();},
- () => frappe.timeout(0.5),
- () => {assert.ok(($(`h1.editable-title`).text()).indexOf('Enabled') != -1, 'Student entry successfully created');}, // Checking if the Student entry has been enabled
- // Enrolling the Student into a Program
- () => {$('.form-documents .row:nth-child(1) .col-xs-6:nth-child(1) .octicon-plus').click();},
- () => frappe.timeout(1),
- () => cur_frm.set_value('program', 'Standard Test'),
- () => frappe.timeout(1),
- () => {
- cur_frm.set_value('student_category', 'Reservation');
- cur_frm.set_value('student_batch_name', 'A');
- cur_frm.set_value('academic_year', '2016-17');
- cur_frm.set_value('academic_term', '2016-17 (Semester 1)');
- cur_frm.set_value('school_house', 'Test_house');
- },
- () => cur_frm.save(),
-
- // Submitting Program Enrollment form for our Test Student
- () => frappe.timeout(1),
- () => frappe.tests.click_button('Submit'),
- () => frappe.tests.click_button('Yes'),
- () => {
- assert.ok(cur_frm.doc.docstatus == 1, "Program enrollment successfully submitted");
- },
- () => done()
- ]);
-});
diff --git a/erpnext/education/doctype/student_attendance/test_student_attendance.js b/erpnext/education/doctype/student_attendance/test_student_attendance.js
deleted file mode 100644
index 3d30b09..0000000
--- a/erpnext/education/doctype/student_attendance/test_student_attendance.js
+++ /dev/null
@@ -1,31 +0,0 @@
-// Testing Attendance Module in Education
-QUnit.module('education');
-
-QUnit.test('Test: Student Attendance', function(assert){
- assert.expect(2);
- let done = assert.async();
- let student_code;
-
- frappe.run_serially([
- () => frappe.db.get_value('Student', {'student_email_id': 'test2@testmail.com'}, 'name'),
- (student) => {student_code = student.message.name;}, // fetching student code from db
-
- () => {
- return frappe.tests.make('Student Attendance', [
- {student: student_code},
- {date: frappe.datetime.nowdate()},
- {student_group: "test-batch-wise-group-2"},
- {status: "Absent"}
- ]);
- },
-
- () => frappe.timeout(0.5),
- () => {assert.equal(cur_frm.doc.status, "Absent", "Attendance correctly saved");},
-
- () => frappe.timeout(0.5),
- () => cur_frm.set_value("status", "Present"),
- () => {assert.equal(cur_frm.doc.status, "Present", "Attendance correctly saved");},
-
- () => done()
- ]);
-});
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 4e3f98d..7deb6b1 100644
--- a/erpnext/education/doctype/student_attendance_tool/student_attendance_tool.py
+++ b/erpnext/education/doctype/student_attendance_tool/student_attendance_tool.py
@@ -14,24 +14,36 @@
student_list = []
student_attendance_list = []
- if based_on=="Course Schedule":
+ if based_on == "Course Schedule":
student_group = frappe.db.get_value("Course Schedule", course_schedule, "student_group")
if student_group:
- student_list = frappe.get_list("Student Group Student", fields=["student", "student_name", "group_roll_number"] , \
+ student_list = frappe.get_all("Student Group Student", fields=["student", "student_name", "group_roll_number"],
filters={"parent": student_group, "active": 1}, order_by= "group_roll_number")
if not student_list:
- student_list = frappe.get_list("Student Group Student", fields=["student", "student_name", "group_roll_number"] ,
+ student_list = frappe.get_all("Student Group Student", fields=["student", "student_name", "group_roll_number"],
filters={"parent": student_group, "active": 1}, order_by= "group_roll_number")
+ table = frappe.qb.DocType("Student Attendance")
+
if course_schedule:
- student_attendance_list= frappe.db.sql('''select student, status from `tabStudent Attendance` where \
- course_schedule= %s''', (course_schedule), as_dict=1)
+ student_attendance_list = (
+ frappe.qb.from_(table)
+ .select(table.student, table.status)
+ .where(
+ (table.course_schedule == course_schedule)
+ )
+ ).run(as_dict=True)
else:
- student_attendance_list= frappe.db.sql('''select student, status from `tabStudent Attendance` where \
- student_group= %s and date= %s and \
- (course_schedule is Null or course_schedule='')''',
- (student_group, date), as_dict=1)
+ student_attendance_list = (
+ frappe.qb.from_(table)
+ .select(table.student, table.status)
+ .where(
+ (table.student_group == student_group)
+ & (table.date == date)
+ & (table.course_schedule == "") | (table.course_schedule.isnull())
+ )
+ ).run(as_dict=True)
for attendance in student_attendance_list:
for student in 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
deleted file mode 100644
index b66d839..0000000
--- a/erpnext/education/doctype/student_attendance_tool/test_student_attendance_tool.js
+++ /dev/null
@@ -1,85 +0,0 @@
-// Testing Attendance Module in Education
-QUnit.module('education');
-
-QUnit.test('Test: Student Attendace Tool', function(assert){
- assert.expect(10);
- let done = assert.async();
- let i, count = 0;
-
- frappe.run_serially([
- () => frappe.timeout(0.2),
- () => frappe.set_route('Form', 'Student Attendance Tool'),
- () => frappe.timeout(0.5),
-
- () => {
- if(cur_frm.doc.based_on == 'Student Group' || cur_frm.doc.based_on == 'Course Schedule'){
- cur_frm.doc.based_on = 'Student Group';
- assert.equal(1, 1, 'Attendance basis correctly set');
- cur_frm.set_value("group_based_on", 'Batch');
- cur_frm.set_value("student_group", "test-batch-wise-group");
- cur_frm.set_value("date", frappe.datetime.nowdate());
- }
- },
- () => frappe.timeout(0.5),
- () => {
- assert.equal($('input.students-check').size(), 5, "Student list based on batch correctly fetched");
- assert.equal(frappe.datetime.nowdate(), cur_frm.doc.date, 'Current date correctly set');
-
- cur_frm.set_value("student_group", "test-batch-wise-group-2");
- assert.equal($('input.students-check').size(), 5, "Student list based on batch 2 correctly fetched");
-
- cur_frm.set_value("group_based_on", 'Course');
-
- cur_frm.set_value("student_group", "test-course-wise-group");
- assert.equal($('input.students-check').size(), 5, "Student list based on course correctly fetched");
-
- cur_frm.set_value("student_group", "test-course-wise-group-2");
- assert.equal($('input.students-check').size(), 5, "Student list based on course 2 correctly fetched");
- },
-
- () => frappe.timeout(1),
- () => frappe.tests.click_button('Check all'), // Marking all Student as checked
- () => {
- for(i = 0; i < $('input.students-check').size(); i++){
- if($('input.students-check')[i].checked == true)
- count++;
- }
-
- if(count == $('input.students-check').size())
- assert.equal($('input.students-check').size(), count, "All students marked checked");
- },
-
- () => frappe.timeout(1),
- () => frappe.tests.click_button('Uncheck all'), // Marking all Student as unchecked
- () => {
- count = 0;
- for(i = 0; i < $('input.students-check').size(); i++){
- if(!($('input.students-check')[i].checked))
- count++;
- }
-
- if(count == $('input.students-check').size())
- assert.equal($('input.students-check').size(), count, "All students marked checked");
- },
-
- () => frappe.timeout(1),
- () => frappe.tests.click_button('Check all'),
- () => frappe.tests.click_button('Mark Attendance'),
- () => frappe.timeout(1),
- () => frappe.tests.click_button('Yes'),
- () => frappe.timeout(1),
- () => {
- assert.equal($('.msgprint').text(), "Attendance has been marked successfully.", "Attendance successfully marked");
- frappe.tests.click_button('Close');
- },
-
- () => frappe.timeout(1),
- () => frappe.set_route('List', 'Student Attendance/List'),
- () => frappe.timeout(1),
- () => {
- assert.equal(cur_list.data.length, count, "Attendance list created");
- },
-
- () => done()
- ]);
-});
diff --git a/erpnext/education/doctype/student_batch_name/test_student_batch_name.js b/erpnext/education/doctype/student_batch_name/test_student_batch_name.js
deleted file mode 100644
index 6c761b8..0000000
--- a/erpnext/education/doctype/student_batch_name/test_student_batch_name.js
+++ /dev/null
@@ -1,19 +0,0 @@
-// Testing Setup Module in Education
-QUnit.module('education');
-
-QUnit.test('Test: Student Batch Name', function(assert){
- assert.expect(1);
- let done = assert.async();
- frappe.run_serially([
- () => {
- return frappe.tests.make('Student Batch Name', [
- {batch_name: 'A'}
- ]);
- },
- () => cur_frm.save(),
- () => {
- assert.ok(cur_frm.doc.batch_name=='A');
- },
- () => done()
- ]);
-});
diff --git a/erpnext/education/doctype/student_category/test_student_category.js b/erpnext/education/doctype/student_category/test_student_category.js
deleted file mode 100644
index 01f50e2..0000000
--- a/erpnext/education/doctype/student_category/test_student_category.js
+++ /dev/null
@@ -1,19 +0,0 @@
-// Testing Setup Module in Education
-QUnit.module('education');
-
-QUnit.test('Test: Student Category', function(assert){
- assert.expect(1);
- let done = assert.async();
- frappe.run_serially([
- () => {
- return frappe.tests.make('Student Category', [
- {category: 'Reservation'}
- ]);
- },
- () => cur_frm.save(),
- () => {
- assert.ok(cur_frm.doc.name=='Reservation');
- },
- () => done()
- ]);
-});
diff --git a/erpnext/education/doctype/student_group/test_student_group.js b/erpnext/education/doctype/student_group/test_student_group.js
deleted file mode 100644
index 4c7e47b..0000000
--- a/erpnext/education/doctype/student_group/test_student_group.js
+++ /dev/null
@@ -1,56 +0,0 @@
-// Testing Student Module in Education
-QUnit.module('education');
-
-QUnit.test('Test: Student Group', function(assert){
- assert.expect(2);
- let done = assert.async();
- let group_based_on = ["test-batch-wise-group", "test-course-wise-group"];
- let tasks = [];
-
- frappe.run_serially([
- // Creating a Batch and Course based group
- () => {
- return frappe.tests.make('Student Group', [
- {academic_year: '2016-17'},
- {academic_term: '2016-17 (Semester 1)'},
- {program: "Standard Test"},
- {group_based_on: 'Batch'},
- {student_group_name: group_based_on[0]},
- {max_strength: 10},
- {batch: 'A'}
- ]);
- },
- () => {
- return frappe.tests.make('Student Group', [
- {academic_year: '2016-17'},
- {academic_term: '2016-17 (Semester 1)'},
- {program: "Standard Test"},
- {group_based_on: 'Course'},
- {student_group_name: group_based_on[1]},
- {max_strength: 10},
- {batch: 'A'},
- {course: 'Test_Sub'},
- ]);
- },
-
- // Populating the created group with Students
- () => {
- tasks = [];
- group_based_on.forEach(index => {
- tasks.push(
- () => frappe.timeout(0.5),
- () => frappe.set_route("Form", ('Student Group/' + index)),
- () => frappe.timeout(0.5),
- () => frappe.tests.click_button('Get Students'),
- () => frappe.timeout(1),
- () => {
- assert.equal(cur_frm.doc.students.length, 5, 'Successfully fetched list of students');
- },
- );
- });
- return frappe.run_serially(tasks);
- },
-
- () => done()
- ]);
-});
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
deleted file mode 100644
index fa612ba..0000000
--- a/erpnext/education/doctype/student_group_creation_tool/test_student_group_creation_tool.js
+++ /dev/null
@@ -1,84 +0,0 @@
-QUnit.module('education');
-
-QUnit.test('Test: Student Group Creation Tool', function(assert){
- assert.expect(5);
- let done = assert.async();
- let instructor_code;
-
- frappe.run_serially([
- // Saving Instructor code beforehand
- () => frappe.db.get_value('Instructor', {'instructor_name': 'Instructor 1'}, 'name'),
- (instructor) => {instructor_code = instructor.message.name;},
-
- // Setting up the creation tool to generate and save Student Group
- () => frappe.set_route('Form', 'Student Group Creation Tool'),
- () => frappe.timeout(0.5),
- () => {
- cur_frm.set_value("academic_year", "2016-17");
- cur_frm.set_value("academic_term", "2016-17 (Semester 1)");
- cur_frm.set_value("program", "Standard Test");
- frappe.tests.click_button('Get Courses');
- },
- () => frappe.timeout(1),
- () => {
- let no_of_courses = $('input.grid-row-check.pull-left').size() - 1;
- assert.equal(cur_frm.doc.courses.length, no_of_courses, 'Successfully created groups using the tool');
- },
-
- () => {
- let d, grid, grid_row;
-
- for(d = 0; d < cur_frm.doc.courses.length; d++)
- {
- grid = cur_frm.get_field("courses").grid;
- grid_row = grid.get_row(d).toggle_view(true);
- if(grid_row.doc.student_group_name == 'Standard Test/A/2016-17 (Semester 1)'){
- grid_row.doc.max_strength = 10;
- grid_row.doc.student_group_name = "test-batch-wise-group-2";
- $(`.octicon.octicon-triangle-up`).click();
- continue;
- }
- else if(grid_row.doc.student_group_name == 'Test_Sub/Standard Test/2016-17 (Semester 1)'){
- grid_row.doc.max_strength = 10;
- grid_row.doc.student_group_name = "test-course-wise-group-2";
- $(`.octicon.octicon-triangle-up`).click();
- continue;
- }
- }
- },
-
- // Generating Student Group
- () => frappe.timeout(0.5),
- () => frappe.tests.click_button("Create Student Groups"),
- () => frappe.timeout(0.5),
- () => frappe.tests.click_button("Close"),
-
- // Goin to the generated group to set up student and instructor list
- () => {
- let group_name = ['Student Group/test-batch-wise-group-2', 'Student Group/test-course-wise-group-2'];
- let tasks = [];
- group_name.forEach(index => {
- tasks.push(
- () => frappe.timeout(1),
- () => frappe.set_route("Form", index),
- () => frappe.timeout(0.5),
- () => {
- assert.equal(cur_frm.doc.students.length, 5, 'Successfully fetched list of students');
- },
- () => frappe.timeout(0.5),
- () => {
- d = cur_frm.add_child('instructors');
- d.instructor = instructor_code;
- cur_frm.save();
- },
- () => {
- assert.equal(cur_frm.doc.instructors.length, 1, 'Instructor detail stored successfully');
- },
- );
- });
- return frappe.run_serially(tasks);
- },
-
- () => done()
- ]);
-});
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
deleted file mode 100644
index 6bbf17b..0000000
--- a/erpnext/education/doctype/student_leave_application/test_student_leave_application.js
+++ /dev/null
@@ -1,69 +0,0 @@
-// Testing Attendance Module in Education
-QUnit.module('education');
-
-QUnit.test('Test: Student Leave Application', function(assert){
- assert.expect(4);
- let done = assert.async();
- let student_code;
- let leave_code;
- frappe.run_serially([
- () => frappe.db.get_value('Student', {'student_email_id': 'test2@testmail.com'}, 'name'),
- (student) => {student_code = student.message.name;}, // fetching student code from db
-
- () => {
- return frappe.tests.make('Student Leave Application', [
- {student: student_code},
- {from_date: '2017-08-02'},
- {to_date: '2017-08-04'},
- {mark_as_present: 0},
- {reason: "Sick Leave."}
- ]);
- },
- () => frappe.tests.click_button('Submit'), // Submitting the leave application
- () => frappe.timeout(0.7),
- () => frappe.tests.click_button('Yes'),
- () => frappe.timeout(0.7),
- () => {
- assert.equal(cur_frm.doc.docstatus, 1, "Submitted leave application");
- leave_code = frappe.get_route()[2];
- },
- () => frappe.tests.click_button('Cancel'), // Cancelling the leave application
- () => frappe.timeout(0.7),
- () => frappe.tests.click_button('Yes'),
- () => frappe.timeout(1),
- () => {assert.equal(cur_frm.doc.docstatus, 2, "Cancelled leave application");},
- () => frappe.tests.click_button('Amend'), // Amending the leave application
- () => frappe.timeout(1),
- () => {
- cur_frm.doc.mark_as_present = 1;
- cur_frm.save();
- },
- () => frappe.timeout(0.7),
- () => frappe.tests.click_button('Submit'),
- () => frappe.timeout(0.7),
- () => frappe.tests.click_button('Yes'),
- () => frappe.timeout(0.7),
- () => {assert.equal(cur_frm.doc.amended_from, leave_code, "Amended successfully");},
-
- () => frappe.timeout(0.5),
- () => {
- return frappe.tests.make('Student Leave Application', [
- {student: student_code},
- {from_date: '2017-08-07'},
- {to_date: '2017-08-09'},
- {mark_as_present: 0},
- {reason: "Sick Leave."}
- ]);
- },
- () => frappe.tests.click_button('Submit'),
- () => frappe.timeout(0.7),
- () => frappe.tests.click_button('Yes'),
- () => frappe.timeout(0.7),
- () => {
- assert.equal(cur_frm.doc.docstatus, 1, "Submitted leave application");
- leave_code = frappe.get_route()[2];
- },
-
- () => done()
- ]);
-});
diff --git a/erpnext/education/doctype/student_log/test_student_log.js b/erpnext/education/doctype/student_log/test_student_log.js
deleted file mode 100644
index 4c90c5f..0000000
--- a/erpnext/education/doctype/student_log/test_student_log.js
+++ /dev/null
@@ -1,35 +0,0 @@
-// Testing Student Module in Education
-QUnit.module('education');
-
-QUnit.test('Test: Student Log', function(assert){
- assert.expect(9);
- let done = assert.async();
- let student_code;
- frappe.run_serially([
- () => frappe.db.get_value('Student', {'student_email_id': 'test2@testmail.com'}, 'name'),
- (student) => {student_code = student.message.name;},
- () => {
- return frappe.tests.make("Student Log", [
- {student: student_code},
- {academic_year: '2016-17'},
- {academic_term: '2016-17 (Semester 1)'},
- {program: "Standard Test"},
- {date: '2017-07-31'},
- {student_batch: 'A'},
- {log: 'This is Test log.'}
- ]);
- },
- () => {
- assert.equal(cur_frm.doc.student, student_code, 'Student code was fetched properly');
- assert.equal(cur_frm.doc.student_name, 'Fname Mname Lname', 'Student name was correctly auto-fetched');
- assert.equal(cur_frm.doc.type, 'General', 'Default type selected');
- assert.equal(cur_frm.doc.academic_year, '2016-17');
- assert.equal(cur_frm.doc.academic_term, '2016-17 (Semester 1)');
- assert.equal(cur_frm.doc.program, 'Standard Test', 'Program correctly selected');
- assert.equal(cur_frm.doc.student_batch, 'A');
- assert.equal(cur_frm.doc.date, '2017-07-31');
- assert.equal(cur_frm.doc.log, 'This is Test log.');
- },
- () => done()
- ]);
-});
diff --git a/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.json b/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.json
index 23ccb7e..6afd3f7 100644
--- a/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.json
+++ b/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.json
@@ -20,7 +20,6 @@
"configuration_cb",
"shipping_account_head",
"section_break_12",
- "nexus_address",
"nexus"
],
"fields": [
@@ -87,16 +86,12 @@
"fieldtype": "Column Break"
},
{
+ "depends_on": "nexus",
"fieldname": "section_break_12",
"fieldtype": "Section Break",
"label": "Nexus List"
},
{
- "fieldname": "nexus_address",
- "fieldtype": "HTML",
- "label": "Nexus Address"
- },
- {
"fieldname": "nexus",
"fieldtype": "Table",
"label": "Nexus",
@@ -108,19 +103,19 @@
"fieldtype": "Column Break"
},
{
- "fieldname": "column_break_10",
- "fieldtype": "Column Break"
- },
- {
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company"
+ },
+ {
+ "fieldname": "column_break_10",
+ "fieldtype": "Column Break"
}
],
"issingle": 1,
"links": [],
- "modified": "2021-11-08 18:02:29.232090",
+ "modified": "2021-11-30 12:17:24.647979",
"modified_by": "Administrator",
"module": "ERPNext Integrations",
"name": "TaxJar Settings",
diff --git a/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.py b/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.py
index b9f24b6..d4bbe88 100644
--- a/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.py
+++ b/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.py
@@ -16,9 +16,9 @@
class TaxJarSettings(Document):
def on_update(self):
- TAXJAR_CREATE_TRANSACTIONS = frappe.db.get_single_value("TaxJar Settings", "taxjar_create_transactions")
- TAXJAR_CALCULATE_TAX = frappe.db.get_single_value("TaxJar Settings", "taxjar_calculate_tax")
- TAXJAR_SANDBOX_MODE = frappe.db.get_single_value("TaxJar Settings", "is_sandbox")
+ TAXJAR_CREATE_TRANSACTIONS = self.taxjar_create_transactions
+ TAXJAR_CALCULATE_TAX = self.taxjar_calculate_tax
+ TAXJAR_SANDBOX_MODE = self.is_sandbox
fields_already_exist = frappe.db.exists('Custom Field', {'dt': ('in', ['Item','Sales Invoice Item']), 'fieldname':'product_tax_category'})
fields_hidden = frappe.get_value('Custom Field', {'dt': ('in', ['Sales Invoice Item'])}, 'hidden')
diff --git a/erpnext/erpnext_integrations/workspace/erpnext_integrations_settings/erpnext_integrations_settings.json b/erpnext/erpnext_integrations/workspace/erpnext_integrations_settings/erpnext_integrations_settings.json
deleted file mode 100644
index 5fe5afa..0000000
--- a/erpnext/erpnext_integrations/workspace/erpnext_integrations_settings/erpnext_integrations_settings.json
+++ /dev/null
@@ -1,89 +0,0 @@
-{
- "charts": [],
- "content": "[{\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Integrations Settings\", \"col\": 4}}]",
- "creation": "2020-07-31 10:38:54.021237",
- "docstatus": 0,
- "doctype": "Workspace",
- "for_user": "",
- "hide_custom": 0,
- "icon": "setting",
- "idx": 0,
- "label": "ERPNext Integrations Settings",
- "links": [
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Integrations Settings",
- "link_count": 0,
- "onboard": 0,
- "type": "Card Break"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Woocommerce Settings",
- "link_count": 0,
- "link_to": "Woocommerce Settings",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Shopify Settings",
- "link_count": 0,
- "link_to": "Shopify Settings",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Amazon MWS Settings",
- "link_count": 0,
- "link_to": "Amazon MWS Settings",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Plaid Settings",
- "link_count": 0,
- "link_to": "Plaid Settings",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Exotel Settings",
- "link_count": 0,
- "link_to": "Exotel Settings",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- }
- ],
- "modified": "2021-08-05 12:15:58.951705",
- "modified_by": "Administrator",
- "module": "ERPNext Integrations",
- "name": "ERPNext Integrations Settings",
- "owner": "Administrator",
- "parent_page": "",
- "public": 1,
- "restrict_to_domain": "",
- "roles": [],
- "sequence_id": 11,
- "shortcuts": [],
- "title": "ERPNext Integrations Settings"
-}
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index 3eb6ce7..e779e0f 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -65,7 +65,6 @@
calendars = ["Task", "Work Order", "Leave Application", "Sales Order", "Holiday List", "Course Schedule"]
domains = {
- 'Agriculture': 'erpnext.domains.agriculture',
'Distribution': 'erpnext.domains.distribution',
'Education': 'erpnext.domains.education',
'Hospitality': 'erpnext.domains.hospitality',
@@ -234,7 +233,7 @@
},
"Communication": {
"on_update": [
- "erpnext.support.doctype.service_level_agreement.service_level_agreement.update_hold_time",
+ "erpnext.support.doctype.service_level_agreement.service_level_agreement.on_communication_update",
"erpnext.support.doctype.issue.issue.set_first_response_time"
]
},
@@ -245,25 +244,26 @@
"validate": "erpnext.regional.india.utils.validate_tax_category"
},
"Sales Invoice": {
- "after_insert": "erpnext.regional.saudi_arabia.utils.create_qr_code",
"on_submit": [
"erpnext.regional.create_transaction_log",
"erpnext.regional.italy.utils.sales_invoice_on_submit",
+ "erpnext.regional.saudi_arabia.utils.create_qr_code",
"erpnext.erpnext_integrations.taxjar_integration.create_transaction"
],
"on_cancel": [
"erpnext.regional.italy.utils.sales_invoice_on_cancel",
- "erpnext.erpnext_integrations.taxjar_integration.delete_transaction"
- ],
- "on_trash": [
- "erpnext.regional.check_deletion_permission",
+ "erpnext.erpnext_integrations.taxjar_integration.delete_transaction",
"erpnext.regional.saudi_arabia.utils.delete_qr_code_file"
],
+ "on_trash": "erpnext.regional.check_deletion_permission",
"validate": [
"erpnext.regional.india.utils.validate_document_name",
"erpnext.regional.india.utils.update_taxable_values"
]
},
+ "POS Invoice": {
+ "on_submit": ["erpnext.regional.saudi_arabia.utils.create_qr_code"]
+ },
"Purchase Invoice": {
"validate": [
"erpnext.regional.india.utils.validate_reverse_charge_transaction",
@@ -303,7 +303,8 @@
'validate': ["erpnext.erpnext_integrations.taxjar_integration.set_sales_tax"]
},
"Company": {
- "on_trash": "erpnext.regional.india.utils.delete_gst_settings_for_company"
+ "on_trash": ["erpnext.regional.india.utils.delete_gst_settings_for_company",
+ "erpnext.regional.saudi_arabia.utils.delete_vat_settings_for_company"]
},
"Integration Request": {
"validate": "erpnext.accounts.doctype.payment_request.payment_request.validate_payment"
@@ -338,8 +339,7 @@
"erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.automatic_synchronization",
"erpnext.projects.doctype.project.project.hourly_reminder",
"erpnext.projects.doctype.project.project.collect_project_status",
- "erpnext.hr.doctype.shift_type.shift_type.process_auto_attendance_for_all_shifts",
- "erpnext.support.doctype.service_level_agreement.service_level_agreement.set_service_level_agreement_variance"
+ "erpnext.hr.doctype.shift_type.shift_type.process_auto_attendance_for_all_shifts"
],
"hourly_long": [
"erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries"
@@ -370,7 +370,7 @@
"erpnext.selling.doctype.quotation.quotation.set_expired_status",
"erpnext.buying.doctype.supplier_quotation.supplier_quotation.set_expired_status",
"erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.send_auto_email",
- "erpnext.non_profit.doctype.membership.membership.set_expired_status"
+ "erpnext.non_profit.doctype.membership.membership.set_expired_status",
"erpnext.hr.doctype.interview.interview.send_daily_feedback_reminder"
],
"daily_long": [
@@ -563,18 +563,6 @@
{'doctype': 'Assessment Code', 'index': 39},
{'doctype': 'Discussion', 'index': 40},
],
- "Agriculture": [
- {'doctype': 'Weather', 'index': 1},
- {'doctype': 'Soil Texture', 'index': 2},
- {'doctype': 'Water Analysis', 'index': 3},
- {'doctype': 'Soil Analysis', 'index': 4},
- {'doctype': 'Plant Analysis', 'index': 5},
- {'doctype': 'Agriculture Analysis Criteria', 'index': 6},
- {'doctype': 'Disease', 'index': 7},
- {'doctype': 'Crop', 'index': 8},
- {'doctype': 'Fertilizer', 'index': 9},
- {'doctype': 'Crop Cycle', 'index': 10}
- ],
"Non Profit": [
{'doctype': 'Certified Consultant', 'index': 1},
{'doctype': 'Certification Application', 'index': 2},
diff --git a/erpnext/hr/doctype/appointment_letter/appointment_letter.py b/erpnext/hr/doctype/appointment_letter/appointment_letter.py
index 0120188..71327bf 100644
--- a/erpnext/hr/doctype/appointment_letter/appointment_letter.py
+++ b/erpnext/hr/doctype/appointment_letter/appointment_letter.py
@@ -12,14 +12,15 @@
@frappe.whitelist()
def get_appointment_letter_details(template):
body = []
- intro= frappe.get_list("Appointment Letter Template",
- fields = ['introduction', 'closing_notes'],
- filters={'name': template
- })[0]
- content = frappe.get_list("Appointment Letter content",
- fields = ['title', 'description'],
- filters={'parent': template
- })
+ intro = frappe.get_list('Appointment Letter Template',
+ fields=['introduction', 'closing_notes'],
+ filters={'name': template}
+ )[0]
+ content = frappe.get_all('Appointment Letter content',
+ fields=['title', 'description'],
+ filters={'parent': template},
+ order_by='idx'
+ )
body.append(intro)
body.append({'description': content})
return body
diff --git a/erpnext/hr/doctype/appraisal/test_appraisal.js b/erpnext/hr/doctype/appraisal/test_appraisal.js
deleted file mode 100644
index fb1354c..0000000
--- a/erpnext/hr/doctype/appraisal/test_appraisal.js
+++ /dev/null
@@ -1,57 +0,0 @@
-QUnit.module('hr');
-
-QUnit.test("Test: Expense Claim [HR]", function (assert) {
- assert.expect(3);
- let done = assert.async();
- let employee_name;
-
- frappe.run_serially([
- // Creating Appraisal
- () => frappe.set_route('List','Appraisal','List'),
- () => frappe.timeout(0.3),
- () => frappe.click_button('Make a new Appraisal'),
- () => {
- cur_frm.set_value('kra_template','Test Appraisal 1'),
- cur_frm.set_value('start_date','2017-08-21'),
- cur_frm.set_value('end_date','2017-09-21');
- },
- () => frappe.timeout(1),
- () => frappe.model.set_value('Appraisal Goal','New Appraisal Goal 1','score',4),
- () => frappe.model.set_value('Appraisal Goal','New Appraisal Goal 1','score_earned',2),
- () => frappe.model.set_value('Appraisal Goal','New Appraisal Goal 2','score',4),
- () => frappe.model.set_value('Appraisal Goal','New Appraisal Goal 2','score_earned',2),
- () => frappe.timeout(1),
- () => frappe.db.get_value('Employee', {'employee_name': 'Test Employee 1'}, 'name'),
- (r) => {
- employee_name = r.message.name;
- },
-
- () => frappe.timeout(1),
- () => cur_frm.set_value('employee',employee_name),
- () => cur_frm.set_value('employee_name','Test Employee 1'),
- () => cur_frm.set_value('company','For Testing'),
- () => frappe.click_button('Calculate Total Score'),
- () => frappe.timeout(1),
- () => cur_frm.save(),
- () => frappe.timeout(1),
- () => cur_frm.save(),
-
- // Submitting the Appraisal
- () => frappe.click_button('Submit'),
- () => frappe.click_button('Yes'),
- () => frappe.timeout(3),
-
- // Checking if the appraisal is correctly set for the employee
- () => {
- assert.equal('Submitted',cur_frm.get_field('status').value,
- 'Appraisal is submitted');
-
- assert.equal('Test Employee 1',cur_frm.get_field('employee_name').value,
- 'Appraisal is created for correct employee');
-
- assert.equal(4,cur_frm.get_field('total_score').value,
- 'Total score is correctly calculated');
- },
- () => done()
- ]);
-});
diff --git a/erpnext/hr/doctype/appraisal_template/test_appraisal_template.js b/erpnext/hr/doctype/appraisal_template/test_appraisal_template.js
deleted file mode 100644
index 3eb64e0..0000000
--- a/erpnext/hr/doctype/appraisal_template/test_appraisal_template.js
+++ /dev/null
@@ -1,29 +0,0 @@
-QUnit.module('hr');
-QUnit.test("Test: Appraisal Template [HR]", function (assert) {
- assert.expect(1);
- let done = assert.async();
- frappe.run_serially([
- // Job Opening creation
- () => {
- frappe.tests.make('Appraisal Template', [
- { kra_title: 'Test Appraisal 1'},
- { description: 'This is just a test'},
- { goals: [
- [
- { kra: 'Design'},
- { per_weightage: 50}
- ],
- [
- { kra: 'Code creation'},
- { per_weightage: 50}
- ]
- ]},
- ]);
- },
- () => frappe.timeout(10),
- () => {
- assert.equal('Test Appraisal 1',cur_frm.doc.kra_title, 'Appraisal name correctly set');
- },
- () => done()
- ]);
-});
diff --git a/erpnext/hr/doctype/attendance/test_attendance.js b/erpnext/hr/doctype/attendance/test_attendance.js
deleted file mode 100644
index b3e7fef..0000000
--- a/erpnext/hr/doctype/attendance/test_attendance.js
+++ /dev/null
@@ -1,39 +0,0 @@
-QUnit.module('hr');
-
-QUnit.test("Test: Attendance [HR]", function (assert) {
- assert.expect(4);
- let done = assert.async();
-
- frappe.run_serially([
- // test attendance creation for one employee
- () => frappe.set_route("List", "Attendance", "List"),
- () => frappe.timeout(0.5),
- () => frappe.new_doc("Attendance"),
- () => frappe.timeout(1),
- () => assert.equal("Attendance", cur_frm.doctype,
- "Form for new Attendance opened successfully."),
- // set values in form
- () => cur_frm.set_value("company", "For Testing"),
- () => {
- frappe.db.get_value('Employee', {'employee_name':'Test Employee 1'}, 'name', function(r) {
- cur_frm.set_value("employee", r.name)
- });
- },
- () => frappe.timeout(1),
- () => cur_frm.save(),
- () => frappe.timeout(1),
- // check docstatus of attendance before submit [Draft]
- () => assert.equal("0", cur_frm.doc.docstatus,
- "attendance is currently drafted"),
- // check docstatus of attendance after submit [Present]
- () => cur_frm.savesubmit(),
- () => frappe.timeout(0.5),
- () => frappe.click_button('Yes'),
- () => assert.equal("1", cur_frm.doc.docstatus,
- "attendance is saved after submit"),
- // check if auto filled date is present day
- () => assert.equal(frappe.datetime.nowdate(), cur_frm.doc.attendance_date,
- "attendance for Present day is marked"),
- () => done()
- ]);
-});
diff --git a/erpnext/hr/doctype/department/department.js b/erpnext/hr/doctype/department/department.js
index 7db8cfb..46cfbda 100644
--- a/erpnext/hr/doctype/department/department.js
+++ b/erpnext/hr/doctype/department/department.js
@@ -6,6 +6,15 @@
frm.set_query("parent_department", function(){
return {"filters": [["Department", "is_group", "=", 1]]};
});
+
+ frm.set_query("payroll_cost_center", function() {
+ return {
+ filters: {
+ "company": frm.doc.company,
+ "is_group": 0
+ }
+ };
+ });
},
refresh: function(frm) {
// read-only for root department
diff --git a/erpnext/hr/doctype/employee/employee.js b/erpnext/hr/doctype/employee/employee.js
index 13b33e2..8c73e9c 100755
--- a/erpnext/hr/doctype/employee/employee.js
+++ b/erpnext/hr/doctype/employee/employee.js
@@ -47,6 +47,15 @@
}
};
});
+
+ frm.set_query("payroll_cost_center", function() {
+ return {
+ filters: {
+ "company": frm.doc.company,
+ "is_group": 0
+ }
+ };
+ });
},
onload: function (frm) {
frm.set_query("department", function() {
diff --git a/erpnext/hr/doctype/employee/employee.py b/erpnext/hr/doctype/employee/employee.py
index 79e8f61..a2df26c 100755
--- a/erpnext/hr/doctype/employee/employee.py
+++ b/erpnext/hr/doctype/employee/employee.py
@@ -68,12 +68,18 @@
self.employee_name = ' '.join(filter(lambda x: x, [self.first_name, self.middle_name, self.last_name]))
def validate_user_details(self):
- data = frappe.db.get_value('User',
- self.user_id, ['enabled', 'user_image'], as_dict=1)
- if data.get("user_image") and self.image == '':
- self.image = data.get("user_image")
- self.validate_for_enabled_user_id(data.get("enabled", 0))
- self.validate_duplicate_user_id()
+ if self.user_id:
+ data = frappe.db.get_value('User',
+ self.user_id, ['enabled', 'user_image'], as_dict=1)
+
+ if not data:
+ self.user_id = None
+ return
+
+ if data.get("user_image") and self.image == '':
+ self.image = data.get("user_image")
+ self.validate_for_enabled_user_id(data.get("enabled", 0))
+ self.validate_duplicate_user_id()
def update_nsm_model(self):
frappe.utils.nestedset.update_nsm(self)
@@ -96,15 +102,8 @@
'user': self.user_id
})
- if employee_user_permission_exists: return
-
- employee_user_permission_exists = frappe.db.exists('User Permission', {
- 'allow': 'Employee',
- 'for_value': self.name,
- 'user': self.user_id
- })
-
- if employee_user_permission_exists: return
+ if employee_user_permission_exists:
+ return
add_user_permission("Employee", self.name, self.user_id)
set_user_permission_if_allowed("Company", self.company, self.user_id)
diff --git a/erpnext/hr/doctype/employee/employee_dashboard.py b/erpnext/hr/doctype/employee/employee_dashboard.py
index a4c0af0..a1247d9 100644
--- a/erpnext/hr/doctype/employee/employee_dashboard.py
+++ b/erpnext/hr/doctype/employee/employee_dashboard.py
@@ -21,7 +21,11 @@
},
{
'label': _('Lifecycle'),
- 'items': ['Employee Transfer', 'Employee Promotion', 'Employee Separation', 'Employee Grievance']
+ 'items': ['Employee Transfer', 'Employee Promotion', 'Employee Grievance']
+ },
+ {
+ 'label': _('Exit'),
+ 'items': ['Employee Separation', 'Exit Interview', 'Full and Final Statement']
},
{
'label': _('Shift'),
diff --git a/erpnext/hr/doctype/employee/test_employee.js b/erpnext/hr/doctype/employee/test_employee.js
deleted file mode 100644
index 3a41458..0000000
--- a/erpnext/hr/doctype/employee/test_employee.js
+++ /dev/null
@@ -1,40 +0,0 @@
-QUnit.module('hr');
-
-QUnit.test("Test: Employee [HR]", function (assert) {
- assert.expect(4);
- let done = assert.async();
- // let today_date = frappe.datetime.nowdate();
- let employee_creation = (name, joining_date, birth_date) => {
- frappe.run_serially([
- // test employee creation
- () => {
- frappe.tests.make('Employee', [
- { employee_name: name},
- { salutation: 'Mr'},
- { company: 'For Testing'},
- { date_of_joining: joining_date},
- { date_of_birth: birth_date},
- { employment_type: 'Test Employment Type'},
- { holiday_list: 'Test Holiday List'},
- { branch: 'Test Branch'},
- { department: 'Test Department'},
- { designation: 'Test Designation'}
- ]);
- },
- () => frappe.timeout(2),
- () => {
- assert.ok(cur_frm.get_field('employee_name').value==name,
- 'Name of an Employee is correctly set');
- assert.ok(cur_frm.get_field('gender').value=='Male',
- 'Gender of an Employee is correctly set');
- },
- ]);
- };
- frappe.run_serially([
- () => employee_creation('Test Employee 1','2017-04-01','1992-02-02'),
- () => frappe.timeout(10),
- () => employee_creation('Test Employee 3','2017-04-01','1992-02-02'),
- () => frappe.timeout(10),
- () => done()
- ]);
-});
diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.py b/erpnext/hr/doctype/employee_advance/employee_advance.py
index 8a8e8db..7aac2b6 100644
--- a/erpnext/hr/doctype/employee_advance/employee_advance.py
+++ b/erpnext/hr/doctype/employee_advance/employee_advance.py
@@ -5,6 +5,7 @@
import frappe
from frappe import _
from frappe.model.document import Document
+from frappe.query_builder.functions import Sum
from frappe.utils import flt, nowdate
import erpnext
@@ -41,24 +42,34 @@
self.status = "Cancelled"
def set_total_advance_paid(self):
- paid_amount = frappe.db.sql("""
- select ifnull(sum(debit), 0) as paid_amount
- from `tabGL Entry`
- where against_voucher_type = 'Employee Advance'
- and against_voucher = %s
- and party_type = 'Employee'
- and party = %s
- """, (self.name, self.employee), as_dict=1)[0].paid_amount
+ gle = frappe.qb.DocType("GL Entry")
- return_amount = frappe.db.sql("""
- select ifnull(sum(credit), 0) as return_amount
- from `tabGL Entry`
- where against_voucher_type = 'Employee Advance'
- and voucher_type != 'Expense Claim'
- and against_voucher = %s
- and party_type = 'Employee'
- and party = %s
- """, (self.name, self.employee), as_dict=1)[0].return_amount
+ paid_amount = (
+ frappe.qb.from_(gle)
+ .select(Sum(gle.debit).as_("paid_amount"))
+ .where(
+ (gle.against_voucher_type == 'Employee Advance')
+ & (gle.against_voucher == self.name)
+ & (gle.party_type == 'Employee')
+ & (gle.party == self.employee)
+ & (gle.docstatus == 1)
+ & (gle.is_cancelled == 0)
+ )
+ ).run(as_dict=True)[0].paid_amount or 0
+
+ return_amount = (
+ frappe.qb.from_(gle)
+ .select(Sum(gle.credit).as_("return_amount"))
+ .where(
+ (gle.against_voucher_type == 'Employee Advance')
+ & (gle.voucher_type != 'Expense Claim')
+ & (gle.against_voucher == self.name)
+ & (gle.party_type == 'Employee')
+ & (gle.party == self.employee)
+ & (gle.docstatus == 1)
+ & (gle.is_cancelled == 0)
+ )
+ ).run(as_dict=True)[0].return_amount or 0
if paid_amount != 0:
paid_amount = flt(paid_amount) / flt(self.exchange_rate)
diff --git a/erpnext/hr/doctype/employee_advance/test_employee_advance.py b/erpnext/hr/doctype/employee_advance/test_employee_advance.py
index 4ecfa60..5f2e720 100644
--- a/erpnext/hr/doctype/employee_advance/test_employee_advance.py
+++ b/erpnext/hr/doctype/employee_advance/test_employee_advance.py
@@ -34,6 +34,24 @@
journal_entry1 = make_payment_entry(advance)
self.assertRaises(EmployeeAdvanceOverPayment, journal_entry1.submit)
+ def test_paid_amount_on_pe_cancellation(self):
+ employee_name = make_employee("_T@employe.advance")
+ advance = make_employee_advance(employee_name)
+
+ pe = make_payment_entry(advance)
+ pe.submit()
+
+ advance.reload()
+
+ self.assertEqual(advance.paid_amount, 1000)
+ self.assertEqual(advance.status, "Paid")
+
+ pe.cancel()
+ advance.reload()
+
+ self.assertEqual(advance.paid_amount, 0)
+ self.assertEqual(advance.status, "Unpaid")
+
def test_repay_unclaimed_amount_from_salary(self):
employee_name = make_employee("_T@employe.advance")
advance = make_employee_advance(employee_name, {"repay_unclaimed_amount_from_salary": 1})
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
deleted file mode 100644
index 48d4344..0000000
--- a/erpnext/hr/doctype/employee_attendance_tool/test_employee_attendance_tool.js
+++ /dev/null
@@ -1,61 +0,0 @@
-QUnit.module('hr');
-
-QUnit.test("Test: Employee attendance tool [HR]", function (assert) {
- assert.expect(2);
- let done = assert.async();
- let today_date = frappe.datetime.nowdate();
- let date_of_attendance = frappe.datetime.add_days(today_date, -2); // previous day
-
- frappe.run_serially([
- // create employee
- () => {
- return frappe.tests.make('Employee', [
- {salutation: "Mr"},
- {employee_name: "Test Employee 2"},
- {company: "For Testing"},
- {date_of_joining: frappe.datetime.add_months(today_date, -2)}, // joined 2 month from now
- {date_of_birth: frappe.datetime.add_months(today_date, -240)}, // age is 20 years
- {employment_type: "Test Employment type"},
- {holiday_list: "Test Holiday list"},
- {branch: "Test Branch"},
- {department: "Test Department"},
- {designation: "Test Designation"}
- ]);
- },
- () => frappe.set_route("Form", "Employee Attendance Tool"),
- () => frappe.timeout(0.5),
- () => assert.equal("Employee Attendance Tool", cur_frm.doctype,
- "Form for Employee Attendance Tool opened successfully."),
- // set values in form
- () => cur_frm.set_value("date", date_of_attendance),
- () => cur_frm.set_value("branch", "Test Branch"),
- () => cur_frm.set_value("department", "Test Department"),
- () => cur_frm.set_value("company", "For Testing"),
- () => frappe.timeout(1),
- () => frappe.click_button('Check all'),
- () => frappe.click_button('Mark Present'),
- // check if attendance is marked
- () => frappe.set_route("List", "Attendance", "List"),
- () => frappe.timeout(1),
- () => {
- return frappe.call({
- method: "frappe.client.get_list",
- args: {
- doctype: "Employee",
- filters: {
- "branch": "Test Branch",
- "department": "Test Department",
- "company": "For Testing",
- "status": "Active"
- }
- },
- callback: function(r) {
- let marked_attendance = cur_list.data.filter(d => d.attendance_date == date_of_attendance);
- assert.equal(marked_attendance.length, r.message.length,
- 'all the attendance are marked for correct date');
- }
- });
- },
- () => done()
- ]);
-});
diff --git a/erpnext/hr/doctype/employee_transfer/test_employee_transfer.py b/erpnext/hr/doctype/employee_transfer/test_employee_transfer.py
index 287dfba..64eee40 100644
--- a/erpnext/hr/doctype/employee_transfer/test_employee_transfer.py
+++ b/erpnext/hr/doctype/employee_transfer/test_employee_transfer.py
@@ -2,7 +2,6 @@
# See license.txt
import unittest
-from datetime import date
import frappe
from frappe.utils import add_days, getdate
@@ -12,16 +11,14 @@
class TestEmployeeTransfer(unittest.TestCase):
def setUp(self):
- make_employee("employee2@transfers.com")
- make_employee("employee3@transfers.com")
create_company()
- create_employee()
- create_employee_transfer()
def tearDown(self):
frappe.db.rollback()
def test_submit_before_transfer_date(self):
+ make_employee("employee2@transfers.com")
+
transfer_obj = frappe.get_doc({
"doctype": "Employee Transfer",
"employee": frappe.get_value("Employee", {"user_id":"employee2@transfers.com"}, "name"),
@@ -43,6 +40,8 @@
self.assertEqual(transfer.docstatus, 1)
def test_new_employee_creation(self):
+ make_employee("employee3@transfers.com")
+
transfer = frappe.get_doc({
"doctype": "Employee Transfer",
"employee": frappe.get_value("Employee", {"user_id":"employee3@transfers.com"}, "name"),
@@ -63,60 +62,51 @@
self.assertEqual(frappe.get_value("Employee", transfer.employee, "status"), "Left")
def test_employee_history(self):
- name = frappe.get_value("Employee", {"first_name": "John", "company": "Test Company"}, "name")
- doc = frappe.get_doc("Employee",name)
+ employee = make_employee("employee4@transfers.com",
+ company="Test Company",
+ date_of_birth=getdate("30-09-1980"),
+ date_of_joining=getdate("01-10-2021"),
+ department="Accounts - TC",
+ designation="Accountant"
+ )
+ transfer = create_employee_transfer(employee)
+
count = 0
department = ["Accounts - TC", "Management - TC"]
designation = ["Accountant", "Manager"]
- dt = [getdate("01-10-2021"), date.today()]
+ dt = [getdate("01-10-2021"), getdate()]
- for data in doc.internal_work_history:
+ employee = frappe.get_doc("Employee", employee)
+ for data in employee.internal_work_history:
self.assertEqual(data.department, department[count])
self.assertEqual(data.designation, designation[count])
self.assertEqual(data.from_date, dt[count])
count = count + 1
- data = frappe.db.get_list("Employee Transfer", filters={"employee":name}, fields=["*"])
- doc = frappe.get_doc("Employee Transfer", data[0]["name"])
- doc.cancel()
- employee_doc = frappe.get_doc("Employee",name)
+ transfer.cancel()
+ employee.reload()
- for data in employee_doc.internal_work_history:
+ for data in employee.internal_work_history:
self.assertEqual(data.designation, designation[0])
self.assertEqual(data.department, department[0])
self.assertEqual(data.from_date, dt[0])
-def create_employee():
- doc = frappe.get_doc({
- "doctype": "Employee",
- "first_name": "John",
- "company": "Test Company",
- "gender": "Male",
- "date_of_birth": getdate("30-09-1980"),
- "date_of_joining": getdate("01-10-2021"),
- "department": "Accounts - TC",
- "designation": "Accountant"
- })
-
- doc.save()
def create_company():
- exists = frappe.db.exists("Company", "Test Company")
- if not exists:
- doc = frappe.get_doc({
- "doctype": "Company",
- "company_name": "Test Company",
- "default_currency": "INR",
- "country": "India"
- })
+ if not frappe.db.exists("Company", "Test Company"):
+ frappe.get_doc({
+ "doctype": "Company",
+ "company_name": "Test Company",
+ "default_currency": "INR",
+ "country": "India"
+ }).insert()
- doc.save()
-def create_employee_transfer():
+def create_employee_transfer(employee):
doc = frappe.get_doc({
"doctype": "Employee Transfer",
- "employee": frappe.get_value("Employee", {"first_name": "John", "company": "Test Company"}, "name"),
- "transfer_date": date.today(),
+ "employee": employee,
+ "transfer_date": getdate(),
"transfer_details": [
{
"property": "Designation",
@@ -134,4 +124,6 @@
})
doc.save()
- doc.submit()
\ No newline at end of file
+ doc.submit()
+
+ return doc
\ No newline at end of file
diff --git a/erpnext/hr/doctype/employment_type/test_employment_type.js b/erpnext/hr/doctype/employment_type/test_employment_type.js
deleted file mode 100644
index fd7c6a1..0000000
--- a/erpnext/hr/doctype/employment_type/test_employment_type.js
+++ /dev/null
@@ -1,22 +0,0 @@
-QUnit.module('hr');
-
-QUnit.test("Test: Employment type [HR]", function (assert) {
- assert.expect(1);
- let done = assert.async();
-
- frappe.run_serially([
- // test employment type creation
- () => frappe.set_route("List", "Employment Type", "List"),
- () => frappe.new_doc("Employment Type"),
- () => frappe.timeout(1),
- () => frappe.quick_entry.dialog.$wrapper.find('.edit-full').click(),
- () => frappe.timeout(1),
- () => cur_frm.set_value("employee_type_name", "Test Employment type"),
- // save form
- () => cur_frm.save(),
- () => frappe.timeout(1),
- () => assert.equal("Test Employment type", cur_frm.doc.employee_type_name,
- 'name of employment type correctly saved'),
- () => done()
- ]);
-});
diff --git a/erpnext/agriculture/__init__.py b/erpnext/hr/doctype/exit_interview/__init__.py
similarity index 100%
copy from erpnext/agriculture/__init__.py
copy to erpnext/hr/doctype/exit_interview/__init__.py
diff --git a/erpnext/hr/doctype/exit_interview/exit_interview.js b/erpnext/hr/doctype/exit_interview/exit_interview.js
new file mode 100644
index 0000000..502af42
--- /dev/null
+++ b/erpnext/hr/doctype/exit_interview/exit_interview.js
@@ -0,0 +1,38 @@
+// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Exit Interview', {
+ refresh: function(frm) {
+ if (!frm.doc.__islocal && !frm.doc.questionnaire_email_sent && frappe.boot.user.can_write.includes('Exit Interview')) {
+ frm.add_custom_button(__('Send Exit Questionnaire'), function () {
+ frm.trigger('send_exit_questionnaire');
+ });
+ }
+ },
+
+ employee: function(frm) {
+ frappe.db.get_value('Employee', frm.doc.employee, 'relieving_date', (message) => {
+ if (!message.relieving_date) {
+ frappe.throw({
+ message: __('Please set the relieving date for employee {0}',
+ ['<a href="/app/employee/' + frm.doc.employee +'">' + frm.doc.employee + '</a>']),
+ title: __('Relieving Date Missing')
+ });
+ }
+ });
+ },
+
+ send_exit_questionnaire: function(frm) {
+ frappe.call({
+ method: 'erpnext.hr.doctype.exit_interview.exit_interview.send_exit_questionnaire',
+ args: {
+ 'interviews': [frm.doc]
+ },
+ callback: function(r) {
+ if (!r.exc) {
+ frm.refresh_field('questionnaire_email_sent');
+ }
+ }
+ });
+ }
+});
diff --git a/erpnext/hr/doctype/exit_interview/exit_interview.json b/erpnext/hr/doctype/exit_interview/exit_interview.json
new file mode 100644
index 0000000..989a1b8
--- /dev/null
+++ b/erpnext/hr/doctype/exit_interview/exit_interview.json
@@ -0,0 +1,246 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "autoname": "naming_series:",
+ "creation": "2021-12-05 13:56:36.241690",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "email_append_to": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "naming_series",
+ "employee",
+ "employee_name",
+ "email",
+ "column_break_5",
+ "company",
+ "status",
+ "date",
+ "employee_details_section",
+ "department",
+ "designation",
+ "reports_to",
+ "column_break_9",
+ "date_of_joining",
+ "relieving_date",
+ "exit_questionnaire_section",
+ "ref_doctype",
+ "questionnaire_email_sent",
+ "column_break_10",
+ "reference_document_name",
+ "interview_summary_section",
+ "interviewers",
+ "interview_summary",
+ "employee_status_section",
+ "employee_status",
+ "amended_from"
+ ],
+ "fields": [
+ {
+ "fieldname": "employee",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Employee",
+ "options": "Employee",
+ "reqd": 1
+ },
+ {
+ "fetch_from": "employee.employee_name",
+ "fieldname": "employee_name",
+ "fieldtype": "Data",
+ "label": "Employee Name",
+ "read_only": 1
+ },
+ {
+ "fetch_from": "employee.department",
+ "fieldname": "department",
+ "fieldtype": "Link",
+ "label": "Department",
+ "options": "Department",
+ "read_only": 1
+ },
+ {
+ "fetch_from": "employee.relieving_date",
+ "fieldname": "relieving_date",
+ "fieldtype": "Date",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Relieving Date",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_5",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "in_standard_filter": 1,
+ "label": "Company",
+ "options": "Company",
+ "reqd": 1
+ },
+ {
+ "fieldname": "date",
+ "fieldtype": "Date",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Date",
+ "mandatory_depends_on": "eval:doc.status==='Scheduled';"
+ },
+ {
+ "fieldname": "exit_questionnaire_section",
+ "fieldtype": "Section Break",
+ "label": "Exit Questionnaire"
+ },
+ {
+ "fieldname": "ref_doctype",
+ "fieldtype": "Link",
+ "label": "Reference Document Type",
+ "options": "DocType"
+ },
+ {
+ "fieldname": "reference_document_name",
+ "fieldtype": "Dynamic Link",
+ "in_list_view": 1,
+ "label": "Reference Document Name",
+ "options": "ref_doctype"
+ },
+ {
+ "fieldname": "interview_summary_section",
+ "fieldtype": "Section Break",
+ "label": "Interview Details"
+ },
+ {
+ "fieldname": "column_break_10",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "interviewers",
+ "fieldtype": "Table MultiSelect",
+ "label": "Interviewers",
+ "mandatory_depends_on": "eval:doc.status==='Scheduled';",
+ "options": "Interviewer"
+ },
+ {
+ "fetch_from": "employee.date_of_joining",
+ "fieldname": "date_of_joining",
+ "fieldtype": "Date",
+ "label": "Date of Joining",
+ "read_only": 1
+ },
+ {
+ "fetch_from": "employee.reports_to",
+ "fieldname": "reports_to",
+ "fieldtype": "Link",
+ "in_standard_filter": 1,
+ "label": "Reports To",
+ "options": "Employee",
+ "read_only": 1
+ },
+ {
+ "fieldname": "employee_details_section",
+ "fieldtype": "Section Break",
+ "label": "Employee Details"
+ },
+ {
+ "fetch_from": "employee.designation",
+ "fieldname": "designation",
+ "fieldtype": "Link",
+ "label": "Designation",
+ "options": "Designation",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_9",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "naming_series",
+ "fieldtype": "Select",
+ "label": "Naming Series",
+ "options": "HR-EXIT-INT-"
+ },
+ {
+ "default": "0",
+ "fieldname": "questionnaire_email_sent",
+ "fieldtype": "Check",
+ "in_standard_filter": 1,
+ "label": "Questionnaire Email Sent",
+ "no_copy": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "email",
+ "fieldtype": "Data",
+ "label": "Email ID",
+ "options": "Email",
+ "read_only": 1
+ },
+ {
+ "fieldname": "status",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Status",
+ "options": "Pending\nScheduled\nCompleted\nCancelled",
+ "reqd": 1
+ },
+ {
+ "fieldname": "employee_status_section",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "employee_status",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Final Decision",
+ "mandatory_depends_on": "eval:doc.status==='Completed';",
+ "options": "\nEmployee Retained\nExit Confirmed"
+ },
+ {
+ "fieldname": "amended_from",
+ "fieldtype": "Link",
+ "label": "Amended From",
+ "no_copy": 1,
+ "options": "Exit Interview",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "interview_summary",
+ "fieldtype": "Text Editor",
+ "label": "Interview Summary"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "is_submittable": 1,
+ "links": [],
+ "modified": "2021-12-07 23:39:22.645401",
+ "modified_by": "Administrator",
+ "module": "HR",
+ "name": "Exit Interview",
+ "naming_rule": "By \"Naming Series\" field",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "sender_field": "email",
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "title_field": "employee_name",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/exit_interview/exit_interview.py b/erpnext/hr/doctype/exit_interview/exit_interview.py
new file mode 100644
index 0000000..30e19f1
--- /dev/null
+++ b/erpnext/hr/doctype/exit_interview/exit_interview.py
@@ -0,0 +1,131 @@
+# 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
+from frappe.utils import get_link_to_form
+
+from erpnext.hr.doctype.employee.employee import get_employee_email
+
+
+class ExitInterview(Document):
+ def validate(self):
+ self.validate_relieving_date()
+ self.validate_duplicate_interview()
+ self.set_employee_email()
+
+ def validate_relieving_date(self):
+ if not frappe.db.get_value('Employee', self.employee, 'relieving_date'):
+ frappe.throw(_('Please set the relieving date for employee {0}').format(
+ get_link_to_form('Employee', self.employee)),
+ title=_('Relieving Date Missing'))
+
+ def validate_duplicate_interview(self):
+ doc = frappe.db.exists('Exit Interview', {
+ 'employee': self.employee,
+ 'name': ('!=', self.name),
+ 'docstatus': ('!=', 2)
+ })
+ if doc:
+ frappe.throw(_('Exit Interview {0} already exists for Employee: {1}').format(
+ get_link_to_form('Exit Interview', doc), frappe.bold(self.employee)),
+ frappe.DuplicateEntryError)
+
+ def set_employee_email(self):
+ employee = frappe.get_doc('Employee', self.employee)
+ self.email = get_employee_email(employee)
+
+ def on_submit(self):
+ if self.status != 'Completed':
+ frappe.throw(_('Only Completed documents can be submitted'))
+
+ self.update_interview_date_in_employee()
+
+ def on_cancel(self):
+ self.update_interview_date_in_employee()
+ self.db_set('status', 'Cancelled')
+
+ def update_interview_date_in_employee(self):
+ if self.docstatus == 1:
+ frappe.db.set_value('Employee', self.employee, 'held_on', self.date)
+ elif self.docstatus == 2:
+ frappe.db.set_value('Employee', self.employee, 'held_on', None)
+
+
+@frappe.whitelist()
+def send_exit_questionnaire(interviews):
+ interviews = get_interviews(interviews)
+ validate_questionnaire_settings()
+
+ email_success = []
+ email_failure = []
+
+ for exit_interview in interviews:
+ interview = frappe.get_doc('Exit Interview', exit_interview.get('name'))
+ if interview.get('questionnaire_email_sent'):
+ continue
+
+ employee = frappe.get_doc('Employee', interview.employee)
+ email = get_employee_email(employee)
+
+ context = interview.as_dict()
+ context.update(employee.as_dict())
+ template_name = frappe.db.get_single_value('HR Settings', 'exit_questionnaire_notification_template')
+ template = frappe.get_doc('Email Template', template_name)
+
+ if email:
+ frappe.sendmail(
+ recipients=email,
+ subject=template.subject,
+ message=frappe.render_template(template.response, context),
+ reference_doctype=interview.doctype,
+ reference_name=interview.name
+ )
+ interview.db_set('questionnaire_email_sent', True)
+ interview.notify_update()
+ email_success.append(email)
+ else:
+ email_failure.append(get_link_to_form('Employee', employee.name))
+
+ show_email_summary(email_success, email_failure)
+
+
+def get_interviews(interviews):
+ import json
+
+ if isinstance(interviews, str):
+ interviews = json.loads(interviews)
+
+ if not len(interviews):
+ frappe.throw(_('Atleast one interview has to be selected.'))
+
+ return interviews
+
+
+def validate_questionnaire_settings():
+ settings = frappe.db.get_value('HR Settings', 'HR Settings',
+ ['exit_questionnaire_web_form', 'exit_questionnaire_notification_template'], as_dict=True)
+
+ if not settings.exit_questionnaire_web_form or not settings.exit_questionnaire_notification_template:
+ frappe.throw(
+ _('Please set {0} and {1} in {2}.').format(
+ frappe.bold('Exit Questionnaire Web Form'),
+ frappe.bold('Notification Template'),
+ get_link_to_form('HR Settings', 'HR Settings')),
+ title=_('Settings Missing')
+ )
+
+
+def show_email_summary(email_success, email_failure):
+ message = ''
+ if email_success:
+ message += _('{0}: {1}').format(
+ frappe.bold('Sent Successfully'), ', '.join(email_success))
+ if message and email_failure:
+ message += '<br><br>'
+ if email_failure:
+ message += _('{0} due to missing email information for employee(s): {1}').format(
+ frappe.bold('Sending Failed'), ', '.join(email_failure))
+
+ frappe.msgprint(message, title=_('Exit Questionnaire'), indicator='blue', is_minimizable=True, wide=True)
\ No newline at end of file
diff --git a/erpnext/hr/doctype/exit_interview/exit_interview_list.js b/erpnext/hr/doctype/exit_interview/exit_interview_list.js
new file mode 100644
index 0000000..93d7b21
--- /dev/null
+++ b/erpnext/hr/doctype/exit_interview/exit_interview_list.js
@@ -0,0 +1,27 @@
+frappe.listview_settings['Exit Interview'] = {
+ has_indicator_for_draft: 1,
+ get_indicator: function(doc) {
+ let status_color = {
+ 'Pending': 'orange',
+ 'Scheduled': 'yellow',
+ 'Completed': 'green',
+ 'Cancelled': 'red',
+ };
+ return [__(doc.status), status_color[doc.status], 'status,=,'+doc.status];
+ },
+
+ onload: function(listview) {
+ if (frappe.boot.user.can_write.includes('Exit Interview')) {
+ listview.page.add_action_item(__('Send Exit Questionnaires'), function() {
+ const interviews = listview.get_checked_items();
+ frappe.call({
+ method: 'erpnext.hr.doctype.exit_interview.exit_interview.send_exit_questionnaire',
+ freeze: true,
+ args: {
+ 'interviews': interviews
+ }
+ });
+ });
+ }
+ }
+};
diff --git a/erpnext/hr/doctype/exit_interview/exit_questionnaire_notification_template.html b/erpnext/hr/doctype/exit_interview/exit_questionnaire_notification_template.html
new file mode 100644
index 0000000..0317b1a
--- /dev/null
+++ b/erpnext/hr/doctype/exit_interview/exit_questionnaire_notification_template.html
@@ -0,0 +1,16 @@
+<h2>Exit Questionnaire</h2>
+<br>
+
+<p>
+ Dear {{ employee_name }},
+ <br><br>
+
+ Thank you for the contribution you have made during your time at {{ company }}. We value your opinion and welcome the feedback on your experience working with us.
+ Request you to take out a few minutes to fill up this Exit Questionnaire.
+
+ {% set web_form = frappe.db.get_value('HR Settings', 'HR Settings', 'exit_questionnaire_web_form') %}
+ {% set web_form_link = frappe.utils.get_url(uri=frappe.db.get_value('Web Form', web_form, 'route')) %}
+
+ <br><br>
+ <a class="btn btn-primary" href="{{ web_form_link }}" target="_blank">{{ _('Submit Now') }}</a>
+</p>
diff --git a/erpnext/hr/doctype/exit_interview/test_exit_interview.py b/erpnext/hr/doctype/exit_interview/test_exit_interview.py
new file mode 100644
index 0000000..8e076ed
--- /dev/null
+++ b/erpnext/hr/doctype/exit_interview/test_exit_interview.py
@@ -0,0 +1,118 @@
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+import os
+import unittest
+
+import frappe
+from frappe import _
+from frappe.core.doctype.user_permission.test_user_permission import create_user
+from frappe.tests.test_webform import create_custom_doctype, create_webform
+from frappe.utils import getdate
+
+from erpnext.hr.doctype.employee.test_employee import make_employee
+from erpnext.hr.doctype.exit_interview.exit_interview import send_exit_questionnaire
+
+
+class TestExitInterview(unittest.TestCase):
+ def setUp(self):
+ frappe.db.sql('delete from `tabExit Interview`')
+
+ def test_duplicate_interview(self):
+ employee = make_employee('employeeexitint1@example.com')
+ frappe.db.set_value('Employee', employee, 'relieving_date', getdate())
+ interview = create_exit_interview(employee)
+
+ doc = frappe.copy_doc(interview)
+ self.assertRaises(frappe.DuplicateEntryError, doc.save)
+
+ def test_relieving_date_validation(self):
+ employee = make_employee('employeeexitint2@example.com')
+ # unset relieving date
+ frappe.db.set_value('Employee', employee, 'relieving_date', None)
+
+ interview = create_exit_interview(employee, save=False)
+ self.assertRaises(frappe.ValidationError, interview.save)
+
+ # set relieving date
+ frappe.db.set_value('Employee', employee, 'relieving_date', getdate())
+ interview = create_exit_interview(employee)
+ self.assertTrue(interview.name)
+
+ def test_interview_date_updated_in_employee_master(self):
+ employee = make_employee('employeeexit3@example.com')
+ frappe.db.set_value('Employee', employee, 'relieving_date', getdate())
+
+ interview = create_exit_interview(employee)
+ interview.status = 'Completed'
+ interview.employee_status = 'Exit Confirmed'
+
+ # exit interview date updated on submit
+ interview.submit()
+ self.assertEqual(frappe.db.get_value('Employee', employee, 'held_on'), interview.date)
+
+ # exit interview reset on cancel
+ interview.reload()
+ interview.cancel()
+ self.assertEqual(frappe.db.get_value('Employee', employee, 'held_on'), None)
+
+ def test_send_exit_questionnaire(self):
+ create_custom_doctype()
+ create_webform()
+ template = create_notification_template()
+
+ webform = frappe.db.get_all('Web Form', limit=1)
+ frappe.db.set_value('HR Settings', 'HR Settings', {
+ 'exit_questionnaire_web_form': webform[0].name,
+ 'exit_questionnaire_notification_template': template
+ })
+
+ employee = make_employee('employeeexit3@example.com')
+ frappe.db.set_value('Employee', employee, 'relieving_date', getdate())
+
+ interview = create_exit_interview(employee)
+ send_exit_questionnaire([interview])
+
+ email_queue = frappe.db.get_all('Email Queue', ['name', 'message'], limit=1)
+ self.assertTrue('Subject: Exit Questionnaire Notification' in email_queue[0].message)
+
+ def tearDown(self):
+ frappe.db.rollback()
+
+
+def create_exit_interview(employee, save=True):
+ interviewer = create_user('test_exit_interviewer@example.com')
+
+ doc = frappe.get_doc({
+ 'doctype': 'Exit Interview',
+ 'employee': employee,
+ 'company': '_Test Company',
+ 'status': 'Pending',
+ 'date': getdate(),
+ 'interviewers': [{
+ 'interviewer': interviewer.name
+ }],
+ 'interview_summary': 'Test'
+ })
+
+ if save:
+ return doc.insert()
+ return doc
+
+
+def create_notification_template():
+ template = frappe.db.exists('Email Template', _('Exit Questionnaire Notification'))
+ if not template:
+ base_path = frappe.get_app_path('erpnext', 'hr', 'doctype')
+ response = frappe.read_file(os.path.join(base_path, 'exit_interview/exit_questionnaire_notification_template.html'))
+
+ template = frappe.get_doc({
+ 'doctype': 'Email Template',
+ 'name': _('Exit Questionnaire Notification'),
+ 'response': response,
+ 'subject': _('Exit Questionnaire Notification'),
+ 'owner': frappe.session.user,
+ }).insert(ignore_permissions=True)
+ template = template.name
+
+ return template
\ No newline at end of file
diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.js b/erpnext/hr/doctype/expense_claim/expense_claim.js
index 218e97d..0479457 100644
--- a/erpnext/hr/doctype/expense_claim/expense_claim.js
+++ b/erpnext/hr/doctype/expense_claim/expense_claim.js
@@ -171,7 +171,7 @@
['docstatus', '=', 1],
['employee', '=', frm.doc.employee],
['paid_amount', '>', 0],
- ['paid_amount', '>', 'claimed_amount']
+ ['status', '!=', 'Claimed']
]
};
});
@@ -389,7 +389,9 @@
sanctioned_amount: function(frm, cdt, cdn) {
cur_frm.cscript.calculate_total(frm.doc, cdt, cdn);
frm.trigger("get_taxes");
+ frm.trigger("calculate_grand_total");
},
+
cost_center: function(frm, cdt, cdn) {
erpnext.utils.copy_value_in_all_rows(frm.doc, cdt, cdn, "expenses", "cost_center");
}
diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.json b/erpnext/hr/doctype/expense_claim/expense_claim.json
index a268c15..45b78bf 100644
--- a/erpnext/hr/doctype/expense_claim/expense_claim.json
+++ b/erpnext/hr/doctype/expense_claim/expense_claim.json
@@ -379,11 +379,12 @@
"idx": 1,
"is_submittable": 1,
"links": [],
- "modified": "2021-05-04 05:35:12.040199",
+ "modified": "2021-11-22 16:26:57.787838",
"modified_by": "Administrator",
"module": "HR",
"name": "Expense Claim",
"name_case": "Title Case",
+ "naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
diff --git a/erpnext/hr/doctype/expense_claim/test_expense_claim.js b/erpnext/hr/doctype/expense_claim/test_expense_claim.js
deleted file mode 100644
index 2529fae..0000000
--- a/erpnext/hr/doctype/expense_claim/test_expense_claim.js
+++ /dev/null
@@ -1,44 +0,0 @@
-QUnit.module('hr');
-
-QUnit.test("Test: Expense Claim [HR]", function (assert) {
- assert.expect(3);
- let done = assert.async();
- let employee_name;
- let d;
- frappe.run_serially([
- // Creating Expense Claim
- () => frappe.set_route('List','Expense Claim','List'),
- () => frappe.timeout(0.3),
- () => frappe.click_button('New'),
- () => {
- cur_frm.set_value('is_paid',1),
- cur_frm.set_value('expenses',[]),
- d = frappe.model.add_child(cur_frm.doc,'Expense Claim Detail','expenses'),
- d.expense_date = '2017-08-01',
- d.expense_type = 'Test Expense Type 1',
- d.description = 'This is just to test Expense Claim',
- d.amount = 2000,
- d.sanctioned_amount=2000,
- refresh_field('expenses');
- },
- () => frappe.timeout(1),
- () => cur_frm.set_value('employee','Test Employee 1'),
- () => cur_frm.set_value('company','For Testing'),
- () => cur_frm.set_value('payable_account','Creditors - FT'),
- () => cur_frm.set_value('cost_center','Main - FT'),
- () => cur_frm.set_value('mode_of_payment','Cash'),
- () => cur_frm.save(),
- () => frappe.click_button('Submit'),
- () => frappe.click_button('Yes'),
- () => frappe.timeout(3),
-
- // Checking if the amount is correctly reimbursed for the employee
- () => {
- assert.equal("Test Employee 1",cur_frm.doc.employee, 'Employee name set correctly');
- assert.equal(1, cur_frm.doc.is_paid, 'Expense is paid as required');
- assert.equal(2000, cur_frm.doc.total_amount_reimbursed, 'Amount is reimbursed correctly');
-
- },
- () => done()
- ]);
-});
diff --git a/erpnext/hr/doctype/expense_claim/test_expense_claim.py b/erpnext/hr/doctype/expense_claim/test_expense_claim.py
index ec70361..2a07920 100644
--- a/erpnext/hr/doctype/expense_claim/test_expense_claim.py
+++ b/erpnext/hr/doctype/expense_claim/test_expense_claim.py
@@ -10,15 +10,17 @@
from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.hr.doctype.expense_claim.expense_claim import make_bank_entry
-test_records = frappe.get_test_records('Expense Claim')
test_dependencies = ['Employee']
-company_name = '_Test Company 4'
+company_name = '_Test Company 3'
class TestExpenseClaim(unittest.TestCase):
+ def tearDown(self):
+ frappe.db.rollback()
+
def test_total_expense_claim_for_project(self):
- frappe.db.sql("""delete from `tabTask` where project = "_Test Project 1" """)
- frappe.db.sql("""delete from `tabProject` where name = "_Test Project 1" """)
+ frappe.db.sql("""delete from `tabTask`""")
+ frappe.db.sql("""delete from `tabProject`""")
frappe.db.sql("update `tabExpense Claim` set project = '', task = ''")
project = frappe.get_doc({
@@ -37,12 +39,12 @@
task_name = task.name
payable_account = get_payable_account(company_name)
- make_expense_claim(payable_account, 300, 200, company_name, "Travel Expenses - _TC4", project.name, task_name)
+ make_expense_claim(payable_account, 300, 200, company_name, "Travel Expenses - _TC3", project.name, task_name)
self.assertEqual(frappe.db.get_value("Task", task_name, "total_expense_claim"), 200)
self.assertEqual(frappe.db.get_value("Project", project.name, "total_expense_claim"), 200)
- expense_claim2 = make_expense_claim(payable_account, 600, 500, company_name, "Travel Expenses - _TC4", project.name, task_name)
+ expense_claim2 = make_expense_claim(payable_account, 600, 500, company_name, "Travel Expenses - _TC3", project.name, task_name)
self.assertEqual(frappe.db.get_value("Task", task_name, "total_expense_claim"), 700)
self.assertEqual(frappe.db.get_value("Project", project.name, "total_expense_claim"), 700)
@@ -54,7 +56,7 @@
def test_expense_claim_status(self):
payable_account = get_payable_account(company_name)
- 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 - _TC3")
je_dict = make_bank_entry("Expense Claim", expense_claim.name)
je = frappe.get_doc(je_dict)
@@ -73,7 +75,7 @@
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 - _TC3",
do_not_submit=True, taxes=taxes)
expense_claim.submit()
@@ -84,9 +86,9 @@
self.assertTrue(gl_entries)
expected_values = dict((d[0], d) for d in [
- ['Output Tax CGST - _TC4',18.0, 0.0],
+ ['Output Tax CGST - _TC3',18.0, 0.0],
[payable_account, 0.0, 218.0],
- ["Travel Expenses - _TC4", 200.0, 0.0]
+ ["Travel Expenses - _TC3", 200.0, 0.0]
])
for gle in gl_entries:
@@ -102,7 +104,7 @@
"payable_account": payable_account,
"approval_status": "Rejected",
"expenses":
- [{ "expense_type": "Travel", "default_account": "Travel Expenses - _TC4", "amount": 300, "sanctioned_amount": 200 }]
+ [{"expense_type": "Travel", "default_account": "Travel Expenses - _TC3", "amount": 300, "sanctioned_amount": 200}]
})
expense_claim.submit()
diff --git a/erpnext/hr/doctype/expense_claim/test_records.json b/erpnext/hr/doctype/expense_claim/test_records.json
deleted file mode 100644
index fe51488..0000000
--- a/erpnext/hr/doctype/expense_claim/test_records.json
+++ /dev/null
@@ -1 +0,0 @@
-[]
diff --git a/erpnext/hr/doctype/expense_claim_advance/expense_claim_advance.json b/erpnext/hr/doctype/expense_claim_advance/expense_claim_advance.json
index 4550925..aa479c8 100644
--- a/erpnext/hr/doctype/expense_claim_advance/expense_claim_advance.json
+++ b/erpnext/hr/doctype/expense_claim_advance/expense_claim_advance.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"creation": "2017-10-09 16:53:26.410762",
"doctype": "DocType",
"document_type": "Document",
@@ -50,7 +51,7 @@
"fieldname": "unclaimed_amount",
"fieldtype": "Currency",
"in_list_view": 1,
- "label": "Unclaimed amount",
+ "label": "Unclaimed Amount",
"no_copy": 1,
"oldfieldname": "advance_amount",
"oldfieldtype": "Currency",
@@ -65,7 +66,7 @@
"fieldname": "allocated_amount",
"fieldtype": "Currency",
"in_list_view": 1,
- "label": "Allocated amount",
+ "label": "Allocated Amount",
"no_copy": 1,
"oldfieldname": "allocated_amount",
"oldfieldtype": "Currency",
@@ -87,7 +88,7 @@
],
"istable": 1,
"links": [],
- "modified": "2019-12-17 13:53:22.111766",
+ "modified": "2021-11-22 16:33:58.515819",
"modified_by": "Administrator",
"module": "HR",
"name": "Expense Claim Advance",
diff --git a/erpnext/hr/doctype/expense_claim_detail/expense_claim_detail.json b/erpnext/hr/doctype/expense_claim_detail/expense_claim_detail.json
index 70a48f9..6edbcb5c 100644
--- a/erpnext/hr/doctype/expense_claim_detail/expense_claim_detail.json
+++ b/erpnext/hr/doctype/expense_claim_detail/expense_claim_detail.json
@@ -94,7 +94,6 @@
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Sanctioned Amount",
- "no_copy": 1,
"oldfieldname": "sanctioned_amount",
"oldfieldtype": "Currency",
"options": "Company:company:default_currency",
@@ -120,7 +119,7 @@
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2020-09-18 17:26:09.703215",
+ "modified": "2021-11-26 14:23:45.539922",
"modified_by": "Administrator",
"module": "HR",
"name": "Expense Claim Detail",
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
deleted file mode 100644
index 3c9ed35..0000000
--- a/erpnext/hr/doctype/expense_claim_type/test_expense_claim_type.js
+++ /dev/null
@@ -1,29 +0,0 @@
-QUnit.module('hr');
-
-QUnit.test("Test: Expense Claim Type [HR]", function (assert) {
- assert.expect(1);
- let done = assert.async();
- frappe.run_serially([
- // Creating a Expense Claim Type
- () => {
- frappe.tests.make('Expense Claim Type', [
- { expense_type: 'Test Expense Type 1'},
- { description:'This is just a test'},
- { accounts: [
- [
- { company: 'For Testing'},
- { default_account: 'Rounded Off - FT'}
- ]
- ]},
- ]);
- },
- () => frappe.timeout(5),
-
- // Checking if the created type is present in the list
- () => {
- assert.equal('Test Expense Type 1', cur_frm.doc.expense_type,
- 'Expense Claim Type created successfully');
- },
- () => done()
- ]);
-});
diff --git a/erpnext/hr/doctype/holiday_list/test_holiday_list.js b/erpnext/hr/doctype/holiday_list/test_holiday_list.js
deleted file mode 100644
index ce76614..0000000
--- a/erpnext/hr/doctype/holiday_list/test_holiday_list.js
+++ /dev/null
@@ -1,42 +0,0 @@
-QUnit.module('hr');
-
-QUnit.test("Test: Holiday list [HR]", function (assert) {
- assert.expect(3);
- let done = assert.async();
- let date = frappe.datetime.add_months(frappe.datetime.nowdate(), -2); // date 2 months from now
-
- frappe.run_serially([
- // test holiday list creation
- () => frappe.set_route("List", "Holiday List", "List"),
- () => frappe.new_doc("Holiday List"),
- () => frappe.timeout(1),
- () => cur_frm.set_value("holiday_list_name", "Test Holiday list"),
- () => cur_frm.set_value("from_date", date),
- () => cur_frm.set_value("weekly_off", "Sunday"), // holiday list for sundays
- () => frappe.click_button('Get Weekly Off Dates'),
-
- // save form
- () => cur_frm.save(),
- () => frappe.timeout(1),
- () => assert.equal("Test Holiday list", cur_frm.doc.holiday_list_name,
- 'name of holiday list correctly saved'),
-
- // check if holiday list contains correct days
- () => {
- var list = cur_frm.doc.holidays;
- var list_length = list.length;
- var i = 0;
- for ( ; i < list_length; i++)
- if (list[i].description != 'Sunday') break;
- assert.equal(list_length, i, "all holidays are sundays in holiday list");
- },
-
- // check if to_date is set one year from from_date
- () => {
- var date_year_later = frappe.datetime.add_days(frappe.datetime.add_months(date, 12), -1); // date after one year
- assert.equal(date_year_later, cur_frm.doc.to_date,
- "to date set correctly");
- },
- () => done()
- ]);
-});
diff --git a/erpnext/hr/doctype/hr_settings/hr_settings.json b/erpnext/hr/doctype/hr_settings/hr_settings.json
index 5148435..f9a3e05 100644
--- a/erpnext/hr/doctype/hr_settings/hr_settings.json
+++ b/erpnext/hr/doctype/hr_settings/hr_settings.json
@@ -36,7 +36,11 @@
"remind_before",
"column_break_4",
"send_interview_feedback_reminder",
- "feedback_reminder_notification_template"
+ "feedback_reminder_notification_template",
+ "employee_exit_section",
+ "exit_questionnaire_web_form",
+ "column_break_34",
+ "exit_questionnaire_notification_template"
],
"fields": [
{
@@ -226,13 +230,34 @@
"fieldname": "check_vacancies",
"fieldtype": "Check",
"label": "Check Vacancies On Job Offer Creation"
+ },
+ {
+ "fieldname": "employee_exit_section",
+ "fieldtype": "Section Break",
+ "label": "Employee Exit Settings"
+ },
+ {
+ "fieldname": "exit_questionnaire_web_form",
+ "fieldtype": "Link",
+ "label": "Exit Questionnaire Web Form",
+ "options": "Web Form"
+ },
+ {
+ "fieldname": "exit_questionnaire_notification_template",
+ "fieldtype": "Link",
+ "label": "Exit Questionnaire Notification Template",
+ "options": "Email Template"
+ },
+ {
+ "fieldname": "column_break_34",
+ "fieldtype": "Column Break"
}
],
"icon": "fa fa-cog",
"idx": 1,
"issingle": 1,
"links": [],
- "modified": "2021-10-01 23:46:11.098236",
+ "modified": "2021-12-05 14:48:10.884253",
"modified_by": "Administrator",
"module": "HR",
"name": "HR Settings",
diff --git a/erpnext/hr/doctype/interview_feedback/test_interview_feedback.py b/erpnext/hr/doctype/interview_feedback/test_interview_feedback.py
index 4185f28..d2ec5b9 100644
--- a/erpnext/hr/doctype/interview_feedback/test_interview_feedback.py
+++ b/erpnext/hr/doctype/interview_feedback/test_interview_feedback.py
@@ -59,7 +59,7 @@
}, 'average_rating')
# 1. average should be reflected in Interview Detail.
- self.assertEqual(avg_on_interview_detail, round(feedback_1.average_rating))
+ self.assertEqual(avg_on_interview_detail, feedback_1.average_rating)
'''For Second Interviewer Feedback'''
interviewer = interview.interview_details[1].interviewer
diff --git a/erpnext/hr/doctype/job_applicant/test_job_applicant.js b/erpnext/hr/doctype/job_applicant/test_job_applicant.js
deleted file mode 100644
index 741a182..0000000
--- a/erpnext/hr/doctype/job_applicant/test_job_applicant.js
+++ /dev/null
@@ -1,28 +0,0 @@
-QUnit.module('hr');
-
-QUnit.test("Test: Job Opening [HR]", function (assert) {
- assert.expect(2);
- let done = assert.async();
-
- frappe.run_serially([
- // Job Applicant creation
- () => {
- frappe.tests.make('Job Applicant', [
- { applicant_name: 'Utkarsh Goswami'},
- { email_id: 'goswamiutkarsh0@gmail.com'},
- { job_title: 'software-developer'},
- { cover_letter: 'Highly skilled in designing, testing, and developing software.'+
- ' This is just a test.'}
- ]);
- },
- () => frappe.timeout(4),
- () => frappe.set_route('List','Job Applicant'),
- () => frappe.timeout(3),
- () => {
- assert.ok(cur_list.data.length==1, 'Job Applicant created successfully');
- assert.ok(cur_list.data[0].name=='Utkarsh Goswami - goswamiutkarsh0@gmail.com - software-developer',
- 'Correct job applicant with valid job title');
- },
- () => done()
- ]);
-});
diff --git a/erpnext/hr/doctype/job_offer/test_job_offer.js b/erpnext/hr/doctype/job_offer/test_job_offer.js
deleted file mode 100644
index 5339b9c..0000000
--- a/erpnext/hr/doctype/job_offer/test_job_offer.js
+++ /dev/null
@@ -1,51 +0,0 @@
-QUnit.module('hr');
-
-QUnit.test("Test: Job Offer [HR]", function (assert) {
- assert.expect(3);
- let done = assert.async();
- frappe.run_serially([
- // Job Offer Creation
- () => {
- frappe.tests.make('Job Offer', [
- { job_applicant: 'Utkarsh Goswami - goswamiutkarsh0@gmail.com - software-developer'},
- { applicant_name: 'Utkarsh Goswami'},
- { status: 'Accepted'},
- { designation: 'Software Developer'},
- { offer_terms: [
- [
- {offer_term: 'Responsibilities'},
- {value: 'Design, installation, testing and maintenance of software systems.'}
- ],
- [
- {offer_term: 'Department'},
- {value: 'Research & Development'}
- ],
- [
- {offer_term: 'Probationary Period'},
- {value: 'The Probation period is for 3 months.'}
- ]
- ]},
- ]);
- },
- () => frappe.timeout(10),
- () => frappe.click_button('Submit'),
- () => frappe.timeout(2),
- () => frappe.click_button('Yes'),
- () => frappe.timeout(5),
- // To check if the fields are correctly set
- () => {
- assert.ok(cur_frm.get_field('status').value=='Accepted',
- 'Status of job offer is correct');
- assert.ok(cur_frm.get_field('designation').value=='Software Developer',
- 'Designation of applicant is correct');
- },
- () => frappe.set_route('List','Job Offer','List'),
- () => frappe.timeout(2),
- // Checking the submission of and Job Offer
- () => {
- assert.ok(cur_list.data[0].docstatus==1,'Job Offer Submitted successfully');
- },
- () => frappe.timeout(2),
- () => done()
- ]);
-});
diff --git a/erpnext/hr/doctype/job_opening/test_job_opening.js b/erpnext/hr/doctype/job_opening/test_job_opening.js
deleted file mode 100644
index cc2f027..0000000
--- a/erpnext/hr/doctype/job_opening/test_job_opening.js
+++ /dev/null
@@ -1,26 +0,0 @@
-QUnit.module('hr');
-
-QUnit.test("Test: Job Opening [HR]", function (assert) {
- assert.expect(2);
- let done = assert.async();
-
- frappe.run_serially([
- // Job Opening creation
- () => {
- frappe.tests.make('Job Opening', [
- { job_title: 'Software Developer'},
- { description:
- 'You might be responsible for writing and coding individual'+
- ' programmes or providing an entirely new software resource.'}
- ]);
- },
- () => frappe.timeout(4),
- () => frappe.set_route('List','Job Opening'),
- () => frappe.timeout(3),
- () => {
- assert.ok(cur_list.data.length==1, 'Job Opening created successfully');
- assert.ok(cur_list.data[0].job_title=='Software Developer', 'Job title Correctly set');
- },
- () => done()
- ]);
-});
diff --git a/erpnext/hr/doctype/leave_allocation/test_leave_allocation.js b/erpnext/hr/doctype/leave_allocation/test_leave_allocation.js
deleted file mode 100644
index d5364fc..0000000
--- a/erpnext/hr/doctype/leave_allocation/test_leave_allocation.js
+++ /dev/null
@@ -1,41 +0,0 @@
-QUnit.module('hr');
-
-QUnit.test("Test: Leave allocation [HR]", function (assert) {
- assert.expect(3);
- let done = assert.async();
- let today_date = frappe.datetime.nowdate();
-
- frappe.run_serially([
- // test creating leave alloction
- () => frappe.set_route("List", "Leave Allocation", "List"),
- () => frappe.new_doc("Leave Allocation"),
- () => frappe.timeout(1),
- () => {
- frappe.db.get_value('Employee', {'employee_name':'Test Employee 1'}, 'name', function(r) {
- cur_frm.set_value("employee", r.name)
- });
- },
- () => frappe.timeout(1),
- () => cur_frm.set_value("leave_type", "Test Leave type"),
- () => cur_frm.set_value("to_date", frappe.datetime.add_months(today_date, 2)), // for two months
- () => cur_frm.set_value("description", "This is just for testing"),
- () => cur_frm.set_value("new_leaves_allocated", 2),
- () => frappe.click_check('Add unused leaves from previous allocations'),
- // save form
- () => cur_frm.save(),
- () => frappe.timeout(1),
- () => cur_frm.savesubmit(),
- () => frappe.timeout(1),
- () => assert.equal("Confirm", cur_dialog.title,
- 'confirmation for leave alloction shown'),
- () => frappe.click_button('Yes'),
- () => frappe.timeout(1),
- // check auto filled from date
- () => assert.equal(today_date, cur_frm.doc.from_date,
- "from date correctly set"),
- // check for total leaves
- () => assert.equal(cur_frm.doc.unused_leaves + 2, cur_frm.doc.total_leaves_allocated,
- "total leave calculation is correctly set"),
- () => done()
- ]);
-});
diff --git a/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py b/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py
index 46401a2..1fe9139 100644
--- a/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py
+++ b/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py
@@ -4,6 +4,7 @@
from frappe.utils import add_days, add_months, getdate, nowdate
import erpnext
+from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import process_expired_allocation
from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type
@@ -13,16 +14,19 @@
def setUpClass(cls):
frappe.db.sql("delete from `tabLeave Period`")
- def test_overlapping_allocation(self):
- frappe.db.sql("delete from `tabLeave Allocation`")
+ emp_id = make_employee("test_emp_leave_allocation@salary.com")
+ cls.employee = frappe.get_doc("Employee", emp_id)
- employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0])
+ def tearDown(self):
+ frappe.db.rollback()
+
+ def test_overlapping_allocation(self):
leaves = [
{
"doctype": "Leave Allocation",
"__islocal": 1,
- "employee": employee.name,
- "employee_name": employee.employee_name,
+ "employee": self.employee.name,
+ "employee_name": self.employee.employee_name,
"leave_type": "_Test Leave Type",
"from_date": getdate("2015-10-01"),
"to_date": getdate("2015-10-31"),
@@ -32,8 +36,8 @@
{
"doctype": "Leave Allocation",
"__islocal": 1,
- "employee": employee.name,
- "employee_name": employee.employee_name,
+ "employee": self.employee.name,
+ "employee_name": self.employee.employee_name,
"leave_type": "_Test Leave Type",
"from_date": getdate("2015-09-01"),
"to_date": getdate("2015-11-30"),
@@ -45,40 +49,36 @@
self.assertRaises(frappe.ValidationError, frappe.get_doc(leaves[1]).save)
def test_invalid_period(self):
- employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0])
-
doc = frappe.get_doc({
"doctype": "Leave Allocation",
"__islocal": 1,
- "employee": employee.name,
- "employee_name": employee.employee_name,
+ "employee": self.employee.name,
+ "employee_name": self.employee.employee_name,
"leave_type": "_Test Leave Type",
"from_date": getdate("2015-09-30"),
"to_date": getdate("2015-09-1"),
"new_leaves_allocated": 5
})
- #invalid period
+ # invalid period
self.assertRaises(frappe.ValidationError, doc.save)
def test_allocated_leave_days_over_period(self):
- employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0])
doc = frappe.get_doc({
"doctype": "Leave Allocation",
"__islocal": 1,
- "employee": employee.name,
- "employee_name": employee.employee_name,
+ "employee": self.employee.name,
+ "employee_name": self.employee.employee_name,
"leave_type": "_Test Leave Type",
"from_date": getdate("2015-09-1"),
"to_date": getdate("2015-09-30"),
"new_leaves_allocated": 35
})
- #allocated leave more than period
+
+ # allocated leave more than period
self.assertRaises(frappe.ValidationError, doc.save)
def test_carry_forward_calculation(self):
- frappe.db.sql("delete from `tabLeave Allocation`")
- frappe.db.sql("delete from `tabLeave Ledger Entry`")
leave_type = create_leave_type(leave_type_name="_Test_CF_leave", is_carry_forward=1)
leave_type.maximum_carry_forwarded_leaves = 10
leave_type.max_leaves_allowed = 30
@@ -86,6 +86,8 @@
# initial leave allocation = 15
leave_allocation = create_leave_allocation(
+ employee=self.employee.name,
+ employee_name=self.employee.employee_name,
leave_type="_Test_CF_leave",
from_date=add_months(nowdate(), -12),
to_date=add_months(nowdate(), -1),
@@ -95,6 +97,8 @@
# carry forwarded leaves considering maximum_carry_forwarded_leaves
# new_leaves = 15, carry_forwarded = 10
leave_allocation_1 = create_leave_allocation(
+ employee=self.employee.name,
+ employee_name=self.employee.employee_name,
leave_type="_Test_CF_leave",
carry_forward=1)
leave_allocation_1.submit()
@@ -106,6 +110,8 @@
# carry forwarded leaves considering max_leave_allowed
# max_leave_allowed = 30, new_leaves = 25, carry_forwarded = 5
leave_allocation_2 = create_leave_allocation(
+ employee=self.employee.name,
+ employee_name=self.employee.employee_name,
leave_type="_Test_CF_leave",
carry_forward=1,
new_leaves_allocated=25)
@@ -114,8 +120,6 @@
self.assertEqual(leave_allocation_2.unused_leaves, 5)
def test_carry_forward_leaves_expiry(self):
- frappe.db.sql("delete from `tabLeave Allocation`")
- frappe.db.sql("delete from `tabLeave Ledger Entry`")
leave_type = create_leave_type(
leave_type_name="_Test_CF_leave_expiry",
is_carry_forward=1,
@@ -124,6 +128,8 @@
# initial leave allocation
leave_allocation = create_leave_allocation(
+ employee=self.employee.name,
+ employee_name=self.employee.employee_name,
leave_type="_Test_CF_leave_expiry",
from_date=add_months(nowdate(), -24),
to_date=add_months(nowdate(), -12),
@@ -131,6 +137,8 @@
leave_allocation.submit()
leave_allocation = create_leave_allocation(
+ employee=self.employee.name,
+ employee_name=self.employee.employee_name,
leave_type="_Test_CF_leave_expiry",
from_date=add_days(nowdate(), -90),
to_date=add_days(nowdate(), 100),
@@ -142,6 +150,8 @@
# leave allocation with carry forward of only new leaves allocated
leave_allocation_1 = create_leave_allocation(
+ employee=self.employee.name,
+ employee_name=self.employee.employee_name,
leave_type="_Test_CF_leave_expiry",
carry_forward=1,
from_date=add_months(nowdate(), 6),
@@ -151,9 +161,10 @@
self.assertEqual(leave_allocation_1.unused_leaves, leave_allocation.new_leaves_allocated)
def test_creation_of_leave_ledger_entry_on_submit(self):
- frappe.db.sql("delete from `tabLeave Allocation`")
-
- leave_allocation = create_leave_allocation()
+ leave_allocation = create_leave_allocation(
+ employee=self.employee.name,
+ employee_name=self.employee.employee_name
+ )
leave_allocation.submit()
leave_ledger_entry = frappe.get_all('Leave Ledger Entry', fields='*', filters=dict(transaction_name=leave_allocation.name))
@@ -168,10 +179,10 @@
self.assertFalse(frappe.db.exists("Leave Ledger Entry", {'transaction_name':leave_allocation.name}))
def test_leave_addition_after_submit(self):
- frappe.db.sql("delete from `tabLeave Allocation`")
- frappe.db.sql("delete from `tabLeave Ledger Entry`")
-
- leave_allocation = create_leave_allocation()
+ leave_allocation = create_leave_allocation(
+ employee=self.employee.name,
+ employee_name=self.employee.employee_name
+ )
leave_allocation.submit()
self.assertTrue(leave_allocation.total_leaves_allocated, 15)
leave_allocation.new_leaves_allocated = 40
@@ -179,44 +190,55 @@
self.assertTrue(leave_allocation.total_leaves_allocated, 40)
def test_leave_subtraction_after_submit(self):
- frappe.db.sql("delete from `tabLeave Allocation`")
- frappe.db.sql("delete from `tabLeave Ledger Entry`")
- leave_allocation = create_leave_allocation()
+ leave_allocation = create_leave_allocation(
+ employee=self.employee.name,
+ employee_name=self.employee.employee_name
+ )
leave_allocation.submit()
self.assertTrue(leave_allocation.total_leaves_allocated, 15)
leave_allocation.new_leaves_allocated = 10
leave_allocation.submit()
self.assertTrue(leave_allocation.total_leaves_allocated, 10)
- def test_against_leave_application_validation_after_submit(self):
- frappe.db.sql("delete from `tabLeave Allocation`")
- frappe.db.sql("delete from `tabLeave Ledger Entry`")
+ def test_validation_against_leave_application_after_submit(self):
+ from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list
- leave_allocation = create_leave_allocation()
+ make_holiday_list()
+ frappe.db.set_value("Company", self.employee.company, "default_holiday_list", "Salary Slip Test Holiday List")
+
+ leave_allocation = create_leave_allocation(
+ employee=self.employee.name,
+ employee_name=self.employee.employee_name
+ )
leave_allocation.submit()
self.assertTrue(leave_allocation.total_leaves_allocated, 15)
- employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0])
+
leave_application = frappe.get_doc({
"doctype": 'Leave Application',
- "employee": employee.name,
+ "employee": self.employee.name,
"leave_type": "_Test Leave Type",
"from_date": add_months(nowdate(), 2),
"to_date": add_months(add_days(nowdate(), 10), 2),
- "company": erpnext.get_default_company() or "_Test Company",
+ "company": self.employee.company,
"docstatus": 1,
"status": "Approved",
"leave_approver": 'test@example.com'
})
leave_application.submit()
- leave_allocation.new_leaves_allocated = 8
- leave_allocation.total_leaves_allocated = 8
+ leave_application.reload()
+
+ # allocate less leaves than the ones which are already approved
+ leave_allocation.new_leaves_allocated = leave_application.total_leave_days - 1
+ leave_allocation.total_leaves_allocated = leave_application.total_leave_days - 1
self.assertRaises(frappe.ValidationError, leave_allocation.submit)
def create_leave_allocation(**args):
args = frappe._dict(args)
- employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0])
- leave_allocation = frappe.get_doc({
+ emp_id = make_employee("test_emp_leave_allocation@salary.com")
+ employee = frappe.get_doc("Employee", emp_id)
+
+ return frappe.get_doc({
"doctype": "Leave Allocation",
"__islocal": 1,
"employee": args.employee or employee.name,
@@ -227,6 +249,5 @@
"carry_forward": args.carry_forward or 0,
"to_date": args.to_date or add_months(nowdate(), 12)
})
- return leave_allocation
test_dependencies = ["Employee", "Leave Type"]
diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.js b/erpnext/hr/doctype/leave_application/test_leave_application.js
deleted file mode 100644
index 0866b0b..0000000
--- a/erpnext/hr/doctype/leave_application/test_leave_application.js
+++ /dev/null
@@ -1,42 +0,0 @@
-QUnit.module('hr');
-
-QUnit.test("Test: Leave application [HR]", function (assert) {
- assert.expect(4);
- let done = assert.async();
- let today_date = frappe.datetime.nowdate();
- let leave_date = frappe.datetime.add_days(today_date, 1); // leave for tomorrow
-
- frappe.run_serially([
- // test creating leave application
- () => frappe.db.get_value('Employee', {'employee_name':'Test Employee 1'}, 'name'),
- (employee) => {
- return frappe.tests.make('Leave Application', [
- {leave_type: "Test Leave type"},
- {from_date: leave_date}, // for today
- {to_date: leave_date},
- {half_day: 1},
- {employee: employee.message.name},
- {follow_via_email: 0}
- ]);
- },
-
- () => frappe.timeout(1),
- () => frappe.click_button('Actions'),
- () => frappe.click_link('Approve'), // approve the application [as administrator]
- () => frappe.click_button('Yes'),
- () => frappe.timeout(1),
- () => assert.ok(cur_frm.doc.docstatus,
- "leave application submitted after approval"),
-
- // check auto filled posting date [today]
-
- () => assert.equal(today_date, cur_frm.doc.posting_date,
- "posting date correctly set"),
- () => frappe.set_route("List", "Leave Application", "List"),
- () => frappe.timeout(1),
- // // check approved application in list
- () => assert.deepEqual(["Test Employee 1", 1], [cur_list.data[0].employee_name, cur_list.data[0].docstatus]),
- // "leave for correct employee is submitted"),
- () => done()
- ]);
-});
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
deleted file mode 100644
index b39601b..0000000
--- a/erpnext/hr/doctype/leave_block_list/test_leave_block_list.js
+++ /dev/null
@@ -1,27 +0,0 @@
-QUnit.module('hr');
-
-QUnit.test("Test: Leave block list [HR]", function (assert) {
- assert.expect(1);
- let done = assert.async();
- let today_date = frappe.datetime.nowdate();
-
- frappe.run_serially([
- // test leave block list creation
- () => frappe.set_route("List", "Leave Block List", "List"),
- () => frappe.new_doc("Leave Block List"),
- () => frappe.timeout(1),
- () => cur_frm.set_value("leave_block_list_name", "Test Leave block list"),
- () => cur_frm.set_value("company", "For Testing"),
- () => frappe.click_button('Add Row'),
- () => {
- cur_frm.fields_dict.leave_block_list_dates.grid.grid_rows[0].doc.block_date = today_date;
- cur_frm.fields_dict.leave_block_list_dates.grid.grid_rows[0].doc.reason = "Blocked leave test";
- },
- // save form
- () => cur_frm.save(),
- () => frappe.timeout(1),
- () => assert.equal("Test Leave block list", cur_frm.doc.leave_block_list_name,
- 'name of blocked leave list correctly saved'),
- () => done()
- ]);
-});
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
deleted file mode 100644
index 9d37327..0000000
--- a/erpnext/hr/doctype/leave_control_panel/test_leave_control_panel.js
+++ /dev/null
@@ -1,50 +0,0 @@
-QUnit.module('hr');
-
-QUnit.test("Test: Leave control panel [HR]", function (assert) {
- assert.expect(2);
- let done = assert.async();
- let today_date = frappe.datetime.nowdate();
-
- frappe.run_serially([
- // test leave allocation using leave control panel
- () => frappe.set_route("Form", "Leave Control Panel"),
- () => frappe.timeout(1),
- () => cur_frm.set_value("leave_type", "Test Leave type"),
- () => cur_frm.set_value("company", "For Testing"),
- () => cur_frm.set_value("employment_type", "Test Employment Type"),
- () => cur_frm.set_value("branch", "Test Branch"),
- () => cur_frm.set_value("department", "Test Department"),
- () => cur_frm.set_value("designation", "Test Designation"),
- () => cur_frm.set_value("from_date", frappe.datetime.add_months(today_date, -2)),
- () => cur_frm.set_value("to_date", frappe.datetime.add_days(today_date, -1)), // for two months [not today]
- () => cur_frm.set_value("no_of_days", 3),
- // allocate leaves
- () => frappe.click_button('Allocate'),
- () => frappe.timeout(1),
- () => assert.equal("Message", cur_dialog.title, "leave alloction message shown"),
- () => frappe.click_button('Close'),
- () => frappe.set_route("List", "Leave Allocation", "List"),
- () => frappe.timeout(1),
- () => {
- return frappe.call({
- method: "frappe.client.get_list",
- args: {
- doctype: "Employee",
- filters: {
- "branch": "Test Branch",
- "department": "Test Department",
- "company": "For Testing",
- "designation": "Test Designation",
- "status": "Active"
- }
- },
- callback: function(r) {
- let leave_allocated = cur_list.data.filter(d => d.leave_type == "Test Leave type");
- assert.equal(r.message.length, leave_allocated.length,
- 'leave allocation successfully done for all the employees');
- }
- });
- },
- () => done()
- ]);
-});
diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py
index dca7e48..355370f 100644
--- a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py
+++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py
@@ -56,9 +56,7 @@
leave_policy_detail.leave_type, leave_policy_detail.annual_allocation,
leave_type_details, date_of_joining
)
-
- leave_allocations[leave_policy_detail.leave_type] = {"name": leave_allocation, "leaves": new_leaves_allocated}
-
+ leave_allocations[leave_policy_detail.leave_type] = {"name": leave_allocation, "leaves": new_leaves_allocated}
self.db_set("leaves_allocated", 1)
return leave_allocations
@@ -130,6 +128,8 @@
monthly_earned_leave = get_monthly_earned_leave(new_leaves_allocated,
leave_type_details.get(leave_type).earned_leave_frequency, leave_type_details.get(leave_type).rounding)
new_leaves_allocated = monthly_earned_leave * months_passed
+ else:
+ new_leaves_allocated = 0
return new_leaves_allocated
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 b1861ad..8953a51 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
@@ -4,6 +4,7 @@
import unittest
import frappe
+from frappe.utils import add_months, get_first_day, getdate
from erpnext.hr.doctype.leave_application.test_leave_application import (
get_employee,
@@ -17,9 +18,8 @@
test_dependencies = ["Employee"]
class TestLeavePolicyAssignment(unittest.TestCase):
-
def setUp(self):
- for doctype in ["Leave Application", "Leave Allocation", "Leave Policy Assignment", "Leave Ledger Entry"]:
+ for doctype in ["Leave Period", "Leave Application", "Leave Allocation", "Leave Policy Assignment", "Leave Ledger Entry"]:
frappe.db.sql("delete from `tab{0}`".format(doctype)) #nosec
def test_grant_leaves(self):
@@ -54,8 +54,8 @@
self.assertEqual(leave_alloc_doc.new_leaves_allocated, 10)
self.assertEqual(leave_alloc_doc.leave_type, "_Test Leave Type")
- self.assertEqual(leave_alloc_doc.from_date, leave_period.from_date)
- self.assertEqual(leave_alloc_doc.to_date, leave_period.to_date)
+ self.assertEqual(getdate(leave_alloc_doc.from_date), getdate(leave_period.from_date))
+ self.assertEqual(getdate(leave_alloc_doc.to_date), getdate(leave_period.to_date))
self.assertEqual(leave_alloc_doc.leave_policy, leave_policy.name)
self.assertEqual(leave_alloc_doc.leave_policy_assignment, leave_policy_assignments[0])
@@ -101,6 +101,55 @@
# User are now allowed to grant leave
self.assertEqual(leave_policy_assignment_doc.leaves_allocated, 0)
+ def test_earned_leave_allocation(self):
+ leave_period = create_leave_period("Test Earned Leave Period")
+ employee = get_employee()
+ leave_type = create_earned_leave_type("Test Earned Leave")
+
+ leave_policy = frappe.get_doc({
+ "doctype": "Leave Policy",
+ "leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 6}]
+ }).insert()
+
+ data = {
+ "assignment_based_on": "Leave Period",
+ "leave_policy": leave_policy.name,
+ "leave_period": leave_period.name
+ }
+ leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data))
+
+ # leaves allocated should be 0 since it is an earned leave and allocation happens via scheduler based on set frequency
+ leaves_allocated = frappe.db.get_value("Leave Allocation", {
+ "leave_policy_assignment": leave_policy_assignments[0]
+ }, "total_leaves_allocated")
+ self.assertEqual(leaves_allocated, 0)
+
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
+ frappe.db.rollback()
+
+
+def create_earned_leave_type(leave_type):
+ frappe.delete_doc_if_exists("Leave Type", leave_type, force=1)
+
+ return frappe.get_doc(dict(
+ leave_type_name=leave_type,
+ doctype="Leave Type",
+ is_earned_leave=1,
+ earned_leave_frequency="Monthly",
+ rounding=0.5,
+ max_leaves_allowed=6
+ )).insert()
+
+
+def create_leave_period(name):
+ frappe.delete_doc_if_exists("Leave Period", name, force=1)
+ start_date = get_first_day(getdate())
+
+ return frappe.get_doc(dict(
+ name=name,
+ doctype="Leave Period",
+ from_date=start_date,
+ to_date=add_months(start_date, 12),
+ company="_Test Company",
+ is_active=1
+ )).insert()
\ 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
deleted file mode 100644
index db910cd..0000000
--- a/erpnext/hr/doctype/leave_type/test_leave_type.js
+++ /dev/null
@@ -1,22 +0,0 @@
-QUnit.module('hr');
-
-QUnit.test("Test: Leave type [HR]", function (assert) {
- assert.expect(1);
- let done = assert.async();
-
- frappe.run_serially([
- // test leave type creation
- () => frappe.set_route("List", "Leave Type", "List"),
- () => frappe.new_doc("Leave Type"),
- () => frappe.timeout(1),
- () => cur_frm.set_value("leave_type_name", "Test Leave type"),
- () => cur_frm.set_value("max_continuous_days_allowed", "5"),
- () => frappe.click_check('Is Carry Forward'),
- // save form
- () => cur_frm.save(),
- () => frappe.timeout(1),
- () => assert.equal("Test Leave type", cur_frm.doc.leave_type_name,
- 'leave type correctly saved'),
- () => done()
- ]);
-});
diff --git a/erpnext/hr/doctype/shift_assignment/shift_assignment.py b/erpnext/hr/doctype/shift_assignment/shift_assignment.py
index 4e829a3..5177302 100644
--- a/erpnext/hr/doctype/shift_assignment/shift_assignment.py
+++ b/erpnext/hr/doctype/shift_assignment/shift_assignment.py
@@ -19,8 +19,8 @@
validate_active_employee(self.employee)
self.validate_overlapping_dates()
- if self.end_date and self.end_date <= self.start_date:
- frappe.throw(_("End Date must not be lesser than Start Date"))
+ if self.end_date:
+ self.validate_from_to_dates('start_date', 'end_date')
def validate_overlapping_dates(self):
if not self.name:
diff --git a/erpnext/hr/doctype/shift_type/shift_type.js b/erpnext/hr/doctype/shift_type/shift_type.js
index ba53312..7138e3b 100644
--- a/erpnext/hr/doctype/shift_type/shift_type.js
+++ b/erpnext/hr/doctype/shift_type/shift_type.js
@@ -4,15 +4,32 @@
frappe.ui.form.on('Shift Type', {
refresh: function(frm) {
frm.add_custom_button(
- 'Mark Attendance',
- () => frm.call({
- doc: frm.doc,
- method: 'process_auto_attendance',
- freeze: true,
- callback: () => {
- frappe.msgprint(__("Attendance has been marked as per employee check-ins"));
+ __('Mark Attendance'),
+ () => {
+ if (!frm.doc.enable_auto_attendance) {
+ frm.scroll_to_field('enable_auto_attendance');
+ frappe.throw(__('Please Enable Auto Attendance and complete the setup first.'));
}
- })
+
+ if (!frm.doc.process_attendance_after) {
+ frm.scroll_to_field('process_attendance_after');
+ frappe.throw(__('Please set {0}.', [__('Process Attendance After').bold()]));
+ }
+
+ if (!frm.doc.last_sync_of_checkin) {
+ frm.scroll_to_field('last_sync_of_checkin');
+ frappe.throw(__('Please set {0}.', [__('Last Sync of Checkin').bold()]));
+ }
+
+ frm.call({
+ doc: frm.doc,
+ method: 'process_auto_attendance',
+ freeze: true,
+ callback: () => {
+ frappe.msgprint(__('Attendance has been marked as per employee check-ins'));
+ }
+ });
+ }
);
}
});
diff --git a/erpnext/hr/doctype/training_event/tests/test_training_event.js b/erpnext/hr/doctype/training_event/tests/test_training_event.js
deleted file mode 100644
index 08031a1..0000000
--- a/erpnext/hr/doctype/training_event/tests/test_training_event.js
+++ /dev/null
@@ -1,59 +0,0 @@
-QUnit.module('hr');
-
-QUnit.test("Test: Training Event [HR]", function (assert) {
- assert.expect(5);
- let done = assert.async();
- let employee_name;
-
- frappe.run_serially([
- // Creation of Training Event
- () => frappe.db.get_value('Employee', {'employee_name': 'Test Employee 1'}, 'name'),
- (r) => {
- employee_name = r.message.name;
- },
- () => {
- frappe.tests.make('Training Event', [
- { event_name: 'Test Training Event 1'},
- { location: 'Mumbai'},
- { start_time: '2017-09-01 11:00:0'},
- { end_time: '2017-09-01 17:00:0'},
- { introduction: 'This is just a test'},
- { employees: [
- [
- {employee: employee_name},
- {employee_name: 'Test Employee 1'},
- {attendance: 'Optional'}
- ]
- ]},
- ]);
- },
- () => frappe.timeout(7),
- () => frappe.click_button('Submit'),
- () => frappe.timeout(1),
- () => frappe.click_button('Yes'),
- () => frappe.timeout(8),
- () => {
- // To check if the fields are correctly set
- assert.ok(cur_frm.get_field('event_name').value == 'Test Training Event 1',
- 'Event created successfully');
-
- assert.ok(cur_frm.get_field('event_status').value=='Scheduled',
- 'Status of event is correctly set');
-
- assert.ok(cur_frm.doc.employees[0].employee_name=='Test Employee 1',
- 'Attendee Employee is correctly set');
-
- assert.ok(cur_frm.doc.employees[0].attendance=='Optional',
- 'Attendance is correctly set');
- },
-
- () => frappe.set_route('List','Training Event','List'),
- () => frappe.timeout(2),
- // Checking the submission of Training Event
- () => {
- assert.ok(cur_list.data[0].docstatus==1,'Training Event Submitted successfully');
- },
- () => frappe.timeout(2),
- () => done()
- ]);
-});
diff --git a/erpnext/hr/doctype/training_feedback/test_training_feedback.js b/erpnext/hr/doctype/training_feedback/test_training_feedback.js
deleted file mode 100644
index 5c825ae..0000000
--- a/erpnext/hr/doctype/training_feedback/test_training_feedback.js
+++ /dev/null
@@ -1,51 +0,0 @@
-QUnit.module('hr');
-
-QUnit.test("Test: Training Feedback [HR]", function (assert) {
- assert.expect(3);
- let done = assert.async();
- let employee_name;
-
- frappe.run_serially([
- // Creating Training Feedback
- () => frappe.set_route('List','Training Feedback','List'),
- () => frappe.timeout(0.3),
- () => frappe.click_button('Make a new Training Feedback'),
- () => frappe.timeout(1),
- () => frappe.db.get_value('Employee', {'employee_name': 'Test Employee 1'}, 'name'),
- (r) => {
- employee_name = r.message.name;
- },
- () => cur_frm.set_value('employee',employee_name),
- () => cur_frm.set_value('employee_name','Test Employee 1'),
- () => cur_frm.set_value('training_event','Test Training Event 1'),
- () => cur_frm.set_value('event_name','Test Training Event 1'),
- () => cur_frm.set_value('feedback','Great Experience. This is just a test.'),
- () => frappe.timeout(1),
- () => cur_frm.save(),
- () => frappe.timeout(1),
- () => cur_frm.save(),
-
- // Submitting the feedback
- () => frappe.click_button('Submit'),
- () => frappe.click_button('Yes'),
- () => frappe.timeout(3),
-
- // Checking if the feedback is given by correct employee
- () => {
- assert.equal('Test Employee 1',cur_frm.get_field('employee_name').value,
- 'Feedback is given by correct employee');
-
- assert.equal('Test Training Event 1',cur_frm.get_field('training_event').value,
- 'Feedback is given for correct event');
- },
-
- () => frappe.set_route('List','Training Feedback','List'),
- () => frappe.timeout(2),
-
- // Checking the submission of Training Result
- () => {
- assert.ok(cur_list.data[0].docstatus==1,'Training Feedback Submitted successfully');
- },
- () => done()
- ]);
-});
diff --git a/erpnext/hr/doctype/training_result_employee/test_training_result.js b/erpnext/hr/doctype/training_result_employee/test_training_result.js
deleted file mode 100644
index 3f39750..0000000
--- a/erpnext/hr/doctype/training_result_employee/test_training_result.js
+++ /dev/null
@@ -1,52 +0,0 @@
-QUnit.module('hr');
-
-QUnit.test("Test: Training Result [HR]", function (assert) {
- assert.expect(5);
- let done = assert.async();
- frappe.run_serially([
- // Creating Training Result
- () => frappe.set_route('List','Training Result','List'),
- () => frappe.timeout(0.3),
- () => frappe.click_button('Make a new Training Result'),
- () => {
- cur_frm.set_value('training_event','Test Training Event 1');
- },
- () => frappe.timeout(1),
- () => frappe.model.set_value('Training Result Employee','New Training Result Employee 1','hours',4),
- () => frappe.model.set_value('Training Result Employee','New Training Result Employee 1','grade','A'),
- () => frappe.model.set_value('Training Result Employee','New Training Result Employee 1','comments','Nice Seminar'),
- () => frappe.timeout(1),
- () => cur_frm.save(),
- () => frappe.timeout(1),
- () => cur_frm.save(),
-
- // Submitting the Training Result
- () => frappe.click_button('Submit'),
- () => frappe.click_button('Yes'),
- () => frappe.timeout(4),
-
- // Checking if the fields are correctly set
- () => {
- assert.equal('Test Training Event 1',cur_frm.get_field('training_event').value,
- 'Training Result is created');
-
- assert.equal('Test Employee 1',cur_frm.doc.employees[0].employee_name,
- 'Training Result is created for correct employee');
-
- assert.equal(4,cur_frm.doc.employees[0].hours,
- 'Hours field is correctly calculated');
-
- assert.equal('A',cur_frm.doc.employees[0].grade,
- 'Grade field is correctly set');
- },
-
- () => frappe.set_route('List','Training Result','List'),
- () => frappe.timeout(2),
-
- // Checking the submission of Training Result
- () => {
- assert.ok(cur_list.data[0].docstatus==1,'Training Result Submitted successfully');
- },
- () => done()
- ]);
-});
diff --git a/erpnext/agriculture/__init__.py b/erpnext/hr/notification/exit_interview_scheduled/__init__.py
similarity index 100%
copy from erpnext/agriculture/__init__.py
copy to erpnext/hr/notification/exit_interview_scheduled/__init__.py
diff --git a/erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.json b/erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.json
new file mode 100644
index 0000000..8323ef0
--- /dev/null
+++ b/erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.json
@@ -0,0 +1,29 @@
+{
+ "attach_print": 0,
+ "channel": "Email",
+ "condition": "doc.date and doc.email and doc.docstatus != 2 and doc.status == 'Scheduled'",
+ "creation": "2021-12-05 22:11:47.263933",
+ "date_changed": "date",
+ "days_in_advance": 1,
+ "docstatus": 0,
+ "doctype": "Notification",
+ "document_type": "Exit Interview",
+ "enabled": 1,
+ "event": "Days Before",
+ "idx": 0,
+ "is_standard": 1,
+ "message": "<table class=\"panel-header\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\">\n\t<tr height=\"10\"></tr>\n\t<tr>\n\t\t<td width=\"15\"></td>\n\t\t<td>\n\t\t\t<div class=\"text-medium text-muted\">\n\t\t\t\t<span>{{_(\"Exit Interview Scheduled:\")}} {{ doc.name }}</span>\n\t\t\t</div>\n\t\t</td>\n\t\t<td width=\"15\"></td>\n\t</tr>\n\t<tr height=\"10\"></tr>\n</table>\n\n<table class=\"panel-body\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\">\n\t<tr height=\"10\"></tr>\n\t<tr>\n\t\t<td width=\"15\"></td>\n\t\t<td>\n\t\t\t<div>\n\t\t\t\t<ul class=\"list-unstyled\" style=\"line-height: 1.7\">\n\t\t\t\t\t<li>{{_(\"Employee\")}}: <b>{{ doc.employee }} - {{ doc.employee_name }}</b></li>\n\t\t\t\t\t<li>{{_(\"Date\")}}: <b>{{ doc.date }}</b></li>\n\t\t\t\t\t<li> {{_(\"Interviewers\")}}: </li>\n\t\t\t\t\t{% for entry in doc.interviewers %}\n\t\t\t\t\t\t<ul>\n\t\t\t\t\t\t\t<li>{{ entry.user }}</li>\n\t\t\t\t\t\t</ul>\n\t\t\t\t\t{% endfor %}\n\t\t\t\t\t<li>{{ _(\"Interview Document\") }}: {{ frappe.utils.get_link_to_form(doc.doctype, doc.name) }}</li>\n\t\t\t\t</ul>\n\t\t\t</div>\n\t\t</td>\n\t\t<td width=\"15\"></td>\n\t</tr>\n\t<tr height=\"10\"></tr>\n</table>\n",
+ "modified": "2021-12-05 22:26:57.096159",
+ "modified_by": "Administrator",
+ "module": "HR",
+ "name": "Exit Interview Scheduled",
+ "owner": "Administrator",
+ "recipients": [
+ {
+ "receiver_by_document_field": "email"
+ }
+ ],
+ "send_system_notification": 0,
+ "send_to_all_assignees": 1,
+ "subject": "Exit Interview Scheduled: {{ doc.name }}"
+}
\ No newline at end of file
diff --git a/erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.md b/erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.md
new file mode 100644
index 0000000..6d6db40
--- /dev/null
+++ b/erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.md
@@ -0,0 +1,37 @@
+<table class="panel-header" border="0" cellpadding="0" cellspacing="0" width="100%">
+ <tr height="10"></tr>
+ <tr>
+ <td width="15"></td>
+ <td>
+ <div class="text-medium text-muted">
+ <h2>{{_("Exit Interview Scheduled:")}} {{ doc.name }}</h2>
+ </div>
+ </td>
+ <td width="15"></td>
+ </tr>
+ <tr height="10"></tr>
+</table>
+
+<table class="panel-body" border="0" cellpadding="0" cellspacing="0" width="100%">
+ <tr height="10"></tr>
+ <tr>
+ <td width="15"></td>
+ <td>
+ <div>
+ <ul class="list-unstyled" style="line-height: 1.7">
+ <li><b>{{_("Employee")}}: </b>{{ doc.employee }} - {{ doc.employee_name }}</li>
+ <li><b>{{_("Date")}}: </b>{{ frappe.utils.formatdate(doc.date) }}</li>
+ <li><b>{{_("Interviewers")}}:</b> </li>
+ {% for entry in doc.interviewers %}
+ <ul>
+ <li>{{ entry.user }}</li>
+ </ul>
+ {% endfor %}
+ <li><b>{{ _("Interview Document") }}:</b> {{ frappe.utils.get_link_to_form(doc.doctype, doc.name) }}</li>
+ </ul>
+ </div>
+ </td>
+ <td width="15"></td>
+ </tr>
+ <tr height="10"></tr>
+</table>
diff --git a/erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.py b/erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.py
new file mode 100644
index 0000000..5f697c9
--- /dev/null
+++ b/erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.py
@@ -0,0 +1,6 @@
+# import frappe
+
+
+def get_context(context):
+ # do your magic here
+ pass
diff --git a/erpnext/agriculture/__init__.py b/erpnext/hr/report/employee_exits/__init__.py
similarity index 100%
copy from erpnext/agriculture/__init__.py
copy to erpnext/hr/report/employee_exits/__init__.py
diff --git a/erpnext/hr/report/employee_exits/employee_exits.js b/erpnext/hr/report/employee_exits/employee_exits.js
new file mode 100644
index 0000000..ac677d8
--- /dev/null
+++ b/erpnext/hr/report/employee_exits/employee_exits.js
@@ -0,0 +1,77 @@
+// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+/* eslint-disable */
+
+frappe.query_reports["Employee Exits"] = {
+ filters: [
+ {
+ "fieldname": "from_date",
+ "label": __("From Date"),
+ "fieldtype": "Date",
+ "default": frappe.datetime.add_months(frappe.datetime.nowdate(), -12)
+ },
+ {
+ "fieldname": "to_date",
+ "label": __("To Date"),
+ "fieldtype": "Date",
+ "default": frappe.datetime.nowdate()
+ },
+ {
+ "fieldname": "company",
+ "label": __("Company"),
+ "fieldtype": "Link",
+ "options": "Company"
+ },
+ {
+ "fieldname": "department",
+ "label": __("Department"),
+ "fieldtype": "Link",
+ "options": "Department"
+ },
+ {
+ "fieldname": "designation",
+ "label": __("Designation"),
+ "fieldtype": "Link",
+ "options": "Designation"
+ },
+ {
+ "fieldname": "employee",
+ "label": __("Employee"),
+ "fieldtype": "Link",
+ "options": "Employee"
+ },
+ {
+ "fieldname": "reports_to",
+ "label": __("Reports To"),
+ "fieldtype": "Link",
+ "options": "Employee"
+ },
+ {
+ "fieldname": "interview_status",
+ "label": __("Interview Status"),
+ "fieldtype": "Select",
+ "options": ["", "Pending", "Scheduled", "Completed"]
+ },
+ {
+ "fieldname": "final_decision",
+ "label": __("Final Decision"),
+ "fieldtype": "Select",
+ "options": ["", "Employee Retained", "Exit Confirmed"]
+ },
+ {
+ "fieldname": "exit_interview_pending",
+ "label": __("Exit Interview Pending"),
+ "fieldtype": "Check"
+ },
+ {
+ "fieldname": "questionnaire_pending",
+ "label": __("Exit Questionnaire Pending"),
+ "fieldtype": "Check"
+ },
+ {
+ "fieldname": "fnf_pending",
+ "label": __("FnF Pending"),
+ "fieldtype": "Check"
+ }
+ ]
+};
diff --git a/erpnext/hr/report/employee_exits/employee_exits.json b/erpnext/hr/report/employee_exits/employee_exits.json
new file mode 100644
index 0000000..4fe9a85
--- /dev/null
+++ b/erpnext/hr/report/employee_exits/employee_exits.json
@@ -0,0 +1,33 @@
+{
+ "add_total_row": 0,
+ "columns": [],
+ "creation": "2021-12-05 19:47:18.332319",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "letter_head": "Test",
+ "modified": "2021-12-05 19:47:18.332319",
+ "modified_by": "Administrator",
+ "module": "HR",
+ "name": "Employee Exits",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "Exit Interview",
+ "report_name": "Employee Exits",
+ "report_type": "Script Report",
+ "roles": [
+ {
+ "role": "System Manager"
+ },
+ {
+ "role": "HR Manager"
+ },
+ {
+ "role": "HR User"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/hr/report/employee_exits/employee_exits.py b/erpnext/hr/report/employee_exits/employee_exits.py
new file mode 100644
index 0000000..bde5a89
--- /dev/null
+++ b/erpnext/hr/report/employee_exits/employee_exits.py
@@ -0,0 +1,230 @@
+# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
+# License: MIT. See LICENSE
+
+import frappe
+from frappe import _
+from frappe.query_builder import Order
+from frappe.utils import getdate
+
+
+def execute(filters=None):
+ columns = get_columns()
+ data = get_data(filters)
+ chart = get_chart_data(data)
+ report_summary = get_report_summary(data)
+
+ return columns, data, None, chart, report_summary
+
+def get_columns():
+ return [
+ {
+ 'label': _('Employee'),
+ 'fieldname': 'employee',
+ 'fieldtype': 'Link',
+ 'options': 'Employee',
+ 'width': 150
+ },
+ {
+ 'label': _('Employee Name'),
+ 'fieldname': 'employee_name',
+ 'fieldtype': 'Data',
+ 'width': 150
+ },
+ {
+ 'label': _('Date of Joining'),
+ 'fieldname': 'date_of_joining',
+ 'fieldtype': 'Date',
+ 'width': 120
+ },
+ {
+ 'label': _('Relieving Date'),
+ 'fieldname': 'relieving_date',
+ 'fieldtype': 'Date',
+ 'width': 120
+ },
+ {
+ 'label': _('Exit Interview'),
+ 'fieldname': 'exit_interview',
+ 'fieldtype': 'Link',
+ 'options': 'Exit Interview',
+ 'width': 150
+ },
+ {
+ 'label': _('Interview Status'),
+ 'fieldname': 'interview_status',
+ 'fieldtype': 'Data',
+ 'width': 130
+ },
+ {
+ 'label': _('Final Decision'),
+ 'fieldname': 'employee_status',
+ 'fieldtype': 'Data',
+ 'width': 150
+ },
+ {
+ 'label': _('Full and Final Statement'),
+ 'fieldname': 'full_and_final_statement',
+ 'fieldtype': 'Link',
+ 'options': 'Full and Final Statement',
+ 'width': 180
+ },
+ {
+ 'label': _('Department'),
+ 'fieldname': 'department',
+ 'fieldtype': 'Link',
+ 'options': 'Department',
+ 'width': 120
+ },
+ {
+ 'label': _('Designation'),
+ 'fieldname': 'designation',
+ 'fieldtype': 'Link',
+ 'options': 'Designation',
+ 'width': 120
+ },
+ {
+ 'label': _('Reports To'),
+ 'fieldname': 'reports_to',
+ 'fieldtype': 'Link',
+ 'options': 'Employee',
+ 'width': 120
+ }
+ ]
+
+def get_data(filters):
+ employee = frappe.qb.DocType('Employee')
+ interview = frappe.qb.DocType('Exit Interview')
+ fnf = frappe.qb.DocType('Full and Final Statement')
+
+ query = (
+ frappe.qb.from_(employee)
+ .left_join(interview).on(interview.employee == employee.name)
+ .left_join(fnf).on(fnf.employee == employee.name)
+ .select(
+ employee.name.as_('employee'), employee.employee_name.as_('employee_name'),
+ employee.date_of_joining.as_('date_of_joining'), employee.relieving_date.as_('relieving_date'),
+ employee.department.as_('department'), employee.designation.as_('designation'),
+ employee.reports_to.as_('reports_to'), interview.name.as_('exit_interview'),
+ interview.status.as_('interview_status'), interview.employee_status.as_('employee_status'),
+ interview.reference_document_name.as_('questionnaire'), fnf.name.as_('full_and_final_statement'))
+ .distinct()
+ .where(
+ ((employee.relieving_date.isnotnull()) | (employee.relieving_date != ''))
+ & ((interview.name.isnull()) | ((interview.name.isnotnull()) & (interview.docstatus != 2)))
+ & ((fnf.name.isnull()) | ((fnf.name.isnotnull()) & (fnf.docstatus != 2)))
+ ).orderby(employee.relieving_date, order=Order.asc)
+ )
+
+ query = get_conditions(filters, query, employee, interview, fnf)
+ result = query.run(as_dict=True)
+
+ return result
+
+
+def get_conditions(filters, query, employee, interview, fnf):
+ if filters.get('from_date') and filters.get('to_date'):
+ query = query.where(employee.relieving_date[getdate(filters.get('from_date')):getdate(filters.get('to_date'))])
+
+ elif filters.get('from_date'):
+ query = query.where(employee.relieving_date >= filters.get('from_date'))
+
+ elif filters.get('to_date'):
+ query = query.where(employee.relieving_date <= filters.get('to_date'))
+
+ if filters.get('company'):
+ query = query.where(employee.company == filters.get('company'))
+
+ if filters.get('department'):
+ query = query.where(employee.department == filters.get('department'))
+
+ if filters.get('designation'):
+ query = query.where(employee.designation == filters.get('designation'))
+
+ if filters.get('employee'):
+ query = query.where(employee.name == filters.get('employee'))
+
+ if filters.get('reports_to'):
+ query = query.where(employee.reports_to == filters.get('reports_to'))
+
+ if filters.get('interview_status'):
+ query = query.where(interview.status == filters.get('interview_status'))
+
+ if filters.get('final_decision'):
+ query = query.where(interview.employee_status == filters.get('final_decision'))
+
+ if filters.get('exit_interview_pending'):
+ query = query.where((interview.name == '') | (interview.name.isnull()))
+
+ if filters.get('questionnaire_pending'):
+ query = query.where((interview.reference_document_name == '') | (interview.reference_document_name.isnull()))
+
+ if filters.get('fnf_pending'):
+ query = query.where((fnf.name == '') | (fnf.name.isnull()))
+
+ return query
+
+
+def get_chart_data(data):
+ if not data:
+ return None
+
+ retained = 0
+ exit_confirmed = 0
+ pending = 0
+
+ for entry in data:
+ if entry.employee_status == 'Employee Retained':
+ retained += 1
+ elif entry.employee_status == 'Exit Confirmed':
+ exit_confirmed += 1
+ else:
+ pending += 1
+
+ chart = {
+ 'data': {
+ 'labels': [_('Retained'), _('Exit Confirmed'), _('Decision Pending')],
+ 'datasets': [{'name': _('Employee Status'), 'values': [retained, exit_confirmed, pending]}]
+ },
+ 'type': 'donut',
+ 'colors': ['green', 'red', 'blue'],
+ }
+
+ return chart
+
+
+def get_report_summary(data):
+ if not data:
+ return None
+
+ total_resignations = len(data)
+ interviews_pending = len([entry.name for entry in data if not entry.exit_interview])
+ fnf_pending = len([entry.name for entry in data if not entry.full_and_final_statement])
+ questionnaires_pending = len([entry.name for entry in data if not entry.questionnaire])
+
+ return [
+ {
+ 'value': total_resignations,
+ 'label': _('Total Resignations'),
+ 'indicator': 'Red' if total_resignations > 0 else 'Green',
+ 'datatype': 'Int',
+ },
+ {
+ 'value': interviews_pending,
+ 'label': _('Pending Interviews'),
+ 'indicator': 'Blue' if interviews_pending > 0 else 'Green',
+ 'datatype': 'Int',
+ },
+ {
+ 'value': fnf_pending,
+ 'label': _('Pending FnF'),
+ 'indicator': 'Blue' if fnf_pending > 0 else 'Green',
+ 'datatype': 'Int',
+ },
+ {
+ 'value': questionnaires_pending,
+ 'label': _('Pending Questionnaires'),
+ 'indicator': 'Blue' if questionnaires_pending > 0 else 'Green',
+ 'datatype': 'Int'
+ },
+ ]
+
diff --git a/erpnext/hr/report/employee_exits/test_employee_exits.py b/erpnext/hr/report/employee_exits/test_employee_exits.py
new file mode 100644
index 0000000..d7e95a6
--- /dev/null
+++ b/erpnext/hr/report/employee_exits/test_employee_exits.py
@@ -0,0 +1,242 @@
+import unittest
+
+import frappe
+from frappe.utils import add_days, getdate
+
+from erpnext.hr.doctype.employee.test_employee import make_employee
+from erpnext.hr.doctype.exit_interview.test_exit_interview import create_exit_interview
+from erpnext.hr.doctype.full_and_final_statement.test_full_and_final_statement import (
+ create_full_and_final_statement,
+)
+from erpnext.hr.report.employee_exits.employee_exits import execute
+
+
+class TestEmployeeExits(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ create_company()
+ frappe.db.sql("delete from `tabEmployee` where company='Test Company'")
+ frappe.db.sql("delete from `tabFull and Final Statement` where company='Test Company'")
+ frappe.db.sql("delete from `tabExit Interview` where company='Test Company'")
+
+ cls.create_records()
+
+ @classmethod
+ def tearDownClass(cls):
+ frappe.db.rollback()
+
+ @classmethod
+ def create_records(cls):
+ cls.emp1 = make_employee(
+ 'employeeexit1@example.com',
+ company='Test Company',
+ date_of_joining=getdate('01-10-2021'),
+ relieving_date=add_days(getdate(), 14),
+ designation='Accountant'
+ )
+ cls.emp2 = make_employee(
+ 'employeeexit2@example.com',
+ company='Test Company',
+ date_of_joining=getdate('01-12-2021'),
+ relieving_date=add_days(getdate(), 15),
+ designation='Accountant'
+ )
+
+ cls.emp3 = make_employee(
+ 'employeeexit3@example.com',
+ company='Test Company',
+ date_of_joining=getdate('02-12-2021'),
+ relieving_date=add_days(getdate(), 29),
+ designation='Engineer'
+ )
+ cls.emp4 = make_employee(
+ 'employeeexit4@example.com',
+ company='Test Company',
+ date_of_joining=getdate('01-12-2021'),
+ relieving_date=add_days(getdate(), 30),
+ designation='Engineer'
+ )
+
+ # exit interview for 3 employees only
+ cls.interview1 = create_exit_interview(cls.emp1)
+ cls.interview2 = create_exit_interview(cls.emp2)
+ cls.interview3 = create_exit_interview(cls.emp3)
+
+ # create fnf for some records
+ cls.fnf1 = create_full_and_final_statement(cls.emp1)
+ cls.fnf2 = create_full_and_final_statement(cls.emp2)
+
+ # link questionnaire for a few records
+ # setting employee doctype as reference instead of creating a questionnaire
+ # since this is just for a test
+ frappe.db.set_value('Exit Interview', cls.interview1.name, {
+ 'ref_doctype': 'Employee',
+ 'reference_document_name': cls.emp1
+ })
+
+ frappe.db.set_value('Exit Interview', cls.interview2.name, {
+ 'ref_doctype': 'Employee',
+ 'reference_document_name': cls.emp2
+ })
+
+ frappe.db.set_value('Exit Interview', cls.interview3.name, {
+ 'ref_doctype': 'Employee',
+ 'reference_document_name': cls.emp3
+ })
+
+
+ def test_employee_exits_summary(self):
+ filters = {
+ 'company': 'Test Company',
+ 'from_date': getdate(),
+ 'to_date': add_days(getdate(), 15),
+ 'designation': 'Accountant'
+ }
+
+ report = execute(filters)
+
+ employee1 = frappe.get_doc('Employee', self.emp1)
+ employee2 = frappe.get_doc('Employee', self.emp2)
+ expected_data = [
+ {
+ 'employee': employee1.name,
+ 'employee_name': employee1.employee_name,
+ 'date_of_joining': employee1.date_of_joining,
+ 'relieving_date': employee1.relieving_date,
+ 'department': employee1.department,
+ 'designation': employee1.designation,
+ 'reports_to': None,
+ 'exit_interview': self.interview1.name,
+ 'interview_status': self.interview1.status,
+ 'employee_status': '',
+ 'questionnaire': employee1.name,
+ 'full_and_final_statement': self.fnf1.name
+ },
+ {
+ 'employee': employee2.name,
+ 'employee_name': employee2.employee_name,
+ 'date_of_joining': employee2.date_of_joining,
+ 'relieving_date': employee2.relieving_date,
+ 'department': employee2.department,
+ 'designation': employee2.designation,
+ 'reports_to': None,
+ 'exit_interview': self.interview2.name,
+ 'interview_status': self.interview2.status,
+ 'employee_status': '',
+ 'questionnaire': employee2.name,
+ 'full_and_final_statement': self.fnf2.name
+ }
+ ]
+
+ self.assertEqual(expected_data, report[1]) # rows
+
+
+ def test_pending_exit_interviews_summary(self):
+ filters = {
+ 'company': 'Test Company',
+ 'from_date': getdate(),
+ 'to_date': add_days(getdate(), 30),
+ 'exit_interview_pending': 1
+ }
+
+ report = execute(filters)
+
+ employee4 = frappe.get_doc('Employee', self.emp4)
+ expected_data = [{
+ 'employee': employee4.name,
+ 'employee_name': employee4.employee_name,
+ 'date_of_joining': employee4.date_of_joining,
+ 'relieving_date': employee4.relieving_date,
+ 'department': employee4.department,
+ 'designation': employee4.designation,
+ 'reports_to': None,
+ 'exit_interview': None,
+ 'interview_status': None,
+ 'employee_status': None,
+ 'questionnaire': None,
+ 'full_and_final_statement': None
+ }]
+
+ self.assertEqual(expected_data, report[1]) # rows
+
+ def test_pending_exit_questionnaire_summary(self):
+ filters = {
+ 'company': 'Test Company',
+ 'from_date': getdate(),
+ 'to_date': add_days(getdate(), 30),
+ 'questionnaire_pending': 1
+ }
+
+ report = execute(filters)
+
+ employee4 = frappe.get_doc('Employee', self.emp4)
+ expected_data = [{
+ 'employee': employee4.name,
+ 'employee_name': employee4.employee_name,
+ 'date_of_joining': employee4.date_of_joining,
+ 'relieving_date': employee4.relieving_date,
+ 'department': employee4.department,
+ 'designation': employee4.designation,
+ 'reports_to': None,
+ 'exit_interview': None,
+ 'interview_status': None,
+ 'employee_status': None,
+ 'questionnaire': None,
+ 'full_and_final_statement': None
+ }]
+
+ self.assertEqual(expected_data, report[1]) # rows
+
+
+ def test_pending_fnf_summary(self):
+ filters = {
+ 'company': 'Test Company',
+ 'fnf_pending': 1
+ }
+
+ report = execute(filters)
+
+ employee3 = frappe.get_doc('Employee', self.emp3)
+ employee4 = frappe.get_doc('Employee', self.emp4)
+ expected_data = [
+ {
+ 'employee': employee3.name,
+ 'employee_name': employee3.employee_name,
+ 'date_of_joining': employee3.date_of_joining,
+ 'relieving_date': employee3.relieving_date,
+ 'department': employee3.department,
+ 'designation': employee3.designation,
+ 'reports_to': None,
+ 'exit_interview': self.interview3.name,
+ 'interview_status': self.interview3.status,
+ 'employee_status': '',
+ 'questionnaire': employee3.name,
+ 'full_and_final_statement': None
+ },
+ {
+ 'employee': employee4.name,
+ 'employee_name': employee4.employee_name,
+ 'date_of_joining': employee4.date_of_joining,
+ 'relieving_date': employee4.relieving_date,
+ 'department': employee4.department,
+ 'designation': employee4.designation,
+ 'reports_to': None,
+ 'exit_interview': None,
+ 'interview_status': None,
+ 'employee_status': None,
+ 'questionnaire': None,
+ 'full_and_final_statement': None
+ }
+ ]
+
+ self.assertEqual(expected_data, report[1]) # rows
+
+
+def create_company():
+ if not frappe.db.exists('Company', 'Test Company'):
+ frappe.get_doc({
+ 'doctype': 'Company',
+ 'company_name': 'Test Company',
+ 'default_currency': 'INR',
+ 'country': 'India'
+ }).insert()
\ No newline at end of file
diff --git a/erpnext/hr/workspace/hr/hr.json b/erpnext/hr/workspace/hr/hr.json
index 7408d63..85e641c 100644
--- a/erpnext/hr/workspace/hr/hr.json
+++ b/erpnext/hr/workspace/hr/hr.json
@@ -5,7 +5,7 @@
"label": "Outgoing Salary"
}
],
- "content": "[{\"type\": \"onboarding\", \"data\": {\"onboarding_name\":\"Human Resource\", \"col\": 12}}, {\"type\": \"chart\", \"data\": {\"chart_name\": \"Outgoing Salary\", \"col\": 12}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Employee\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Leave Application\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Attendance\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Job Applicant\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Monthly Attendance Sheet\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Dashboard\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Employee\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Employee Lifecycle\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Shift Management\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Leaves\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Attendance\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Expense Claims\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Settings\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Fleet Management\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Recruitment\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Loans\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Training\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Performance\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Key Reports\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Other Reports\", \"col\": 4}}]",
+ "content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Human Resource\",\"col\":12}},{\"type\":\"chart\",\"data\":{\"chart_name\":\"Outgoing Salary\",\"col\":12}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\",\"level\":4,\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Employee\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Leave Application\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Attendance\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Job Applicant\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Monthly Attendance Sheet\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":4}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\",\"level\":4,\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Employee\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Employee Lifecycle\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Employee Exit\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Shift Management\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Leaves\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Attendance\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Expense Claims\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Loans\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Recruitment\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Performance\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Fleet Management\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Training\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Key Reports\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Other Reports\",\"col\":4}}]",
"creation": "2020-03-02 15:48:58.322521",
"docstatus": 0,
"doctype": "Workspace",
@@ -16,14 +16,6 @@
"label": "HR",
"links": [
{
- "hidden": 0,
- "is_query_report": 0,
- "label": "Employee",
- "link_count": 0,
- "onboard": 0,
- "type": "Card Break"
- },
- {
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
@@ -112,14 +104,6 @@
"type": "Link"
},
{
- "hidden": 0,
- "is_query_report": 0,
- "label": "Employee Lifecycle",
- "link_count": 0,
- "onboard": 0,
- "type": "Card Break"
- },
- {
"dependencies": "Job Applicant",
"hidden": 0,
"is_query_report": 0,
@@ -228,14 +212,6 @@
"type": "Link"
},
{
- "hidden": 0,
- "is_query_report": 0,
- "label": "Shift Management",
- "link_count": 0,
- "onboard": 0,
- "type": "Card Break"
- },
- {
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
@@ -269,14 +245,6 @@
"type": "Link"
},
{
- "hidden": 0,
- "is_query_report": 0,
- "label": "Leaves",
- "link_count": 0,
- "onboard": 0,
- "type": "Card Break"
- },
- {
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
@@ -387,14 +355,6 @@
"type": "Link"
},
{
- "hidden": 0,
- "is_query_report": 0,
- "label": "Attendance",
- "link_count": 0,
- "onboard": 0,
- "type": "Card Break"
- },
- {
"dependencies": "Employee",
"hidden": 0,
"is_query_report": 0,
@@ -450,14 +410,6 @@
"type": "Link"
},
{
- "hidden": 0,
- "is_query_report": 0,
- "label": "Expense Claims",
- "link_count": 0,
- "onboard": 0,
- "type": "Card Break"
- },
- {
"dependencies": "Employee",
"hidden": 0,
"is_query_report": 0,
@@ -490,14 +442,6 @@
"type": "Link"
},
{
- "hidden": 0,
- "is_query_report": 0,
- "label": "Settings",
- "link_count": 0,
- "onboard": 0,
- "type": "Card Break"
- },
- {
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
@@ -533,14 +477,6 @@
{
"hidden": 0,
"is_query_report": 0,
- "label": "Fleet Management",
- "link_count": 0,
- "onboard": 0,
- "type": "Card Break"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
"label": "Driver",
"link_count": 0,
"link_to": "Driver",
@@ -582,14 +518,6 @@
"type": "Link"
},
{
- "hidden": 0,
- "is_query_report": 0,
- "label": "Recruitment",
- "link_count": 0,
- "onboard": 0,
- "type": "Card Break"
- },
- {
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
@@ -809,14 +737,6 @@
"type": "Link"
},
{
- "hidden": 0,
- "is_query_report": 0,
- "label": "Key Reports",
- "link_count": 0,
- "onboard": 0,
- "type": "Card Break"
- },
- {
"dependencies": "Attendance",
"hidden": 0,
"is_query_report": 1,
@@ -933,9 +853,796 @@
"link_type": "Report",
"onboard": 0,
"type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Employee Lifecycle",
+ "link_count": 7,
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "Job Applicant",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Employee Onboarding",
+ "link_count": 0,
+ "link_to": "Employee Onboarding",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Employee",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Employee Skill Map",
+ "link_count": 0,
+ "link_to": "Employee Skill Map",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Employee",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Employee Promotion",
+ "link_count": 0,
+ "link_to": "Employee Promotion",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Employee",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Employee Transfer",
+ "link_count": 0,
+ "link_to": "Employee Transfer",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Grievance Type",
+ "link_count": 0,
+ "link_to": "Grievance Type",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Employee Grievance",
+ "link_count": 0,
+ "link_to": "Employee Grievance",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Employee",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Employee Onboarding Template",
+ "link_count": 0,
+ "link_to": "Employee Onboarding Template",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Employee Exit",
+ "link_count": 4,
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Employee Separation Template",
+ "link_count": 0,
+ "link_to": "Employee Separation Template",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Employee Separation",
+ "link_count": 0,
+ "link_to": "Employee Separation",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Full and Final Statement",
+ "link_count": 0,
+ "link_to": "Full and Final Statement",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Exit Interview",
+ "link_count": 0,
+ "link_to": "Exit Interview",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Employee",
+ "link_count": 8,
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Employee",
+ "link_count": 0,
+ "link_to": "Employee",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Employment Type",
+ "link_count": 0,
+ "link_to": "Employment Type",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Branch",
+ "link_count": 0,
+ "link_to": "Branch",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Department",
+ "link_count": 0,
+ "link_to": "Department",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Designation",
+ "link_count": 0,
+ "link_to": "Designation",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Employee Grade",
+ "link_count": 0,
+ "link_to": "Employee Grade",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Employee",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Employee Group",
+ "link_count": 0,
+ "link_to": "Employee Group",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Employee Health Insurance",
+ "link_count": 0,
+ "link_to": "Employee Health Insurance",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Key Reports",
+ "link_count": 7,
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "Attendance",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Monthly Attendance Sheet",
+ "link_count": 0,
+ "link_to": "Monthly Attendance Sheet",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Staffing Plan",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Recruitment Analytics",
+ "link_count": 0,
+ "link_to": "Recruitment Analytics",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Employee",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Employee Analytics",
+ "link_count": 0,
+ "link_to": "Employee Analytics",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Employee",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Employee Leave Balance",
+ "link_count": 0,
+ "link_to": "Employee Leave Balance",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Employee",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Employee Leave Balance Summary",
+ "link_count": 0,
+ "link_to": "Employee Leave Balance Summary",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Employee Advance",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Employee Advance Summary",
+ "link_count": 0,
+ "link_to": "Employee Advance Summary",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Employee Exits",
+ "link_count": 0,
+ "link_to": "Employee Exits",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Recruitment",
+ "link_count": 11,
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Job Opening",
+ "link_count": 0,
+ "link_to": "Job Opening",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Employee Referral",
+ "link_count": 0,
+ "link_to": "Employee Referral",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Job Applicant",
+ "link_count": 0,
+ "link_to": "Job Applicant",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Job Offer",
+ "link_count": 0,
+ "link_to": "Job Offer",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Staffing Plan",
+ "link_count": 0,
+ "link_to": "Staffing Plan",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Appointment Letter",
+ "link_count": 0,
+ "link_to": "Appointment Letter",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Appointment Letter Template",
+ "link_count": 0,
+ "link_to": "Appointment Letter Template",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Interview Type",
+ "link_count": 0,
+ "link_to": "Interview Type",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Interview Round",
+ "link_count": 0,
+ "link_to": "Interview Round",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Interview",
+ "link_count": 0,
+ "link_to": "Interview",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Interview Feedback",
+ "link_count": 0,
+ "link_to": "Interview Feedback",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Fleet Management",
+ "link_count": 4,
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Driver",
+ "link_count": 0,
+ "link_to": "Driver",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Vehicle",
+ "link_count": 0,
+ "link_to": "Vehicle",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Vehicle Log",
+ "link_count": 0,
+ "link_to": "Vehicle Log",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Vehicle",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Vehicle Expenses",
+ "link_count": 0,
+ "link_to": "Vehicle Expenses",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Settings",
+ "link_count": 3,
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "HR Settings",
+ "link_count": 0,
+ "link_to": "HR Settings",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Daily Work Summary Group",
+ "link_count": 0,
+ "link_to": "Daily Work Summary Group",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Team Updates",
+ "link_count": 0,
+ "link_to": "team-updates",
+ "link_type": "Page",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Expense Claims",
+ "link_count": 3,
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "Employee",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Expense Claim",
+ "link_count": 0,
+ "link_to": "Expense Claim",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Employee",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Employee Advance",
+ "link_count": 0,
+ "link_to": "Employee Advance",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Travel Request",
+ "link_count": 0,
+ "link_to": "Travel Request",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Attendance",
+ "link_count": 5,
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "Employee",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Employee Attendance Tool",
+ "link_count": 0,
+ "link_to": "Employee Attendance Tool",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Employee",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Attendance",
+ "link_count": 0,
+ "link_to": "Attendance",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Employee",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Attendance Request",
+ "link_count": 0,
+ "link_to": "Attendance Request",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Employee",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Upload Attendance",
+ "link_count": 0,
+ "link_to": "Upload Attendance",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Employee",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Employee Checkin",
+ "link_count": 0,
+ "link_to": "Employee Checkin",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Leaves",
+ "link_count": 10,
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Holiday List",
+ "link_count": 0,
+ "link_to": "Holiday List",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Leave Type",
+ "link_count": 0,
+ "link_to": "Leave Type",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Leave Period",
+ "link_count": 0,
+ "link_to": "Leave Period",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Leave Type",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Leave Policy",
+ "link_count": 0,
+ "link_to": "Leave Policy",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Leave Policy",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Leave Policy Assignment",
+ "link_count": 0,
+ "link_to": "Leave Policy Assignment",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Employee",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Leave Application",
+ "link_count": 0,
+ "link_to": "Leave Application",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Employee",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Leave Allocation",
+ "link_count": 0,
+ "link_to": "Leave Allocation",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Employee",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Leave Encashment",
+ "link_count": 0,
+ "link_to": "Leave Encashment",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Leave Block List",
+ "link_count": 0,
+ "link_to": "Leave Block List",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Employee",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Compensatory Leave Request",
+ "link_count": 0,
+ "link_to": "Compensatory Leave Request",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Shift Management",
+ "link_count": 3,
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Shift Type",
+ "link_count": 0,
+ "link_to": "Shift Type",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Shift Request",
+ "link_count": 0,
+ "link_to": "Shift Request",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Shift Assignment",
+ "link_count": 0,
+ "link_to": "Shift Assignment",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
}
],
- "modified": "2021-08-31 12:18:59.842919",
+ "modified": "2021-12-05 22:05:13.004462",
"modified_by": "Administrator",
"module": "HR",
"name": "HR",
diff --git a/erpnext/loan_management/doctype/loan/loan.json b/erpnext/loan_management/doctype/loan/loan.json
index 5979992..af26f7b 100644
--- a/erpnext/loan_management/doctype/loan/loan.json
+++ b/erpnext/loan_management/doctype/loan/loan.json
@@ -240,12 +240,14 @@
"label": "Repayment Schedule"
},
{
+ "allow_on_submit": 1,
"depends_on": "eval:doc.is_term_loan == 1",
"fieldname": "repayment_schedule",
"fieldtype": "Table",
"label": "Repayment Schedule",
"no_copy": 1,
- "options": "Repayment Schedule"
+ "options": "Repayment Schedule",
+ "read_only": 1
},
{
"fieldname": "section_break_17",
@@ -363,6 +365,7 @@
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan",
+ "naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
diff --git a/erpnext/loan_management/doctype/loan/loan.py b/erpnext/loan_management/doctype/loan/loan.py
index 84e0f03..f660a24 100644
--- a/erpnext/loan_management/doctype/loan/loan.py
+++ b/erpnext/loan_management/doctype/loan/loan.py
@@ -7,7 +7,7 @@
import frappe
from frappe import _
-from frappe.utils import add_months, flt, getdate, now_datetime, nowdate
+from frappe.utils import add_months, flt, get_last_day, getdate, now_datetime, nowdate
import erpnext
from erpnext.controllers.accounts_controller import AccountsController
@@ -62,7 +62,7 @@
self.rate_of_interest = frappe.db.get_value("Loan Type", self.loan_type, "rate_of_interest")
if self.repayment_method == "Repay Over Number of Periods":
- self.monthly_repayment_amount = get_monthly_repayment_amount(self.repayment_method, self.loan_amount, self.rate_of_interest, self.repayment_periods)
+ self.monthly_repayment_amount = get_monthly_repayment_amount(self.loan_amount, self.rate_of_interest, self.repayment_periods)
def check_sanctioned_amount_limit(self):
sanctioned_amount_limit = get_sanctioned_amount_limit(self.applicant_type, self.applicant, self.company)
@@ -99,7 +99,7 @@
"total_payment": total_payment,
"balance_loan_amount": balance_amount
})
- next_payment_date = add_months(payment_date, 1)
+ next_payment_date = add_single_month(payment_date)
payment_date = next_payment_date
def set_repayment_period(self):
@@ -211,7 +211,7 @@
if monthly_repayment_amount > loan_amount:
frappe.throw(_("Monthly Repayment Amount cannot be greater than Loan Amount"))
-def get_monthly_repayment_amount(repayment_method, loan_amount, rate_of_interest, repayment_periods):
+def get_monthly_repayment_amount(loan_amount, rate_of_interest, repayment_periods):
if rate_of_interest:
monthly_interest_rate = flt(rate_of_interest) / (12 *100)
monthly_repayment_amount = math.ceil((loan_amount * monthly_interest_rate *
@@ -395,3 +395,9 @@
"value": len(applicants),
"fieldtype": "Int"
}
+
+def add_single_month(date):
+ if getdate(date) == get_last_day(date):
+ return get_last_day(add_months(date, 1))
+ else:
+ return add_months(date, 1)
\ 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 c0f058f..1676c21 100644
--- a/erpnext/loan_management/doctype/loan/test_loan.py
+++ b/erpnext/loan_management/doctype/loan/test_loan.py
@@ -218,6 +218,14 @@
self.assertEqual(flt(loan.total_principal_paid, 0), flt(repayment_entry.amount_paid -
penalty_amount - total_interest_paid, 0))
+ # Check Repayment Entry cancel
+ repayment_entry.load_from_db()
+ repayment_entry.cancel()
+
+ loan.load_from_db()
+ self.assertEqual(loan.total_principal_paid, 0)
+ self.assertEqual(loan.total_principal_paid, 0)
+
def test_loan_closure(self):
pledge = [{
"loan_security": "Test Security 1",
@@ -295,6 +303,27 @@
self.assertEqual(amounts[0], 11250.00)
self.assertEqual(amounts[1], 78303.00)
+ def test_repayment_schedule_update(self):
+ loan = create_loan(self.applicant2, "Personal Loan", 200000, "Repay Over Number of Periods", 4,
+ applicant_type='Customer', repayment_start_date='2021-04-30', posting_date='2021-04-01')
+
+ loan.submit()
+
+ make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date='2021-04-01')
+
+ process_loan_interest_accrual_for_term_loans(posting_date='2021-05-01')
+ process_loan_interest_accrual_for_term_loans(posting_date='2021-06-01')
+
+ repayment_entry = create_repayment_entry(loan.name, self.applicant2, '2021-06-05', 120000)
+ repayment_entry.submit()
+
+ loan.load_from_db()
+
+ self.assertEqual(flt(loan.get('repayment_schedule')[3].principal_amount, 2), 41369.83)
+ self.assertEqual(flt(loan.get('repayment_schedule')[3].interest_amount, 2), 289.59)
+ self.assertEqual(flt(loan.get('repayment_schedule')[3].total_payment, 2), 41659.41)
+ self.assertEqual(flt(loan.get('repayment_schedule')[3].balance_loan_amount, 2), 0)
+
def test_security_shortfall(self):
pledges = [{
"loan_security": "Test Security 2",
@@ -938,18 +967,18 @@
def create_loan(applicant, loan_type, loan_amount, repayment_method, repayment_periods,
- repayment_start_date=None, posting_date=None):
+ applicant_type=None, repayment_start_date=None, posting_date=None):
loan = frappe.get_doc({
"doctype": "Loan",
- "applicant_type": "Employee",
+ "applicant_type": applicant_type or "Employee",
"company": "_Test Company",
"applicant": applicant,
"loan_type": loan_type,
"loan_amount": loan_amount,
"repayment_method": repayment_method,
"repayment_periods": repayment_periods,
- "repayment_start_date": nowdate(),
+ "repayment_start_date": repayment_start_date or nowdate(),
"is_term_loan": 1,
"posting_date": posting_date or nowdate()
})
diff --git a/erpnext/loan_management/doctype/loan_application/loan_application.py b/erpnext/loan_management/doctype/loan_application/loan_application.py
index 24d8d68..a8ffcb9 100644
--- a/erpnext/loan_management/doctype/loan_application/loan_application.py
+++ b/erpnext/loan_management/doctype/loan_application/loan_application.py
@@ -80,7 +80,7 @@
if self.is_term_loan:
if self.repayment_method == "Repay Over Number of Periods":
- self.repayment_amount = get_monthly_repayment_amount(self.repayment_method, self.loan_amount, self.rate_of_interest, self.repayment_periods)
+ self.repayment_amount = get_monthly_repayment_amount(self.loan_amount, self.rate_of_interest, self.repayment_periods)
if self.repayment_method == "Repay Fixed Amount per Period":
monthly_interest_rate = flt(self.rate_of_interest) / (12 *100)
diff --git a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py
index 93b4af9..e2d758b 100644
--- a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py
+++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py
@@ -176,20 +176,19 @@
@frappe.whitelist()
def get_disbursal_amount(loan, on_current_security_price=0):
+ from erpnext.loan_management.doctype.loan_repayment.loan_repayment import (
+ get_pending_principal_amount,
+ )
+
loan_details = frappe.get_value("Loan", loan, ["loan_amount", "disbursed_amount", "total_payment",
"total_principal_paid", "total_interest_payable", "status", "is_term_loan", "is_secured_loan",
- "maximum_loan_amount"], as_dict=1)
+ "maximum_loan_amount", "written_off_amount"], as_dict=1)
if loan_details.is_secured_loan and frappe.get_all('Loan Security Shortfall', filters={'loan': loan,
'status': 'Pending'}):
return 0
- if loan_details.status == 'Disbursed':
- pending_principal_amount = flt(loan_details.total_payment) - flt(loan_details.total_interest_payable) \
- - flt(loan_details.total_principal_paid)
- else:
- pending_principal_amount = flt(loan_details.disbursed_amount) - flt(loan_details.total_interest_payable) \
- - flt(loan_details.total_principal_paid)
+ pending_principal_amount = get_pending_principal_amount(loan_details)
security_value = 0.0
if loan_details.is_secured_loan and on_current_security_price:
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 e945d49..0de073f 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
@@ -74,6 +74,39 @@
})
)
+ if self.payable_principal_amount:
+ gle_map.append(
+ self.get_gl_dict({
+ "account": self.loan_account,
+ "party_type": self.applicant_type,
+ "party": self.applicant,
+ "against": self.interest_income_account,
+ "debit": self.payable_principal_amount,
+ "debit_in_account_currency": self.interest_amount,
+ "against_voucher_type": "Loan",
+ "against_voucher": self.loan,
+ "remarks": _("Interest accrued from {0} to {1} against loan: {2}").format(
+ self.last_accrual_date, self.posting_date, self.loan),
+ "cost_center": erpnext.get_default_cost_center(self.company),
+ "posting_date": self.posting_date
+ })
+ )
+
+ gle_map.append(
+ self.get_gl_dict({
+ "account": self.interest_income_account,
+ "against": self.loan_account,
+ "credit": self.payable_principal_amount,
+ "credit_in_account_currency": self.interest_amount,
+ "against_voucher_type": "Loan",
+ "against_voucher": self.loan,
+ "remarks": ("Interest accrued from {0} to {1} against loan: {2}").format(
+ self.last_accrual_date, self.posting_date, self.loan),
+ "cost_center": erpnext.get_default_cost_center(self.company),
+ "posting_date": self.posting_date
+ })
+ )
+
if gle_map:
make_gl_entries(gle_map, cancel=cancel, adv_adj=adv_adj)
@@ -82,7 +115,10 @@
# rate of interest is 13.5 then first loan interest accural will be on '01-10-2019'
# which means interest will be accrued for 30 days which should be equal to 11095.89
def calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_interest, accrual_type):
- from erpnext.loan_management.doctype.loan_repayment.loan_repayment import calculate_amounts
+ from erpnext.loan_management.doctype.loan_repayment.loan_repayment import (
+ calculate_amounts,
+ get_pending_principal_amount,
+ )
no_of_days = get_no_of_days_for_interest_accural(loan, posting_date)
precision = cint(frappe.db.get_default("currency_precision")) or 2
@@ -90,12 +126,7 @@
if no_of_days <= 0:
return
- if loan.status == 'Disbursed':
- pending_principal_amount = flt(loan.total_payment) - flt(loan.total_interest_payable) \
- - flt(loan.total_principal_paid) - flt(loan.written_off_amount)
- else:
- pending_principal_amount = flt(loan.disbursed_amount) - flt(loan.total_interest_payable) \
- - flt(loan.total_principal_paid) - flt(loan.written_off_amount)
+ pending_principal_amount = get_pending_principal_amount(loan)
interest_per_day = get_per_day_interest(pending_principal_amount, loan.rate_of_interest, posting_date)
payable_interest = interest_per_day * no_of_days
@@ -133,7 +164,7 @@
if not open_loans:
open_loans = frappe.get_all("Loan",
- fields=["name", "total_payment", "total_amount_paid", "loan_account", "interest_income_account",
+ fields=["name", "total_payment", "total_amount_paid", "loan_account", "interest_income_account", "loan_amount",
"is_term_loan", "status", "disbursement_date", "disbursed_amount", "applicant_type", "applicant",
"rate_of_interest", "total_interest_payable", "written_off_amount", "total_principal_paid", "repayment_start_date"],
filters=query_filters)
@@ -190,7 +221,8 @@
AND l.is_term_loan =1
AND rs.payment_date <= %s
AND rs.is_accrued=0 {0}
- AND l.status = 'Disbursed'""".format(condition), (getdate(date)), as_dict=1)
+ AND l.status = 'Disbursed'
+ ORDER BY rs.payment_date""".format(condition), (getdate(date)), as_dict=1)
return term_loans
diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json
index 6479853..93ef217 100644
--- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json
+++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json
@@ -13,8 +13,10 @@
"column_break_3",
"company",
"posting_date",
- "is_term_loan",
"rate_of_interest",
+ "payroll_payable_account",
+ "is_term_loan",
+ "repay_from_salary",
"payment_details_section",
"due_date",
"pending_principal_amount",
@@ -243,15 +245,31 @@
"label": "Total Penalty Paid",
"options": "Company:company:default_currency",
"read_only": 1
+ },
+ {
+ "depends_on": "eval:doc.repay_from_salary",
+ "fieldname": "payroll_payable_account",
+ "fieldtype": "Link",
+ "label": "Payroll Payable Account",
+ "mandatory_depends_on": "eval:doc.repay_from_salary",
+ "options": "Account"
+ },
+ {
+ "default": "0",
+ "fetch_from": "against_loan.repay_from_salary",
+ "fieldname": "repay_from_salary",
+ "fieldtype": "Check",
+ "label": "Repay From Salary"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2021-04-19 18:10:00.935364",
+ "modified": "2022-01-06 01:51:06.707782",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Repayment",
+ "naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
@@ -287,5 +305,6 @@
],
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
index 5922e4f..7e997e8 100644
--- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
+++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
@@ -35,9 +35,12 @@
def on_submit(self):
self.update_paid_amount()
+ self.update_repayment_schedule()
self.make_gl_entries()
def on_cancel(self):
+ self.check_future_accruals()
+ self.update_repayment_schedule(cancel=1)
self.mark_as_unpaid()
self.ignore_linked_doctypes = ['GL Entry']
self.make_gl_entries(cancel=1)
@@ -90,7 +93,7 @@
def book_unaccrued_interest(self):
precision = cint(frappe.db.get_default("currency_precision")) or 2
- if self.total_interest_paid > self.interest_payable:
+ if flt(self.total_interest_paid, precision) > flt(self.interest_payable, precision):
if not self.is_term_loan:
# get last loan interest accrual date
last_accrual_date = get_last_accrual_date(self.against_loan)
@@ -121,7 +124,18 @@
})
def update_paid_amount(self):
- loan = frappe.get_doc("Loan", self.against_loan)
+ loan = frappe.get_value("Loan", self.against_loan, ['total_amount_paid', 'total_principal_paid',
+ 'status', 'is_secured_loan', 'total_payment', 'loan_amount', 'total_interest_payable',
+ 'written_off_amount'], as_dict=1)
+
+ loan.update({
+ 'total_amount_paid': loan.total_amount_paid + self.amount_paid,
+ 'total_principal_paid': loan.total_principal_paid + self.principal_amount_paid
+ })
+
+ pending_principal_amount = get_pending_principal_amount(loan)
+ if not loan.is_secured_loan and pending_principal_amount <= 0:
+ loan.update({'status': 'Loan Closure Requested'})
for payment in self.repayment_details:
frappe.db.sql(""" UPDATE `tabLoan Interest Accrual`
@@ -130,17 +144,31 @@
WHERE name = %s""",
(flt(payment.paid_principal_amount), flt(payment.paid_interest_amount), payment.loan_interest_accrual))
- frappe.db.sql(""" UPDATE `tabLoan` SET total_amount_paid = %s, total_principal_paid = %s
- WHERE name = %s """, (loan.total_amount_paid + self.amount_paid,
- loan.total_principal_paid + self.principal_amount_paid, self.against_loan))
+ frappe.db.sql(""" UPDATE `tabLoan`
+ SET total_amount_paid = %s, total_principal_paid = %s, status = %s
+ WHERE name = %s """, (loan.total_amount_paid, loan.total_principal_paid, loan.status,
+ self.against_loan))
update_shortfall_status(self.against_loan, self.principal_amount_paid)
def mark_as_unpaid(self):
- loan = frappe.get_doc("Loan", self.against_loan)
+ loan = frappe.get_value("Loan", self.against_loan, ['total_amount_paid', 'total_principal_paid',
+ 'status', 'is_secured_loan', 'total_payment', 'loan_amount', 'total_interest_payable',
+ 'written_off_amount'], as_dict=1)
no_of_repayments = len(self.repayment_details)
+ loan.update({
+ 'total_amount_paid': loan.total_amount_paid - self.amount_paid,
+ 'total_principal_paid': loan.total_principal_paid - self.principal_amount_paid
+ })
+
+ if loan.status == 'Loan Closure Requested':
+ if loan.disbursed_amount >= loan.loan_amount:
+ loan['status'] = 'Disbursed'
+ else:
+ loan['status'] = 'Partially Disbursed'
+
for payment in self.repayment_details:
frappe.db.sql(""" UPDATE `tabLoan Interest Accrual`
SET paid_principal_amount = `paid_principal_amount` - %s,
@@ -154,12 +182,20 @@
lia_doc = frappe.get_doc('Loan Interest Accrual', payment.loan_interest_accrual)
lia_doc.cancel()
- frappe.db.sql(""" UPDATE `tabLoan` SET total_amount_paid = %s, total_principal_paid = %s
- WHERE name = %s """, (loan.total_amount_paid - self.amount_paid,
- loan.total_principal_paid - self.principal_amount_paid, self.against_loan))
+ frappe.db.sql(""" UPDATE `tabLoan`
+ SET total_amount_paid = %s, total_principal_paid = %s, status = %s
+ WHERE name = %s """, (loan.total_amount_paid, loan.total_principal_paid, loan.status, self.against_loan))
- if loan.status == "Loan Closure Requested":
- frappe.db.set_value("Loan", self.against_loan, "status", "Disbursed")
+ def check_future_accruals(self):
+ future_accrual_date = frappe.db.get_value("Loan Interest Accrual", {"posting_date": (">", self.posting_date),
+ "docstatus": 1, "loan": self.against_loan}, 'posting_date')
+
+ if future_accrual_date:
+ frappe.throw("Cannot cancel. Interest accruals already processed till {0}".format(get_datetime(future_accrual_date)))
+
+ def update_repayment_schedule(self, cancel=0):
+ if self.is_term_loan and self.principal_amount_paid > self.payable_principal_amount:
+ regenerate_repayment_schedule(self.against_loan, cancel)
def allocate_amounts(self, repayment_details):
self.set('repayment_details', [])
@@ -182,50 +218,93 @@
interest_paid -= self.total_penalty_paid
- total_interest_paid = 0
- # interest_paid = self.amount_paid - self.principal_amount_paid - self.penalty_amount
+ if self.is_term_loan:
+ interest_paid, updated_entries = self.allocate_interest_amount(interest_paid, repayment_details)
+ self.allocate_principal_amount_for_term_loans(interest_paid, repayment_details, updated_entries)
+ else:
+ interest_paid, updated_entries = self.allocate_interest_amount(interest_paid, repayment_details)
+ self.allocate_excess_payment_for_demand_loans(interest_paid, repayment_details)
+
+ def allocate_interest_amount(self, interest_paid, repayment_details):
+ updated_entries = {}
+ self.total_interest_paid = 0
+ idx = 1
if interest_paid > 0:
for lia, amounts in repayment_details.get('pending_accrual_entries', []).items():
- if amounts['interest_amount'] + amounts['payable_principal_amount'] <= interest_paid:
+ interest_amount = 0
+ if amounts['interest_amount'] <= interest_paid:
interest_amount = amounts['interest_amount']
- paid_principal = amounts['payable_principal_amount']
- self.principal_amount_paid += paid_principal
- interest_paid -= (interest_amount + paid_principal)
+ self.total_interest_paid += interest_amount
+ interest_paid -= interest_amount
elif interest_paid:
if interest_paid >= amounts['interest_amount']:
interest_amount = amounts['interest_amount']
- paid_principal = interest_paid - interest_amount
- self.principal_amount_paid += paid_principal
+ self.total_interest_paid += interest_amount
interest_paid = 0
else:
interest_amount = interest_paid
+ self.total_interest_paid += interest_amount
interest_paid = 0
- paid_principal=0
- total_interest_paid += interest_amount
- self.append('repayment_details', {
- 'loan_interest_accrual': lia,
- 'paid_interest_amount': interest_amount,
- 'paid_principal_amount': paid_principal
- })
+ if interest_amount:
+ self.append('repayment_details', {
+ 'loan_interest_accrual': lia,
+ 'paid_interest_amount': interest_amount,
+ 'paid_principal_amount': 0
+ })
+ updated_entries[lia] = idx
+ idx += 1
+ return interest_paid, updated_entries
+
+ def allocate_principal_amount_for_term_loans(self, interest_paid, repayment_details, updated_entries):
+ if interest_paid > 0:
+ for lia, amounts in repayment_details.get('pending_accrual_entries', []).items():
+ paid_principal = 0
+ if amounts['payable_principal_amount'] <= interest_paid:
+ paid_principal = amounts['payable_principal_amount']
+ self.principal_amount_paid += paid_principal
+ interest_paid -= paid_principal
+ elif interest_paid:
+ if interest_paid >= amounts['payable_principal_amount']:
+ paid_principal = amounts['payable_principal_amount']
+ self.principal_amount_paid += paid_principal
+ interest_paid = 0
+ else:
+ paid_principal = interest_paid
+ self.principal_amount_paid += paid_principal
+ interest_paid = 0
+
+ if updated_entries.get(lia):
+ idx = updated_entries.get(lia)
+ self.get('repayment_details')[idx-1].paid_principal_amount += paid_principal
+ else:
+ self.append('repayment_details', {
+ 'loan_interest_accrual': lia,
+ 'paid_interest_amount': 0,
+ 'paid_principal_amount': paid_principal
+ })
+
+ if interest_paid > 0:
+ self.principal_amount_paid += interest_paid
+
+ def allocate_excess_payment_for_demand_loans(self, interest_paid, repayment_details):
if repayment_details['unaccrued_interest'] and interest_paid > 0:
# no of days for which to accrue interest
# Interest can only be accrued for an entire day and not partial
if interest_paid > repayment_details['unaccrued_interest']:
interest_paid -= repayment_details['unaccrued_interest']
- total_interest_paid += repayment_details['unaccrued_interest']
+ self.total_interest_paid += repayment_details['unaccrued_interest']
else:
# get no of days for which interest can be paid
per_day_interest = get_per_day_interest(self.pending_principal_amount,
self.rate_of_interest, self.posting_date)
no_of_days = cint(interest_paid/per_day_interest)
- total_interest_paid += no_of_days * per_day_interest
+ self.total_interest_paid += no_of_days * per_day_interest
interest_paid -= no_of_days * per_day_interest
- self.total_interest_paid = total_interest_paid
if interest_paid > 0:
self.principal_amount_paid += interest_paid
@@ -241,74 +320,79 @@
else:
remarks = _("Repayment against Loan: ") + self.against_loan
- if not loan_details.repay_from_salary:
- if self.total_penalty_paid:
- gle_map.append(
- self.get_gl_dict({
- "account": loan_details.loan_account,
- "against": loan_details.payment_account,
- "debit": self.total_penalty_paid,
- "debit_in_account_currency": self.total_penalty_paid,
- "against_voucher_type": "Loan",
- "against_voucher": self.against_loan,
- "remarks": _("Penalty against loan:") + self.against_loan,
- "cost_center": self.cost_center,
- "party_type": self.applicant_type,
- "party": self.applicant,
- "posting_date": getdate(self.posting_date)
- })
- )
+ if self.repay_from_salary:
+ payment_account = self.payroll_payable_account
+ else:
+ payment_account = loan_details.payment_account
- gle_map.append(
- self.get_gl_dict({
- "account": loan_details.penalty_income_account,
- "against": loan_details.payment_account,
- "credit": self.total_penalty_paid,
- "credit_in_account_currency": self.total_penalty_paid,
- "against_voucher_type": "Loan",
- "against_voucher": self.against_loan,
- "remarks": _("Penalty against loan:") + self.against_loan,
- "cost_center": self.cost_center,
- "posting_date": getdate(self.posting_date)
- })
- )
-
- gle_map.append(
- self.get_gl_dict({
- "account": loan_details.payment_account,
- "against": loan_details.loan_account + ", " + loan_details.interest_income_account
- + ", " + loan_details.penalty_income_account,
- "debit": self.amount_paid,
- "debit_in_account_currency": self.amount_paid,
- "against_voucher_type": "Loan",
- "against_voucher": self.against_loan,
- "remarks": remarks,
- "cost_center": self.cost_center,
- "posting_date": getdate(self.posting_date)
- })
- )
-
+ if self.total_penalty_paid:
gle_map.append(
self.get_gl_dict({
"account": loan_details.loan_account,
- "party_type": loan_details.applicant_type,
- "party": loan_details.applicant,
"against": loan_details.payment_account,
- "credit": self.amount_paid,
- "credit_in_account_currency": self.amount_paid,
+ "debit": self.total_penalty_paid,
+ "debit_in_account_currency": self.total_penalty_paid,
"against_voucher_type": "Loan",
"against_voucher": self.against_loan,
- "remarks": remarks,
+ "remarks": _("Penalty against loan:") + self.against_loan,
+ "cost_center": self.cost_center,
+ "party_type": self.applicant_type,
+ "party": self.applicant,
+ "posting_date": getdate(self.posting_date)
+ })
+ )
+
+ gle_map.append(
+ self.get_gl_dict({
+ "account": loan_details.penalty_income_account,
+ "against": payment_account,
+ "credit": self.total_penalty_paid,
+ "credit_in_account_currency": self.total_penalty_paid,
+ "against_voucher_type": "Loan",
+ "against_voucher": self.against_loan,
+ "remarks": _("Penalty against loan:") + self.against_loan,
"cost_center": self.cost_center,
"posting_date": getdate(self.posting_date)
})
)
- if gle_map:
- make_gl_entries(gle_map, cancel=cancel, adv_adj=adv_adj, merge_entries=False)
+ gle_map.append(
+ self.get_gl_dict({
+ "account": payment_account,
+ "against": loan_details.loan_account + ", " + loan_details.interest_income_account
+ + ", " + loan_details.penalty_income_account,
+ "debit": self.amount_paid,
+ "debit_in_account_currency": self.amount_paid,
+ "against_voucher_type": "Loan",
+ "against_voucher": self.against_loan,
+ "remarks": remarks,
+ "cost_center": self.cost_center,
+ "posting_date": getdate(self.posting_date)
+ })
+ )
+
+ gle_map.append(
+ self.get_gl_dict({
+ "account": loan_details.loan_account,
+ "party_type": loan_details.applicant_type,
+ "party": loan_details.applicant,
+ "against": payment_account,
+ "credit": self.amount_paid,
+ "credit_in_account_currency": self.amount_paid,
+ "against_voucher_type": "Loan",
+ "against_voucher": self.against_loan,
+ "remarks": remarks,
+ "cost_center": self.cost_center,
+ "posting_date": getdate(self.posting_date)
+ })
+ )
+
+ if gle_map:
+ make_gl_entries(gle_map, cancel=cancel, adv_adj=adv_adj, merge_entries=False)
def create_repayment_entry(loan, applicant, company, posting_date, loan_type,
- payment_type, interest_payable, payable_principal_amount, amount_paid, penalty_amount=None):
+ payment_type, interest_payable, payable_principal_amount, amount_paid, penalty_amount=None,
+ payroll_payable_account=None):
lr = frappe.get_doc({
"doctype": "Loan Repayment",
@@ -321,7 +405,8 @@
"interest_payable": interest_payable,
"payable_principal_amount": payable_principal_amount,
"amount_paid": amount_paid,
- "loan_type": loan_type
+ "loan_type": loan_type,
+ "payroll_payable_account": payroll_payable_account
}).insert()
return lr
@@ -361,6 +446,76 @@
else:
return None, 0
+def regenerate_repayment_schedule(loan, cancel=0):
+ from erpnext.loan_management.doctype.loan.loan import (
+ add_single_month,
+ get_monthly_repayment_amount,
+ )
+
+ loan_doc = frappe.get_doc('Loan', loan)
+ next_accrual_date = None
+ accrued_entries = 0
+ last_repayment_amount = 0
+ last_balance_amount = 0
+
+ for term in reversed(loan_doc.get('repayment_schedule')):
+ if not term.is_accrued:
+ next_accrual_date = term.payment_date
+ loan_doc.remove(term)
+ else:
+ accrued_entries += 1
+ if not last_repayment_amount:
+ last_repayment_amount = term.total_payment
+ if not last_balance_amount:
+ last_balance_amount = term.balance_loan_amount
+
+ loan_doc.save()
+
+ balance_amount = get_pending_principal_amount(loan_doc)
+
+ if loan_doc.repayment_method == 'Repay Fixed Amount per Period':
+ monthly_repayment_amount = flt(balance_amount/len(loan_doc.get('repayment_schedule')) - accrued_entries)
+ else:
+ if not cancel:
+ monthly_repayment_amount = get_monthly_repayment_amount(balance_amount,
+ loan_doc.rate_of_interest, loan_doc.repayment_periods - accrued_entries)
+ else:
+ monthly_repayment_amount = last_repayment_amount
+ balance_amount = last_balance_amount
+
+ payment_date = next_accrual_date
+
+ while(balance_amount > 0):
+ interest_amount = flt(balance_amount * flt(loan_doc.rate_of_interest) / (12*100))
+ principal_amount = monthly_repayment_amount - interest_amount
+ balance_amount = flt(balance_amount + interest_amount - monthly_repayment_amount)
+ if balance_amount < 0:
+ principal_amount += balance_amount
+ balance_amount = 0.0
+
+ total_payment = principal_amount + interest_amount
+ loan_doc.append("repayment_schedule", {
+ "payment_date": payment_date,
+ "principal_amount": principal_amount,
+ "interest_amount": interest_amount,
+ "total_payment": total_payment,
+ "balance_loan_amount": balance_amount
+ })
+ next_payment_date = add_single_month(payment_date)
+ payment_date = next_payment_date
+
+ loan_doc.save()
+
+def get_pending_principal_amount(loan):
+ if loan.status in ('Disbursed', 'Closed') or loan.disbursed_amount >= loan.loan_amount:
+ pending_principal_amount = flt(loan.total_payment) - flt(loan.total_principal_paid) \
+ - flt(loan.total_interest_payable) - flt(loan.written_off_amount)
+ else:
+ pending_principal_amount = flt(loan.disbursed_amount) - flt(loan.total_principal_paid) \
+ - flt(loan.total_interest_payable) - flt(loan.written_off_amount)
+
+ return pending_principal_amount
+
# This function returns the amounts that are payable at the time of loan repayment based on posting date
# So it pulls all the unpaid Loan Interest Accrual Entries and calculates the penalty if applicable
@@ -408,12 +563,7 @@
if due_date and not final_due_date:
final_due_date = add_days(due_date, loan_type_details.grace_period_in_days)
- if against_loan_doc.status in ('Disbursed', 'Closed') or against_loan_doc.disbursed_amount >= against_loan_doc.loan_amount:
- pending_principal_amount = against_loan_doc.total_payment - against_loan_doc.total_principal_paid \
- - against_loan_doc.total_interest_payable - against_loan_doc.written_off_amount
- else:
- pending_principal_amount = against_loan_doc.disbursed_amount - against_loan_doc.total_principal_paid \
- - against_loan_doc.total_interest_payable - against_loan_doc.written_off_amount
+ pending_principal_amount = get_pending_principal_amount(against_loan_doc)
unaccrued_interest = 0
if due_date:
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 bff9d5c..4567374 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
@@ -27,6 +27,9 @@
d.idx, frappe.bold(d.loan_security)))
def validate_unpledge_qty(self):
+ from erpnext.loan_management.doctype.loan_repayment.loan_repayment import (
+ get_pending_principal_amount,
+ )
from erpnext.loan_management.doctype.loan_security_shortfall.loan_security_shortfall import (
get_ltv_ratio,
)
@@ -43,15 +46,10 @@
"valid_upto": (">=", get_datetime())
}, as_list=1))
- loan_details = frappe.get_value("Loan", self.loan, ['total_payment', 'total_principal_paid',
+ loan_details = frappe.get_value("Loan", self.loan, ['total_payment', 'total_principal_paid', 'loan_amount',
'total_interest_payable', 'written_off_amount', 'disbursed_amount', 'status'], as_dict=1)
- if loan_details.status == 'Disbursed':
- pending_principal_amount = flt(loan_details.total_payment) - flt(loan_details.total_interest_payable) \
- - flt(loan_details.total_principal_paid) - flt(loan_details.written_off_amount)
- else:
- pending_principal_amount = flt(loan_details.disbursed_amount) - flt(loan_details.total_interest_payable) \
- - flt(loan_details.total_principal_paid) - flt(loan_details.written_off_amount)
+ pending_principal_amount = get_pending_principal_amount(loan_details)
security_value = 0
unpledge_qty_map = {}
diff --git a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py
index db126cd..2ffae1a 100644
--- a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py
+++ b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py
@@ -323,10 +323,14 @@
target.maintenance_schedule = source.name
target.maintenance_schedule_detail = s_id
- def update_sales(source, target, parent):
+ def update_sales_and_serial(source, target, parent):
sales_person = frappe.db.get_value('Maintenance Schedule Detail', s_id, 'sales_person')
target.service_person = sales_person
- target.serial_no = ''
+ serial_nos = get_serial_nos(target.serial_no)
+ if len(serial_nos) == 1:
+ target.serial_no = serial_nos[0]
+ else:
+ target.serial_no = ''
doclist = get_mapped_doc("Maintenance Schedule", source_name, {
"Maintenance Schedule": {
@@ -342,7 +346,7 @@
"Maintenance Schedule Item": {
"doctype": "Maintenance Visit Purpose",
"condition": lambda doc: doc.item_name == item_name,
- "postprocess": update_sales
+ "postprocess": update_sales_and_serial
}
}, target_doc)
diff --git a/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py b/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py
index 37ea3fd..5017126 100644
--- a/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py
+++ b/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py
@@ -56,9 +56,14 @@
ms.submit()
s_id = ms.get_pending_data(data_type = "id", item_name = i.item_name, s_date = expected_dates[1])
- test = make_maintenance_visit(source_name = ms.name, item_name = "_Test Item", s_id = s_id)
+
+ # Check if item is mapped in visit.
+ test_map_visit = make_maintenance_visit(source_name = ms.name, item_name = "_Test Item", s_id = s_id)
+ self.assertEqual(len(test_map_visit.purposes), 1)
+ self.assertEqual(test_map_visit.purposes[0].item_name, "_Test Item")
+
visit = frappe.new_doc('Maintenance Visit')
- visit = test
+ visit = test_map_visit
visit.maintenance_schedule = ms.name
visit.maintenance_schedule_detail = s_id
visit.completion_status = "Partially Completed"
diff --git a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js
index 7f98354..d2197a6 100644
--- a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js
+++ b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js
@@ -43,14 +43,11 @@
}
});
}
- else {
- frm.clear_table("purposes");
- }
-
if (!frm.doc.status) {
frm.set_value({ status: 'Draft' });
}
if (frm.doc.__islocal) {
+ frm.doc.maintenance_type == 'Unscheduled' && frm.clear_table("purposes");
frm.set_value({ mntc_date: frappe.datetime.get_today() });
}
},
diff --git a/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py b/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py
index d5db3fc..eff2344 100644
--- a/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py
+++ b/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py
@@ -1,17 +1,15 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
-
-import unittest
-
import frappe
from frappe.utils import add_months, today
from erpnext import get_company_currency
+from erpnext.tests.utils import ERPNextTestCase
from .blanket_order import make_order
-class TestBlanketOrder(unittest.TestCase):
+class TestBlanketOrder(ERPNextTestCase):
def setUp(self):
frappe.flags.args = frappe._dict()
diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js
index 5f5c20a..6d35d65 100644
--- a/erpnext/manufacturing/doctype/bom/bom.js
+++ b/erpnext/manufacturing/doctype/bom/bom.js
@@ -680,12 +680,6 @@
erpnext.bom.calculate_total(frm.doc);
});
-frappe.ui.form.on("BOM", "with_operations", function(frm) {
- if(!cint(frm.doc.with_operations)) {
- frm.set_value("operations", []);
- }
-});
-
frappe.tour['BOM'] = [
{
fieldname: "item",
diff --git a/erpnext/manufacturing/doctype/bom/bom.json b/erpnext/manufacturing/doctype/bom/bom.json
index 6218707..218ac64 100644
--- a/erpnext/manufacturing/doctype/bom/bom.json
+++ b/erpnext/manufacturing/doctype/bom/bom.json
@@ -237,6 +237,7 @@
"options": "Price List"
},
{
+ "depends_on": "with_operations",
"fieldname": "operations_section",
"fieldtype": "Section Break",
"hide_border": 1,
@@ -539,7 +540,7 @@
"image_field": "image",
"is_submittable": 1,
"links": [],
- "modified": "2021-10-27 14:52:04.500251",
+ "modified": "2021-11-18 13:04:16.271975",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM",
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index 2cd8f8c..5a60fb7 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -530,16 +530,6 @@
row.hour_rate = (hour_rate / flt(self.conversion_rate)
if self.conversion_rate and hour_rate else hour_rate)
- if self.routing:
- time_in_mins = flt(frappe.db.get_value("BOM Operation", {
- "workstation": row.workstation,
- "operation": row.operation,
- "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
@@ -922,7 +912,7 @@
rm_item_exists = True
if bom.item.lower() == item.lower() or \
bom.item.lower() == cstr(frappe.db.get_value("Item", item, "variant_of")).lower():
- rm_item_exists = True
+ rm_item_exists = True
if not rm_item_exists:
frappe.throw(_("BOM {0} does not belong to Item {1}").format(bom_no, item))
diff --git a/erpnext/manufacturing/doctype/bom/bom_item_preview.html b/erpnext/manufacturing/doctype/bom/bom_item_preview.html
index e614a7e..eb4135e 100644
--- a/erpnext/manufacturing/doctype/bom/bom_item_preview.html
+++ b/erpnext/manufacturing/doctype/bom/bom_item_preview.html
@@ -16,26 +16,15 @@
</div>
<hr style="margin: 15px -15px;">
<p>
- {% if data.value %}
- <a style="margin-right: 7px; margin-bottom: 7px" class="btn btn-default btn-xs" href="#Form/BOM/{{ data.value }}">
+ {% if data.value && data.value != "BOM" %}
+ <a style="margin-right: 7px; margin-bottom: 7px" class="btn btn-default btn-xs" href="/app/bom/{{ data.value }}">
{{ __("Open BOM {0}", [data.value.bold()]) }}</a>
{% endif %}
{% if data.item_code %}
- <a class="btn btn-default btn-xs" href="#Form/Item/{{ data.item_code }}">
+ <a style="margin-right: 7px; margin-bottom: 7px" class="btn btn-default btn-xs" href="/app/item/{{ data.item_code }}">
{{ __("Open Item {0}", [data.item_code.bold()]) }}</a>
{% endif %}
</p>
</div>
</div>
- <hr style="margin: 15px -15px;">
- <p>
- {% if data.value %}
- <a style="margin-right: 7px; margin-bottom: 7px" class="btn btn-default btn-xs" href="/app/Form/BOM/{{ data.value }}">
- {{ __("Open BOM {0}", [data.value.bold()]) }}</a>
- {% endif %}
- {% if data.item_code %}
- <a class="btn btn-default btn-xs" href="/app/Form/Item/{{ data.item_code }}">
- {{ __("Open Item {0}", [data.item_code.bold()]) }}</a>
- {% endif %}
- </p>
</div>
diff --git a/erpnext/manufacturing/doctype/bom/bom_tree.js b/erpnext/manufacturing/doctype/bom/bom_tree.js
index 6e2599e..fb99add 100644
--- a/erpnext/manufacturing/doctype/bom/bom_tree.js
+++ b/erpnext/manufacturing/doctype/bom/bom_tree.js
@@ -66,6 +66,7 @@
var bom = frappe.model.get_doc("BOM", node.data.value);
node.data.image = escape(bom.image) || "";
node.data.description = bom.description || "";
+ node.data.item_code = bom.item || "";
});
}
},
diff --git a/erpnext/manufacturing/doctype/bom/test_bom.js b/erpnext/manufacturing/doctype/bom/test_bom.js
deleted file mode 100644
index 98a9198..0000000
--- a/erpnext/manufacturing/doctype/bom/test_bom.js
+++ /dev/null
@@ -1,63 +0,0 @@
-QUnit.test("test: item", function (assert) {
- assert.expect(1);
- let done = assert.async();
-
- frappe.run_serially([
- // test item creation
- () => frappe.set_route("List", "Item"),
-
- // Create a BOM for a laptop
- () => frappe.tests.make(
- "BOM", [
- {item: "Laptop"},
- {quantity: 1},
- {with_operations: 1},
- {company: "For Testing"},
- {operations: [
- [
- {operation: "Assemble CPU"},
- {time_in_mins: 60},
- ],
- [
- {operation: "Assemble Keyboard"},
- {time_in_mins: 30},
- ],
- [
- {operation: "Assemble Screen"},
- {time_in_mins: 30},
- ]
- ]},
- {scrap_items: [
- [
- {item_code: "Scrap item"}
- ]
- ]},
- {items: [
- [
- {item_code: "CPU"},
- {qty: 1}
- ],
- [
- {item_code: "Keyboard"},
- {qty: 1}
- ],
- [
- {item_code: "Screen"},
- {qty: 1}
- ]
- ]},
- ]
- ),
- () => cur_frm.savesubmit(),
- () => frappe.timeout(1),
- () => frappe.click_button('Yes'),
- () => frappe.timeout(1),
-
- () => {
- assert.ok(cur_frm.doc.operating_cost + cur_frm.doc.raw_material_cost -
- cur_frm.doc.scrap_material_cost == cur_frm.doc.total_cost, 'Total_Cost calculated correctly');
- },
-
- () => done()
- ]);
-});
diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py
index 4c03230..178d92c 100644
--- a/erpnext/manufacturing/doctype/bom/test_bom.py
+++ b/erpnext/manufacturing/doctype/bom/test_bom.py
@@ -2,7 +2,6 @@
# License: GNU General Public License v3. See license.txt
-import unittest
from collections import deque
from functools import partial
@@ -18,10 +17,11 @@
create_stock_reconciliation,
)
from erpnext.tests.test_subcontracting import set_backflush_based_on
+from erpnext.tests.utils import ERPNextTestCase
test_records = frappe.get_test_records('BOM')
-class TestBOM(unittest.TestCase):
+class TestBOM(ERPNextTestCase):
def setUp(self):
if not frappe.get_value('Item', '_Test Item'):
make_test_records('Item')
diff --git a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json
index ec617f3..c7be7ef 100644
--- a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json
+++ b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json
@@ -11,6 +11,7 @@
"col_break1",
"workstation",
"time_in_mins",
+ "fixed_time",
"costing_section",
"hour_rate",
"base_hour_rate",
@@ -80,6 +81,14 @@
"reqd": 1
},
{
+ "default": "0",
+ "description": "Operation time does not depend on quantity to produce",
+ "fieldname": "fixed_time",
+ "fieldtype": "Check",
+ "in_list_view": 1,
+ "label": "Fixed Time"
+ },
+ {
"fieldname": "operating_cost",
"fieldtype": "Currency",
"in_list_view": 1,
@@ -177,12 +186,13 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-09-13 16:45:01.092868",
+ "modified": "2021-12-15 03:00:00.473173",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM Operation",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
- "sort_order": "DESC"
+ "sort_order": "DESC",
+ "states": []
}
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py b/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py
index 526c243..12576cb 100644
--- a/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py
+++ b/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py
@@ -1,19 +1,16 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
-
-
-import unittest
-
import frappe
from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
from erpnext.stock.doctype.item.test_item import create_item
+from erpnext.tests.utils import ERPNextTestCase
test_records = frappe.get_test_records('BOM')
-class TestBOMUpdateTool(unittest.TestCase):
+class TestBOMUpdateTool(ERPNextTestCase):
def test_replace_bom(self):
current_bom = "BOM-_Test Item Home Desktop Manufactured-001"
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js
index f9259fb..d85b8a6 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.js
+++ b/erpnext/manufacturing/doctype/job_card/job_card.js
@@ -23,18 +23,12 @@
);
},
- onload: function(frm) {
- if (frm.doc.scrap_items.length == 0) {
- frm.fields_dict['scrap_items_section'].collapse();
- }
- },
-
refresh: function(frm) {
frappe.flags.pause_job = 0;
frappe.flags.resume_job = 0;
let has_items = frm.doc.items && frm.doc.items.length;
- if (frm.doc.__onload.work_order_stopped) {
+ if (frm.doc.__onload.work_order_closed) {
frm.disable_save();
return;
}
@@ -81,6 +75,32 @@
&& (frm.doc.items || !frm.doc.items.length || frm.doc.for_quantity == frm.doc.transferred_qty)) {
frm.trigger("prepare_timer_buttons");
}
+
+ frm.trigger("setup_quality_inspection");
+ if (frm.doc.work_order) {
+ frappe.db.get_value('Work Order', frm.doc.work_order,
+ 'transfer_material_against').then((r) => {
+ if (r.message.transfer_material_against == 'Work Order') {
+ frm.set_df_property('items', 'hidden', 1);
+ }
+ });
+ }
+ },
+
+ setup_quality_inspection: function(frm) {
+ let quality_inspection_field = frm.get_docfield("quality_inspection");
+ quality_inspection_field.get_route_options_for_new_doc = function(frm) {
+ return {
+ "inspection_type": "In Process",
+ "reference_type": "Job Card",
+ "reference_name": frm.doc.name,
+ "item_code": frm.doc.production_item,
+ "item_name": frm.doc.item_name,
+ "item_serial_no": frm.doc.serial_no,
+ "batch_no": frm.doc.batch_no,
+ "quality_inspection_template": frm.doc.quality_inspection_template,
+ };
+ };
},
setup_corrective_job_card: function(frm) {
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.json b/erpnext/manufacturing/doctype/job_card/job_card.json
index 6528199..5a071f1 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.json
+++ b/erpnext/manufacturing/doctype/job_card/job_card.json
@@ -19,6 +19,7 @@
"serial_no",
"column_break_12",
"wip_warehouse",
+ "quality_inspection_template",
"quality_inspection",
"project",
"batch_no",
@@ -408,11 +409,18 @@
"no_copy": 1,
"options": "Job Card Scrap Item",
"print_hide": 1
+ },
+ {
+ "fetch_from": "operation.quality_inspection_template",
+ "fieldname": "quality_inspection_template",
+ "fieldtype": "Link",
+ "label": "Quality Inspection Template",
+ "options": "Quality Inspection Template"
}
],
"is_submittable": 1,
"links": [],
- "modified": "2021-11-12 10:15:03.572401",
+ "modified": "2021-11-24 19:17:40.879235",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Job Card",
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py
index 5eea032..8d00019 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.py
+++ b/erpnext/manufacturing/doctype/job_card/job_card.py
@@ -36,7 +36,7 @@
def onload(self):
excess_transfer = frappe.db.get_single_value("Manufacturing Settings", "job_card_excess_transfer")
self.set_onload("job_card_excess_transfer", excess_transfer)
- self.set_onload("work_order_stopped", self.is_work_order_stopped())
+ self.set_onload("work_order_closed", self.is_work_order_closed())
def validate(self):
self.validate_time_logs()
@@ -549,10 +549,10 @@
.format(message, bold(row.operation), bold(self.operation)), OperationSequenceError)
def validate_work_order(self):
- if self.is_work_order_stopped():
- frappe.throw(_("You can't make any changes to Job Card since Work Order is stopped."))
+ if self.is_work_order_closed():
+ frappe.throw(_("You can't make any changes to Job Card since Work Order is closed."))
- def is_work_order_stopped(self):
+ def is_work_order_closed(self):
if self.work_order:
status = frappe.get_value('Work Order', self.work_order)
@@ -627,17 +627,22 @@
@frappe.whitelist()
def make_stock_entry(source_name, target_doc=None):
- def update_item(obj, target, source_parent):
+ def update_item(source, target, source_parent):
target.t_warehouse = source_parent.wip_warehouse
+
if not target.conversion_factor:
target.conversion_factor = 1
+ pending_rm_qty = flt(source.required_qty) - flt(source.transferred_qty)
+ if pending_rm_qty > 0:
+ target.qty = pending_rm_qty
+
def set_missing_values(source, target):
target.purpose = "Material Transfer for Manufacture"
target.from_bom = 1
# avoid negative 'For Quantity'
- pending_fg_qty = source.get('for_quantity', 0) - source.get('transferred_qty', 0)
+ pending_fg_qty = flt(source.get('for_quantity', 0)) - flt(source.get('transferred_qty', 0))
target.fg_completed_qty = pending_fg_qty if pending_fg_qty > 0 else 0
target.set_transfer_qty()
diff --git a/erpnext/manufacturing/doctype/job_card/job_card_dashboard.py b/erpnext/manufacturing/doctype/job_card/job_card_dashboard.py
index acaa895..2c48872 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card_dashboard.py
+++ b/erpnext/manufacturing/doctype/job_card/job_card_dashboard.py
@@ -4,10 +4,17 @@
def get_data():
return {
'fieldname': 'job_card',
+ 'non_standard_fieldnames': {
+ 'Quality Inspection': 'reference_name'
+ },
'transactions': [
{
'label': _('Transactions'),
'items': ['Material Request', 'Stock Entry']
+ },
+ {
+ 'label': _('Reference'),
+ 'items': ['Quality Inspection']
}
]
}
diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py
index d799283..bb5004b 100644
--- a/erpnext/manufacturing/doctype/job_card/test_job_card.py
+++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py
@@ -1,6 +1,5 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
-import unittest
import frappe
from frappe.utils import random_string
@@ -12,14 +11,26 @@
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
+from erpnext.tests.utils import ERPNextTestCase
-class TestJobCard(unittest.TestCase):
-
+class TestJobCard(ERPNextTestCase):
def setUp(self):
+ make_bom_for_jc_tests()
+
transfer_material_against, source_warehouse = None, None
- tests_that_transfer_against_jc = ("test_job_card_multiple_materials_transfer",
- "test_job_card_excess_material_transfer")
+
+ tests_that_skip_setup = (
+ "test_job_card_material_transfer_correctness",
+ )
+ tests_that_transfer_against_jc = (
+ "test_job_card_multiple_materials_transfer",
+ "test_job_card_excess_material_transfer",
+ "test_job_card_partial_material_transfer"
+ )
+
+ if self._testMethodName in tests_that_skip_setup:
+ return
if self._testMethodName in tests_that_transfer_against_jc:
transfer_material_against = "Job Card"
@@ -190,4 +201,132 @@
job_card.submit()
# JC is Completed with excess transfer
- self.assertEqual(job_card.status, "Completed")
\ No newline at end of file
+ self.assertEqual(job_card.status, "Completed")
+
+ def test_job_card_partial_material_transfer(self):
+ "Test partial material transfer against Job Card"
+
+ make_stock_entry(item_code="_Test Item", target="Stores - _TC",
+ qty=25, basic_rate=100)
+ make_stock_entry(item_code="_Test Item Home Desktop Manufactured",
+ target="Stores - _TC", qty=15, basic_rate=100)
+
+ job_card_name = frappe.db.get_value("Job Card", {'work_order': self.work_order.name})
+ job_card = frappe.get_doc("Job Card", job_card_name)
+
+ # partially transfer
+ transfer_entry = make_stock_entry_from_jc(job_card_name)
+ transfer_entry.fg_completed_qty = 1
+ transfer_entry.get_items()
+ transfer_entry.insert()
+ transfer_entry.submit()
+
+ job_card.reload()
+ self.assertEqual(job_card.transferred_qty, 1)
+ self.assertEqual(transfer_entry.items[0].qty, 5)
+ self.assertEqual(transfer_entry.items[1].qty, 3)
+
+ # transfer remaining
+ transfer_entry_2 = make_stock_entry_from_jc(job_card_name)
+
+ self.assertEqual(transfer_entry_2.fg_completed_qty, 1)
+ self.assertEqual(transfer_entry_2.items[0].qty, 5)
+ self.assertEqual(transfer_entry_2.items[1].qty, 3)
+
+ transfer_entry_2.insert()
+ transfer_entry_2.submit()
+
+ job_card.reload()
+ self.assertEqual(job_card.transferred_qty, 2)
+
+ def test_job_card_material_transfer_correctness(self):
+ """
+ 1. Test if only current Job Card Items are pulled in a Stock Entry against a Job Card
+ 2. Test impact of changing 'For Qty' in such a Stock Entry
+ """
+ create_bom_with_multiple_operations()
+ work_order = make_wo_with_transfer_against_jc()
+
+ job_card_name = frappe.db.get_value(
+ "Job Card",
+ {"work_order": work_order.name,"operation": "Test Operation A"}
+ )
+ job_card = frappe.get_doc("Job Card", job_card_name)
+
+ self.assertEqual(len(job_card.items), 1)
+ self.assertEqual(job_card.items[0].item_code, "_Test Item")
+
+ # check if right items are mapped in transfer entry
+ transfer_entry = make_stock_entry_from_jc(job_card_name)
+ transfer_entry.insert()
+
+ self.assertEqual(len(transfer_entry.items), 1)
+ self.assertEqual(transfer_entry.items[0].item_code, "_Test Item")
+ self.assertEqual(transfer_entry.items[0].qty, 4)
+
+ # change 'For Qty' and check impact on items table
+ # no.of items should be the same with qty change
+ transfer_entry.fg_completed_qty = 2
+ transfer_entry.get_items()
+
+ self.assertEqual(len(transfer_entry.items), 1)
+ self.assertEqual(transfer_entry.items[0].item_code, "_Test Item")
+ self.assertEqual(transfer_entry.items[0].qty, 2)
+
+ # rollback via tearDown method
+
+def create_bom_with_multiple_operations():
+ "Create a BOM with multiple operations and Material Transfer against Job Card"
+ from erpnext.manufacturing.doctype.operation.test_operation import make_operation
+
+ test_record = frappe.get_test_records("BOM")[2]
+ bom_doc = frappe.get_doc(test_record)
+
+ row = {
+ "operation": "Test Operation A",
+ "workstation": "_Test Workstation A",
+ "hour_rate_rent": 300,
+ "time_in_mins": 60
+ }
+ make_workstation(row)
+ make_operation(row)
+
+ bom_doc.append("operations", {
+ "operation": "Test Operation A",
+ "description": "Test Operation A",
+ "workstation": "_Test Workstation A",
+ "hour_rate": 300,
+ "time_in_mins": 60,
+ "operating_cost": 100
+ })
+
+ bom_doc.transfer_material_against = "Job Card"
+ bom_doc.save()
+ bom_doc.submit()
+
+ return bom_doc
+
+def make_wo_with_transfer_against_jc():
+ "Create a WO with multiple operations and Material Transfer against Job Card"
+
+ work_order = make_wo_order_test_record(
+ item="_Test FG Item 2",
+ qty=4,
+ transfer_material_against="Job Card",
+ source_warehouse="Stores - _TC",
+ do_not_submit=True
+ )
+ work_order.required_items[0].operation = "Test Operation A"
+ work_order.required_items[1].operation = "_Test Operation 1"
+ work_order.submit()
+
+ return work_order
+
+def make_bom_for_jc_tests():
+ test_records = frappe.get_test_records('BOM')
+ bom = frappe.copy_doc(test_records[2])
+ bom.set_rate_of_sub_assembly_item_based_on_bom = 0
+ bom.rm_cost_as_per = "Valuation Rate"
+ bom.items[0].uom = "_Test UOM 1"
+ bom.items[0].conversion_factor = 5
+ bom.insert()
diff --git a/erpnext/manufacturing/doctype/operation/operation.json b/erpnext/manufacturing/doctype/operation/operation.json
index 10a97ed..753552c 100644
--- a/erpnext/manufacturing/doctype/operation/operation.json
+++ b/erpnext/manufacturing/doctype/operation/operation.json
@@ -13,6 +13,7 @@
"is_corrective_operation",
"job_card_section",
"create_job_card_based_on_batch_size",
+ "quality_inspection_template",
"column_break_6",
"batch_size",
"sub_operations_section",
@@ -92,15 +93,22 @@
"fieldname": "is_corrective_operation",
"fieldtype": "Check",
"label": "Is Corrective Operation"
+ },
+ {
+ "fieldname": "quality_inspection_template",
+ "fieldtype": "Link",
+ "label": "Quality Inspection Template",
+ "options": "Quality Inspection Template"
}
],
"icon": "fa fa-wrench",
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2021-01-12 15:09:23.593338",
+ "modified": "2021-11-24 19:15:24.357187",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Operation",
+ "naming_rule": "Set by user",
"owner": "Administrator",
"permissions": [
{
diff --git a/erpnext/manufacturing/doctype/operation/test_operation.js b/erpnext/manufacturing/doctype/operation/test_operation.js
deleted file mode 100644
index fd7783f..0000000
--- a/erpnext/manufacturing/doctype/operation/test_operation.js
+++ /dev/null
@@ -1,49 +0,0 @@
-QUnit.test("test: operation", function (assert) {
- assert.expect(2);
- let done = assert.async();
- frappe.run_serially([
- // test operation creation
- () => frappe.set_route("List", "Operation"),
-
- // Create a Keyboard operation
- () => {
- return frappe.tests.make(
- "Operation", [
- {__newname: "Assemble Keyboard"},
- {workstation: "Keyboard assembly workstation"}
- ]
- );
- },
- () => frappe.timeout(3),
- () => {
- assert.ok(cur_frm.docname.includes('Assemble Keyboard'),
- 'Assemble Keyboard created successfully');
- assert.ok(cur_frm.doc.workstation.includes('Keyboard assembly workstation'),
- 'Keyboard assembly workstation was linked successfully');
- },
-
- // Create a Screen operation
- () => {
- return frappe.tests.make(
- "Operation", [
- {__newname: 'Assemble Screen'},
- {workstation: "Screen assembly workstation"}
- ]
- );
- },
- () => frappe.timeout(3),
-
- // Create a CPU operation
- () => {
- return frappe.tests.make(
- "Operation", [
- {__newname: 'Assemble CPU'},
- {workstation: "CPU assembly workstation"}
- ]
- );
- },
- () => frappe.timeout(3),
-
- () => done()
- ]);
-});
diff --git a/erpnext/manufacturing/doctype/operation/test_operation.py b/erpnext/manufacturing/doctype/operation/test_operation.py
index 804cc3f..e511084 100644
--- a/erpnext/manufacturing/doctype/operation/test_operation.py
+++ b/erpnext/manufacturing/doctype/operation/test_operation.py
@@ -17,15 +17,13 @@
args = frappe._dict(args)
- try:
+ if not frappe.db.exists("Operation", args.operation):
doc = frappe.get_doc({
"doctype": "Operation",
"name": args.operation,
"workstation": args.workstation
})
-
doc.insert()
-
return doc
- except frappe.DuplicateEntryError:
- return frappe.get_doc("Operation", args.operation)
+
+ return frappe.get_doc("Operation", args.operation)
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js
index b171086..0babf87 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.js
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js
@@ -238,6 +238,12 @@
method: "get_items",
freeze: true,
doc: frm.doc,
+ callback: function() {
+ frm.refresh_field("po_items");
+ if (frm.doc.sub_assembly_items.length > 0) {
+ frm.trigger("get_sub_assembly_items");
+ }
+ }
});
},
diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
index a2980a7..2febc1e 100644
--- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
@@ -1,8 +1,5 @@
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
-
-import unittest
-
import frappe
from frappe.utils import add_to_date, flt, now_datetime, nowdate
@@ -17,9 +14,10 @@
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
create_stock_reconciliation,
)
+from erpnext.tests.utils import ERPNextTestCase
-class TestProductionPlan(unittest.TestCase):
+class TestProductionPlan(ERPNextTestCase):
def setUp(self):
for item in ['Test Production Item 1', 'Subassembly Item 1',
'Raw Material Item 1', 'Raw Material Item 2']:
diff --git a/erpnext/manufacturing/doctype/routing/test_routing.py b/erpnext/manufacturing/doctype/routing/test_routing.py
index 68d9dec..8bd60ea 100644
--- a/erpnext/manufacturing/doctype/routing/test_routing.py
+++ b/erpnext/manufacturing/doctype/routing/test_routing.py
@@ -1,17 +1,15 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
-
-import unittest
-
import frappe
from frappe.test_runner import make_test_records
from erpnext.manufacturing.doctype.job_card.job_card import OperationSequenceError
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
from erpnext.stock.doctype.item.test_item import make_item
+from erpnext.tests.utils import ERPNextTestCase
-class TestRouting(unittest.TestCase):
+class TestRouting(ERPNextTestCase):
@classmethod
def setUpClass(cls):
cls.item_code = "Test Routing Item - A"
@@ -48,6 +46,7 @@
wo_doc.delete()
def test_update_bom_operation_time(self):
+ """Update cost shouldn't update routing times."""
operations = [
{
"operation": "Test Operation A",
@@ -87,8 +86,8 @@
routing_doc.save()
bom_doc.update_cost()
bom_doc.reload()
- self.assertEqual(bom_doc.operations[0].time_in_mins, 90)
- self.assertEqual(bom_doc.operations[1].time_in_mins, 42.2)
+ self.assertEqual(bom_doc.operations[0].time_in_mins, 30)
+ self.assertEqual(bom_doc.operations[1].time_in_mins, 20)
def setup_operations(rows):
diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.js b/erpnext/manufacturing/doctype/work_order/test_work_order.js
deleted file mode 100644
index 1e224eb..0000000
--- a/erpnext/manufacturing/doctype/work_order/test_work_order.js
+++ /dev/null
@@ -1,130 +0,0 @@
-QUnit.test("test: work order", function (assert) {
- assert.expect(25);
- let done = assert.async();
- let laptop_quantity = 5;
- let items = ["CPU", "Keyboard", "Screen"];
- let operation_items = ["CPU", "Keyboard", "Screen"];
- let click_make = () => {
- let element = $(`.btn-primary:contains("Make"):visible`);
- if(!element.length) {
- throw `did not find any button containing 'Make'`;
- }
- element.click();
- return frappe.timeout(1);
- };
-
- frappe.run_serially([
- // test work order
- () => frappe.set_route("List", "Work Order", "List"),
- () => frappe.timeout(3),
-
- // Create a laptop work order
- () => {
- return frappe.tests.make('Work Order', [
- {production_item: 'Laptop'},
- {company: 'For Testing'},
- {qty: laptop_quantity},
- {scrap_warehouse: "Laptop Scrap Warehouse - FT"},
- {wip_warehouse: "Work In Progress - FT"},
- {fg_warehouse: "Finished Goods - FT"}
- ]);
- },
- () => frappe.timeout(3),
- () => {
- assert.equal(cur_frm.doc.planned_operating_cost, cur_frm.doc.total_operating_cost,
- "Total and Planned Cost is equal");
- assert.equal(cur_frm.doc.planned_operating_cost, cur_frm.doc.total_operating_cost,
- "Total and Planned Cost is equal");
-
- items.forEach(function(item, index) {
- assert.equal(item, cur_frm.doc.required_items[index].item_code, `Required item ${item} added`);
- assert.equal("Stores - FT", cur_frm.doc.required_items[index].source_warehouse, `Item ${item} warhouse verified`);
- assert.equal("5", cur_frm.doc.required_items[index].required_qty, `Item ${item} quantity verified`);
- });
-
- operation_items.forEach(function(operation_item, index) {
- assert.equal(`Assemble ${operation_item}`, cur_frm.doc.operations[index].operation,
- `Operation ${operation_item} added`);
- assert.equal(`${operation_item} assembly workstation`, cur_frm.doc.operations[index].workstation,
- `Workstation ${operation_item} linked`);
- });
- },
-
- // Submit the work order
- () => cur_frm.savesubmit(),
- () => frappe.timeout(1),
- () => frappe.click_button('Yes'),
- () => frappe.timeout(2.5),
-
- // Confirm the work order timesheet, save and submit it
- () => frappe.click_link("TS-00"),
- () => frappe.timeout(1),
- () => frappe.click_button("Submit"),
- () => frappe.timeout(1),
- () => frappe.click_button("Yes"),
- () => frappe.timeout(2.5),
-
- // Start the work order process
- () => frappe.set_route("List", "Work Order", "List"),
- () => frappe.timeout(2),
- () => frappe.click_link("Laptop"),
- () => frappe.timeout(1),
- () => frappe.click_button("Start"),
- () => frappe.timeout(0.5),
- () => click_make(),
- () => frappe.timeout(1),
- () => frappe.click_button("Save"),
- () => frappe.timeout(0.5),
-
- () => {
- assert.equal(cur_frm.doc.total_outgoing_value, cur_frm.doc.total_incoming_value,
- "Total incoming and outgoing cost is equal");
- assert.equal(cur_frm.doc.total_outgoing_value, "99000",
- "Outgoing cost is correct"); // Price of each item x5
- },
- // Submit for work
- () => frappe.click_button("Submit"),
- () => frappe.timeout(0.5),
- () => frappe.click_button("Yes"),
- () => frappe.timeout(0.5),
-
- // Finish the work order by sending for manufacturing
- () => frappe.set_route("List", "Work Order"),
- () => frappe.timeout(1),
- () => frappe.click_link("Laptop"),
- () => frappe.timeout(1),
-
- () => {
- assert.ok(frappe.tests.is_visible("5 items in progress", 'p'), "Work order initiated");
- assert.ok(frappe.tests.is_visible("Finish"), "Finish button visible");
- },
-
- () => frappe.click_button("Finish"),
- () => frappe.timeout(0.5),
- () => click_make(),
- () => {
- assert.equal(cur_frm.doc.total_incoming_value, "105700",
- "Incoming cost is correct "+cur_frm.doc.total_incoming_value); // Price of each item x5, values are in INR
- assert.equal(cur_frm.doc.total_outgoing_value, "99000",
- "Outgoing cost is correct"); // Price of each item x5, values are in INR
- assert.equal(cur_frm.doc.total_incoming_value - cur_frm.doc.total_outgoing_value, cur_frm.doc.value_difference,
- "Value difference is correct"); // Price of each item x5, values are in INR
- },
- () => frappe.click_button("Save"),
- () => frappe.timeout(1),
- () => frappe.click_button("Submit"),
- () => frappe.timeout(1),
- () => frappe.click_button("Yes"),
- () => frappe.timeout(1),
-
- // Manufacturing finished
- () => frappe.set_route("List", "Work Order", "List"),
- () => frappe.timeout(1),
- () => frappe.click_link("Laptop"),
- () => frappe.timeout(1),
-
- () => assert.ok(frappe.tests.is_visible("5 items produced", 'p'), "Work order completed"),
-
- () => done()
- ]);
-});
diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py
index f4a88dc..9926b15 100644
--- a/erpnext/manufacturing/doctype/work_order/test_work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py
@@ -1,6 +1,5 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
-import unittest
import frappe
from frappe.utils import add_months, cint, flt, now, today
@@ -21,13 +20,17 @@
from erpnext.stock.doctype.stock_entry import test_stock_entry
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
from erpnext.stock.utils import get_bin
+from erpnext.tests.utils import ERPNextTestCase, timeout
-class TestWorkOrder(unittest.TestCase):
+class TestWorkOrder(ERPNextTestCase):
def setUp(self):
self.warehouse = '_Test Warehouse 2 - _TC'
self.item = '_Test Item'
+ def tearDown(self):
+ frappe.db.rollback()
+
def check_planned_qty(self):
planned0 = frappe.db.get_value("Bin", {"item_code": "_Test FG Item",
@@ -91,7 +94,7 @@
def test_reserved_qty_for_partial_completion(self):
item = "_Test Item"
- warehouse = create_warehouse("Test Warehouse for reserved_qty - _TC")
+ warehouse = "_Test Warehouse - _TC"
bin1_at_start = get_bin(item, warehouse)
@@ -196,8 +199,6 @@
# no change in reserved / projected
self.assertEqual(cint(bin1_on_end_production.reserved_qty_for_production),
cint(bin1_on_start_production.reserved_qty_for_production))
- self.assertEqual(cint(bin1_on_end_production.projected_qty),
- cint(bin1_on_end_production.projected_qty))
def test_backflush_qty_for_overpduction_manufacture(self):
cancel_stock_entry = []
@@ -376,6 +377,7 @@
self.assertEqual(len(ste.additional_costs), 1)
self.assertEqual(ste.total_additional_costs, 1000)
+ @timeout(seconds=60)
def test_job_card(self):
stock_entries = []
bom = frappe.get_doc('BOM', {
@@ -769,6 +771,7 @@
total_pl_qty
)
+ @timeout(seconds=60)
def test_job_card_scrap_item(self):
items = ['Test FG Item for Scrap Item Test', 'Test RM Item 1 for Scrap Item Test',
'Test RM Item 2 for Scrap Item Test']
@@ -841,6 +844,45 @@
close_work_order(wo_order, "Closed")
self.assertEqual(wo_order.get('status'), "Closed")
+ def test_fix_time_operations(self):
+ bom = frappe.get_doc({
+ "doctype": "BOM",
+ "item": "_Test FG Item 2",
+ "is_active": 1,
+ "is_default": 1,
+ "quantity": 1.0,
+ "with_operations": 1,
+ "operations": [
+ {
+ "operation": "_Test Operation 1",
+ "description": "_Test",
+ "workstation": "_Test Workstation 1",
+ "time_in_mins": 60,
+ "operating_cost": 140,
+ "fixed_time": 1
+ }
+ ],
+ "items": [
+ {
+ "amount": 5000.0,
+ "doctype": "BOM Item",
+ "item_code": "_Test Item",
+ "parentfield": "items",
+ "qty": 1.0,
+ "rate": 5000.0,
+ },
+ ],
+ })
+ bom.save()
+ bom.submit()
+
+
+ wo1 = make_wo_order_test_record(item=bom.item, bom_no=bom.name, qty=1, skip_transfer=1, do_not_submit=1)
+ wo2 = make_wo_order_test_record(item=bom.item, bom_no=bom.name, qty=2, skip_transfer=1, do_not_submit=1)
+
+ self.assertEqual(wo1.operations[0].time_in_mins, wo2.operations[0].time_in_mins)
+
+
def update_job_card(job_card):
job_card_doc = frappe.get_doc('Job Card', job_card)
job_card_doc.set('scrap_items', [
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js
index bfce1b8..5ffbb03 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.js
+++ b/erpnext/manufacturing/doctype/work_order/work_order.js
@@ -442,7 +442,7 @@
additional_operating_cost: function(frm) {
erpnext.work_order.calculate_cost(frm.doc);
erpnext.work_order.calculate_total_cost(frm);
- }
+ },
});
frappe.ui.form.on("Work Order Item", {
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.json b/erpnext/manufacturing/doctype/work_order/work_order.json
index df7ee53..12cd58f 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.json
+++ b/erpnext/manufacturing/doctype/work_order/work_order.json
@@ -326,6 +326,7 @@
"label": "Expected Delivery Date"
},
{
+ "collapsible": 1,
"fieldname": "operations_section",
"fieldtype": "Section Break",
"label": "Operations",
@@ -337,7 +338,7 @@
"fieldname": "transfer_material_against",
"fieldtype": "Select",
"label": "Transfer Material Against",
- "options": "\nWork Order\nJob Card"
+ "options": "Work Order\nJob Card"
},
{
"fieldname": "operations",
@@ -573,8 +574,7 @@
"image_field": "image",
"is_submittable": 1,
"links": [],
- "migration_hash": "a18118963f4fcdb7f9d326de5f4063ba",
- "modified": "2021-10-29 15:12:32.203605",
+ "modified": "2021-11-08 17:36:07.016300",
"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 0090f4d..170454c 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/work_order.py
@@ -505,16 +505,19 @@
"""Fetch operations from BOM and set in 'Work Order'"""
def _get_operations(bom_no, qty=1):
- return frappe.db.sql(
- f"""select
- operation, description, workstation, idx,
- base_hour_rate as hour_rate, time_in_mins * {qty} as time_in_mins,
- "Pending" as status, parent as bom, batch_size, sequence_id
- from
- `tabBOM Operation`
- where
- parent = %s order by idx
- """, bom_no, as_dict=1)
+ data = frappe.get_all("BOM Operation",
+ filters={"parent": bom_no},
+ fields=["operation", "description", "workstation", "idx",
+ "base_hour_rate as hour_rate", "time_in_mins", "parent as bom",
+ "batch_size", "sequence_id", "fixed_time"],
+ order_by="idx")
+
+ for d in data:
+ if not d.fixed_time:
+ d.time_in_mins = flt(d.time_in_mins) * flt(qty)
+ d.status = "Pending"
+
+ return data
self.set('operations', [])
@@ -542,7 +545,8 @@
def calculate_time(self):
for d in self.get("operations"):
- d.time_in_mins = flt(d.time_in_mins) * (flt(self.qty) / flt(d.batch_size))
+ if not d.fixed_time:
+ d.time_in_mins = flt(d.time_in_mins) * (flt(self.qty) / flt(d.batch_size))
self.calculate_operating_cost()
diff --git a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json
index f7b8787..4e1a464 100644
--- a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json
+++ b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json
@@ -6,27 +6,27 @@
"field_order": [
"details",
"operation",
- "bom",
- "column_break_4",
- "description",
- "sequence_id",
- "col_break1",
- "completed_qty",
"status",
+ "completed_qty",
+ "column_break_4",
+ "bom",
"workstation",
+ "sequence_id",
+ "section_break_10",
+ "description",
"estimated_time_and_cost",
"planned_start_time",
- "planned_end_time",
- "column_break_10",
- "time_in_mins",
"hour_rate",
+ "time_in_mins",
+ "column_break_10",
+ "planned_end_time",
"batch_size",
"planned_operating_cost",
"section_break_9",
"actual_start_time",
- "actual_end_time",
- "column_break_11",
"actual_operation_time",
+ "column_break_11",
+ "actual_end_time",
"actual_operating_cost"
],
"fields": [
@@ -42,7 +42,6 @@
"oldfieldname": "operation_no",
"oldfieldtype": "Data",
"options": "Operation",
- "read_only": 1,
"reqd": 1
},
{
@@ -52,20 +51,14 @@
"label": "BOM",
"no_copy": 1,
"options": "BOM",
- "print_hide": 1,
- "read_only": 1
+ "print_hide": 1
},
{
"fieldname": "description",
"fieldtype": "Text Editor",
"label": "Operation Description",
"oldfieldname": "opn_description",
- "oldfieldtype": "Text",
- "read_only": 1
- },
- {
- "fieldname": "col_break1",
- "fieldtype": "Column Break"
+ "oldfieldtype": "Text"
},
{
"columns": 1,
@@ -74,19 +67,16 @@
"fieldtype": "Float",
"in_list_view": 1,
"label": "Completed Qty",
- "no_copy": 1,
- "read_only": 1
+ "no_copy": 1
},
{
"columns": 1,
"default": "Pending",
"fieldname": "status",
"fieldtype": "Select",
- "in_list_view": 1,
"label": "Status",
"no_copy": 1,
- "options": "Pending\nWork in Progress\nCompleted",
- "read_only": 1
+ "options": "Pending\nWork in Progress\nCompleted"
},
{
"fieldname": "workstation",
@@ -106,15 +96,13 @@
"fieldname": "planned_start_time",
"fieldtype": "Datetime",
"label": "Planned Start Time",
- "no_copy": 1,
- "read_only": 1
+ "no_copy": 1
},
{
"fieldname": "planned_end_time",
"fieldtype": "Datetime",
"label": "Planned End Time",
- "no_copy": 1,
- "read_only": 1
+ "no_copy": 1
},
{
"fieldname": "column_break_10",
@@ -122,7 +110,7 @@
},
{
"columns": 1,
- "description": "in Minutes",
+ "description": "In Minutes",
"fieldname": "time_in_mins",
"fieldtype": "Float",
"in_list_view": 1,
@@ -152,6 +140,7 @@
"label": "Actual Time and Cost"
},
{
+ "description": "Updated via 'Time Log' (In Minutes)",
"fieldname": "actual_start_time",
"fieldtype": "Datetime",
"label": "Actual Start Time",
@@ -159,7 +148,7 @@
"read_only": 1
},
{
- "description": "Updated via 'Time Log'",
+ "description": "Updated via 'Time Log' (In Minutes)",
"fieldname": "actual_end_time",
"fieldtype": "Datetime",
"label": "Actual End Time",
@@ -171,7 +160,7 @@
"fieldtype": "Column Break"
},
{
- "description": "in Minutes\nUpdated via 'Time Log'",
+ "description": "Updated via 'Time Log' (In Minutes)",
"fieldname": "actual_operation_time",
"fieldtype": "Float",
"label": "Actual Operation Time",
@@ -189,26 +178,30 @@
},
{
"fieldname": "batch_size",
- "fieldtype": "Int",
+ "fieldtype": "Float",
"label": "Batch Size",
"read_only": 1
},
{
"fieldname": "sequence_id",
"fieldtype": "Int",
+ "hidden": 1,
"label": "Sequence ID",
- "print_hide": 1,
- "read_only": 1
+ "print_hide": 1
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "section_break_10",
+ "fieldtype": "Section Break"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-06-24 14:36:12.835543",
+ "modified": "2021-11-29 16:37:18.824489",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Work Order Operation",
@@ -217,4 +210,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
-}
+}
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/workstation/test_workstation.js b/erpnext/manufacturing/doctype/workstation/test_workstation.js
deleted file mode 100644
index 1df53d0..0000000
--- a/erpnext/manufacturing/doctype/workstation/test_workstation.js
+++ /dev/null
@@ -1,89 +0,0 @@
-QUnit.test("test: workstation", function (assert) {
- assert.expect(9);
- let done = assert.async();
- let elec_rate = 50;
- let rent = 100;
- let consumable_rate = 20;
- let labour_rate = 500;
- frappe.run_serially([
- // test workstation creation
- () => frappe.set_route("List", "Workstation"),
-
- // Create a keyboard workstation
- () => frappe.tests.make(
- "Workstation", [
- {workstation_name: "Keyboard assembly workstation"},
- {hour_rate_electricity: elec_rate},
- {hour_rate_rent: rent},
- {hour_rate_consumable: consumable_rate},
- {hour_rate_labour: labour_rate},
- {working_hours: [
- [
- {enabled: 1},
- {start_time: '11:00:00'},
- {end_time: '18:00:00'}
- ]
- ]}
- ]
- ),
- () => {
- assert.ok(cur_frm.doc.workstation_name.includes('Keyboard assembly workstation'),
- 'Keyboard assembly workstation created successfully');
- assert.equal(cur_frm.doc.hour_rate_electricity, elec_rate,
- 'electricity rate set correctly');
- assert.equal(cur_frm.doc.hour_rate_rent, rent,
- 'rent set correctly');
- assert.equal(cur_frm.doc.hour_rate_consumable, consumable_rate,
- 'consumable rate set correctly');
- assert.equal(cur_frm.doc.hour_rate_labour, labour_rate,
- 'labour rate set correctly');
- assert.equal(cur_frm.doc.working_hours[0].enabled, 1,
- 'working hours enabled');
- assert.ok(cur_frm.doc.working_hours[0].start_time.includes('11:00:0'),
- 'start time set correctly');
- assert.ok(cur_frm.doc.working_hours[0].end_time.includes('18:00:0'),
- 'end time set correctly');
- assert.ok(cur_frm.doc.hour_rate_electricity+cur_frm.doc.hour_rate_rent+
- cur_frm.doc.hour_rate_consumable+cur_frm.doc.hour_rate_labour==
- cur_frm.doc.hour_rate, 'Net hour rate set correctly');
- },
-
- // Create a Screen workstation
- () => frappe.tests.make(
- "Workstation", [
- {workstation_name: "Screen assembly workstation"},
- {hour_rate_electricity: elec_rate},
- {hour_rate_rent: rent},
- {hour_rate_consumable: consumable_rate},
- {hour_rate_labour: labour_rate},
- {working_hours: [
- [
- {enabled: 1},
- {start_time: '11:00:00'},
- {end_time: '18:00:00'}
- ]
- ]}
- ]
- ),
-
- // Create a CPU workstation
- () => frappe.tests.make(
- "Workstation", [
- {workstation_name: "CPU assembly workstation"},
- {hour_rate_electricity: elec_rate},
- {hour_rate_rent: rent},
- {hour_rate_consumable: consumable_rate},
- {hour_rate_labour: labour_rate},
- {working_hours: [
- [
- {enabled: 1},
- {start_time: '11:00:00'},
- {end_time: '18:00:00'}
- ]
- ]}
- ]
- ),
-
- () => done()
- ]);
-});
diff --git a/erpnext/manufacturing/doctype/workstation/test_workstation.py b/erpnext/manufacturing/doctype/workstation/test_workstation.py
index c77cef2..c298c0a 100644
--- a/erpnext/manufacturing/doctype/workstation/test_workstation.py
+++ b/erpnext/manufacturing/doctype/workstation/test_workstation.py
@@ -1,8 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and Contributors
# See license.txt
-
-import unittest
-
import frappe
from frappe.test_runner import make_test_records
@@ -13,12 +10,13 @@
WorkstationHolidayError,
check_if_within_operating_hours,
)
+from erpnext.tests.utils import ERPNextTestCase
test_dependencies = ["Warehouse"]
test_records = frappe.get_test_records('Workstation')
make_test_records('Workstation')
-class TestWorkstation(unittest.TestCase):
+class TestWorkstation(ERPNextTestCase):
def test_validate_timings(self):
check_if_within_operating_hours("_Test Workstation 1", "Operation 1", "2013-02-02 11:00:00", "2013-02-02 19:00:00")
check_if_within_operating_hours("_Test Workstation 1", "Operation 1", "2013-02-02 10:00:00", "2013-02-02 20:00:00")
@@ -89,7 +87,7 @@
args = frappe._dict(args)
workstation_name = args.workstation_name or args.workstation
- try:
+ if not frappe.db.exists("Workstation", workstation_name):
doc = frappe.get_doc({
"doctype": "Workstation",
"workstation_name": workstation_name
@@ -99,5 +97,5 @@
doc.insert()
return doc
- except frappe.DuplicateEntryError:
- return frappe.get_doc("Workstation", workstation_name)
+
+ return frappe.get_doc("Workstation", workstation_name)
diff --git a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py b/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py
index cf19cbf..090a3e7 100644
--- a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py
+++ b/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py
@@ -89,7 +89,7 @@
GROUP BY bom_item.item_code""".format(qty_field=qty_field, table=table, conditions=conditions, bom=bom), as_dict=1)
def get_manufacturer_records():
- details = frappe.get_list('Item Manufacturer', fields = ["manufacturer", "manufacturer_part_no", "parent"])
+ details = frappe.get_all('Item Manufacturer', fields = ["manufacturer", "manufacturer_part_no", "parent"])
manufacture_details = frappe._dict()
for detail in details:
dic = manufacture_details.setdefault(detail.get('parent'), {})
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 b9ddc9f..7741823 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
@@ -1,7 +1,6 @@
-# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
-
import frappe
from frappe import _
from frappe.utils import flt
@@ -30,7 +29,6 @@
for row in job_cards:
row.operating_cost = flt(row.hour_rate) * (flt(row.total_time_in_mins) / 60.0)
- update_raw_material_cost(row, report_filters)
data.append(row)
return data
@@ -46,12 +44,6 @@
return filters
-def update_raw_material_cost(row, filters):
- row.rm_cost = 0.0
- for data in frappe.get_all("Job Card Item", fields = ["amount"],
- filters={"parent": row.name, "docstatus": 1}):
- row.rm_cost += data.amount
-
def get_columns(filters):
return [
{
@@ -59,7 +51,7 @@
"fieldtype": "Link",
"fieldname": "name",
"options": "Job Card",
- "width": "100"
+ "width": "120"
},
{
"label": _("Work Order"),
@@ -111,18 +103,12 @@
"label": _("Operating Cost"),
"fieldtype": "Currency",
"fieldname": "operating_cost",
- "width": "100"
- },
- {
- "label": _("Raw Material Cost"),
- "fieldtype": "Currency",
- "fieldname": "rm_cost",
- "width": "100"
+ "width": "150"
},
{
"label": _("Total Time (in Mins)"),
"fieldtype": "Float",
"fieldname": "total_time_in_mins",
- "width": "100"
+ "width": "150"
}
]
diff --git a/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py b/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py
index 43ef12e..55b1a3f 100644
--- a/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py
+++ b/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py
@@ -28,8 +28,15 @@
production_plan_doc = frappe.get_cached_doc("Production Plan", filters.get("production_plan"))
for row in production_plan_doc.po_items:
- work_order = frappe.get_cached_value("Work Order", {"production_plan_item": row.name,
- "bom_no": row.bom_no, "production_item": row.item_code}, "name")
+ work_order = frappe.get_value(
+ "Work Order",
+ {
+ "production_plan_item": row.name,
+ "bom_no": row.bom_no,
+ "production_item": row.item_code
+ },
+ "name"
+ )
if row.item_code not in itemwise_indent:
itemwise_indent.setdefault(row.item_code, {})
@@ -40,10 +47,10 @@
"item_name": frappe.get_cached_value("Item", row.item_code, "item_name"),
"qty": row.planned_qty,
"document_type": "Work Order",
- "document_name": work_order,
+ "document_name": work_order or "",
"bom_level": frappe.get_cached_value("BOM", row.bom_no, "bom_level"),
- "produced_qty": order_details.get((work_order, row.item_code)).get("produced_qty"),
- "pending_qty": flt(row.planned_qty) - flt(order_details.get((work_order, row.item_code)).get("produced_qty"))
+ "produced_qty": order_details.get((work_order, row.item_code), {}).get("produced_qty", 0),
+ "pending_qty": flt(row.planned_qty) - flt(order_details.get((work_order, row.item_code), {}).get("produced_qty", 0))
})
get_production_plan_sub_assembly_item_details(filters, row, production_plan_doc, data, order_details)
@@ -54,11 +61,23 @@
subcontracted_item = (item.type_of_manufacturing == 'Subcontract')
if subcontracted_item:
- docname = frappe.get_cached_value("Purchase Order Item",
- {"production_plan_sub_assembly_item": item.name, "docstatus": ("<", 2)}, "parent")
+ docname = frappe.get_value(
+ "Purchase Order Item",
+ {
+ "production_plan_sub_assembly_item": item.name,
+ "docstatus": ("<", 2)
+ },
+ "parent"
+ )
else:
- docname = frappe.get_cached_value("Work Order",
- {"production_plan_sub_assembly_item": item.name, "docstatus": ("<", 2)}, "name")
+ docname = frappe.get_value(
+ "Work Order",
+ {
+ "production_plan_sub_assembly_item": item.name,
+ "docstatus": ("<", 2)
+ },
+ "name"
+ )
data.append({
"indent": 1,
@@ -66,10 +85,10 @@
"item_name": item.item_name,
"qty": item.qty,
"document_type": "Work Order" if not subcontracted_item else "Purchase Order",
- "document_name": docname,
+ "document_name": docname or "",
"bom_level": item.bom_level,
- "produced_qty": order_details.get((docname, item.production_item)).get("produced_qty"),
- "pending_qty": flt(item.qty) - flt(order_details.get((docname, item.production_item)).get("produced_qty"))
+ "produced_qty": order_details.get((docname, item.production_item), {}).get("produced_qty", 0),
+ "pending_qty": flt(item.qty) - flt(order_details.get((docname, item.production_item), {}).get("produced_qty", 0))
})
def get_work_order_details(filters, order_details):
diff --git a/erpnext/agriculture/doctype/linked_plant_analysis/__init__.py b/erpnext/manufacturing/report/work_order_consumed_materials/__init__.py
similarity index 100%
copy from erpnext/agriculture/doctype/linked_plant_analysis/__init__.py
copy to erpnext/manufacturing/report/work_order_consumed_materials/__init__.py
diff --git a/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.js b/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.js
new file mode 100644
index 0000000..b2428e8
--- /dev/null
+++ b/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.js
@@ -0,0 +1,70 @@
+// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+/* eslint-disable */
+
+frappe.query_reports["Work Order Consumed Materials"] = {
+ "filters": [
+ {
+ label: __("Company"),
+ fieldname: "company",
+ fieldtype: "Link",
+ options: "Company",
+ default: frappe.defaults.get_user_default("Company"),
+ reqd: 1
+ },
+ {
+ label: __("From Date"),
+ fieldname:"from_date",
+ fieldtype: "Date",
+ default: frappe.datetime.add_months(frappe.datetime.get_today(), -1),
+ reqd: 1
+ },
+ {
+ fieldname:"to_date",
+ label: __("To Date"),
+ fieldtype: "Date",
+ default: frappe.datetime.get_today(),
+ reqd: 1
+ },
+ {
+ label: __("Work Order"),
+ fieldname: "name",
+ fieldtype: "Link",
+ options: "Work Order",
+ get_query: function() {
+ return {
+ filters: {
+ status: ["in", ["In Process", "Completed", "Stopped"]]
+ }
+ }
+ }
+ },
+ {
+ label: __("Production Item"),
+ fieldname: "production_item",
+ fieldtype: "Link",
+ depends_on: "eval: !doc.name",
+ options: "Item"
+ },
+ {
+ label: __("Status"),
+ fieldname: "status",
+ fieldtype: "Select",
+ options: ["In Process", "Completed", "Stopped"]
+ },
+ {
+ label: __("Excess Materials Consumed"),
+ fieldname: "show_extra_consumed_materials",
+ fieldtype: "Check"
+ }
+ ],
+ "formatter": function(value, row, column, data, default_formatter) {
+ value = default_formatter(value, row, column, data);
+
+ if (column.fieldname == "raw_material_name" && data && data.extra_consumed_qty > 0 ) {
+ value = `<div style="color:red">${value}</div>`;
+ }
+
+ return value;
+ },
+};
diff --git a/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.json b/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.json
new file mode 100644
index 0000000..2fc986a
--- /dev/null
+++ b/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.json
@@ -0,0 +1,30 @@
+{
+ "add_total_row": 0,
+ "columns": [],
+ "creation": "2021-11-22 17:36:11.886939",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "letter_head": "Gadgets International",
+ "modified": "2021-11-22 17:36:14.999091",
+ "modified_by": "Administrator",
+ "module": "Manufacturing",
+ "name": "Work Order Consumed Materials",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "Work Order",
+ "report_name": "Work Order Consumed Materials",
+ "report_type": "Script Report",
+ "roles": [
+ {
+ "role": "Manufacturing User"
+ },
+ {
+ "role": "Stock User"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.py b/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.py
new file mode 100644
index 0000000..0528348
--- /dev/null
+++ b/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.py
@@ -0,0 +1,131 @@
+# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe import _
+
+
+def execute(filters=None):
+ columns, data = [], []
+ columns = get_columns()
+ data = get_data(filters)
+
+ return columns, data
+
+def get_data(report_filters):
+ fields = get_fields()
+ filters = get_filter_condition(report_filters)
+
+ wo_items = {}
+ for d in frappe.get_all("Work Order", filters = filters, fields=fields):
+ d.extra_consumed_qty = 0.0
+ if d.consumed_qty and d.consumed_qty > d.required_qty:
+ d.extra_consumed_qty = d.consumed_qty - d.required_qty
+
+ if d.extra_consumed_qty or not report_filters.show_extra_consumed_materials:
+ wo_items.setdefault((d.name, d.production_item), []).append(d)
+
+ data = []
+ for key, wo_data in wo_items.items():
+ for index, row in enumerate(wo_data):
+ if index != 0:
+ #If one work order has multiple raw materials then show parent data in the first row only
+ for field in ["name", "status", "production_item", "qty", "produced_qty"]:
+ row[field] = ""
+
+ data.append(row)
+
+ return data
+
+def get_fields():
+ return ["`tabWork Order Item`.`parent`", "`tabWork Order Item`.`item_code` as raw_material_item_code",
+ "`tabWork Order Item`.`item_name` as raw_material_name", "`tabWork Order Item`.`required_qty`",
+ "`tabWork Order Item`.`transferred_qty`", "`tabWork Order Item`.`consumed_qty`", "`tabWork Order`.`status`",
+ "`tabWork Order`.`name`", "`tabWork Order`.`production_item`", "`tabWork Order`.`qty`",
+ "`tabWork Order`.`produced_qty`"]
+
+def get_filter_condition(report_filters):
+ filters = {
+ "docstatus": 1, "status": ("in", ["In Process", "Completed", "Stopped"]),
+ "creation": ("between", [report_filters.from_date, report_filters.to_date])
+ }
+
+ for field in ["name", "production_item", "company", "status"]:
+ value = report_filters.get(field)
+ if value:
+ key = f"`{field}`"
+ filters.update({key: value})
+
+ return filters
+
+def get_columns():
+ return [
+ {
+ "label": _("Id"),
+ "fieldname": "name",
+ "fieldtype": "Link",
+ "options": "Work Order",
+ "width": 80
+ },
+ {
+ "label": _("Status"),
+ "fieldname": "status",
+ "fieldtype": "Data",
+ "width": 80
+ },
+ {
+ "label": _("Production Item"),
+ "fieldname": "production_item",
+ "fieldtype": "Link",
+ "options": "Item",
+ "width": 130
+ },
+ {
+ "label": _("Qty to Produce"),
+ "fieldname": "qty",
+ "fieldtype": "Float",
+ "width": 120
+ },
+ {
+ "label": _("Produced Qty"),
+ "fieldname": "produced_qty",
+ "fieldtype": "Float",
+ "width": 110
+ },
+ {
+ "label": _("Raw Material Item"),
+ "fieldname": "raw_material_item_code",
+ "fieldtype": "Link",
+ "options": "Item",
+ "width": 150
+ },
+ {
+ "label": _("Item Name"),
+ "fieldname": "raw_material_name",
+ "width": 130
+ },
+ {
+ "label": _("Required Qty"),
+ "fieldname": "required_qty",
+ "fieldtype": "Float",
+ "width": 100
+ },
+ {
+ "label": _("Transferred Qty"),
+ "fieldname": "transferred_qty",
+ "fieldtype": "Float",
+ "width": 100
+ },
+ {
+ "label": _("Consumed Qty"),
+ "fieldname": "consumed_qty",
+ "fieldtype": "Float",
+ "width": 100
+ },
+ {
+ "label": _("Extra Consumed Qty"),
+ "fieldname": "extra_consumed_qty",
+ "fieldtype": "Float",
+ "width": 100
+ }
+ ]
diff --git a/erpnext/manufacturing/report/work_order_summary/work_order_summary.js b/erpnext/manufacturing/report/work_order_summary/work_order_summary.js
index eb23f17..832be23 100644
--- a/erpnext/manufacturing/report/work_order_summary/work_order_summary.js
+++ b/erpnext/manufacturing/report/work_order_summary/work_order_summary.js
@@ -51,7 +51,7 @@
label: __("Status"),
fieldname: "status",
fieldtype: "Select",
- options: ["", "Not Started", "In Process", "Completed", "Stopped"]
+ options: ["", "Not Started", "In Process", "Completed", "Stopped", "Closed"]
},
{
label: __("Sales Orders"),
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 6207904..d7469dd 100644
--- a/erpnext/manufacturing/report/work_order_summary/work_order_summary.py
+++ b/erpnext/manufacturing/report/work_order_summary/work_order_summary.py
@@ -1,6 +1,7 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
+from collections import defaultdict
import frappe
from frappe import _
@@ -58,21 +59,16 @@
return get_chart_based_on_qty(data, filters)
def get_chart_based_on_status(data):
- labels = ["Completed", "In Process", "Stopped", "Not Started"]
+ labels = frappe.get_meta("Work Order").get_options("status").split("\n")
+ if "" in labels:
+ labels.remove("")
- status_wise_data = {
- "Not Started": 0,
- "In Process": 0,
- "Stopped": 0,
- "Completed": 0,
- "Draft": 0
- }
+ status_wise_data = defaultdict(int)
for d in data:
status_wise_data[d.status] += 1
- values = [status_wise_data["Completed"], status_wise_data["In Process"],
- status_wise_data["Stopped"], status_wise_data["Not Started"]]
+ values = [status_wise_data[label] for label in labels]
chart = {
"data": {
diff --git a/erpnext/manufacturing/workspace/manufacturing/manufacturing.json b/erpnext/manufacturing/workspace/manufacturing/manufacturing.json
index cfa80f8..65b4d02 100644
--- a/erpnext/manufacturing/workspace/manufacturing/manufacturing.json
+++ b/erpnext/manufacturing/workspace/manufacturing/manufacturing.json
@@ -1,10 +1,6 @@
{
- "charts": [
- {
- "chart_name": "Produced Quantity"
- }
- ],
- "content": "[{\"type\": \"onboarding\", \"data\": {\"onboarding_name\":\"Manufacturing\", \"col\": 12}}, {\"type\": \"chart\", \"data\": {\"chart_name\": null, \"col\": 12}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Item\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"BOM\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Work Order\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Production Plan\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Forecasting\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Work Order Summary\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"BOM Stock Report\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Production Planning Report\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Dashboard\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Production\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Bill of Materials\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Reports\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Tools\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Settings\", \"col\": 4}}]",
+ "charts": [],
+ "content": "[{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\",\"level\":4,\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Item\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Work Order\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Plan\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Forecasting\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Work Order Summary\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM Stock Report\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Planning Report\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":4}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\",\"level\":4,\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Production\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Bill of Materials\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Tools\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]",
"creation": "2020-03-02 17:11:37.032604",
"docstatus": 0,
"doctype": "Workspace",
@@ -141,14 +137,6 @@
"type": "Link"
},
{
- "hidden": 0,
- "is_query_report": 0,
- "label": "Reports",
- "link_count": 0,
- "onboard": 0,
- "type": "Card Break"
- },
- {
"dependencies": "Work Order",
"hidden": 0,
"is_query_report": 1,
@@ -295,9 +283,126 @@
"link_type": "DocType",
"onboard": 0,
"type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Reports",
+ "link_count": 10,
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "Work Order",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Production Planning Report",
+ "link_count": 0,
+ "link_to": "Production Planning Report",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Work Order",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Work Order Summary",
+ "link_count": 0,
+ "link_to": "Work Order Summary",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Quality Inspection",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Quality Inspection Summary",
+ "link_count": 0,
+ "link_to": "Quality Inspection Summary",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Downtime Entry",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Downtime Analysis",
+ "link_count": 0,
+ "link_to": "Downtime Analysis",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Job Card",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Job Card Summary",
+ "link_count": 0,
+ "link_to": "Job Card Summary",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "BOM",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "BOM Search",
+ "link_count": 0,
+ "link_to": "BOM Search",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "BOM",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "BOM Stock Report",
+ "link_count": 0,
+ "link_to": "BOM Stock Report",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Work Order",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Production Analytics",
+ "link_count": 0,
+ "link_to": "Production Analytics",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "BOM",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "BOM Operations Time",
+ "link_count": 0,
+ "link_to": "BOM Operations Time",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Work Order Consumed Materials",
+ "link_count": 0,
+ "link_to": "Work Order Consumed Materials",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
}
],
- "modified": "2021-08-05 12:16:00.825742",
+ "modified": "2021-11-22 17:55:03.524496",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Manufacturing",
diff --git a/erpnext/modules.txt b/erpnext/modules.txt
index 1952a11..d3b7a17 100644
--- a/erpnext/modules.txt
+++ b/erpnext/modules.txt
@@ -15,7 +15,6 @@
Education
Regional
Restaurant
-Agriculture
ERPNext Integrations
Non Profit
Hotels
diff --git a/erpnext/non_profit/doctype/donor/test_donor.js b/erpnext/non_profit/doctype/donor/test_donor.js
deleted file mode 100644
index e478b34..0000000
--- a/erpnext/non_profit/doctype/donor/test_donor.js
+++ /dev/null
@@ -1,27 +0,0 @@
-/* eslint-disable */
-// rename this file from _test_[name] to test_[name] to activate
-// and remove above this line
-
-QUnit.test("test: Donor", function (assert) {
- let done = assert.async();
-
- // number of asserts
- assert.expect(3);
-
- frappe.run_serially([
- // insert a new Member
- () => frappe.tests.make('Donor', [
- // values to be set
- {donor_name: 'Test Donor'},
- {donor_type: 'Test Organization'},
- {email: 'test@example.com'}
- ]),
- () => {
- assert.equal(cur_frm.doc.donor_name, 'Test Donor');
- assert.equal(cur_frm.doc.donor_type, 'Test Organization');
- assert.equal(cur_frm.doc.email, 'test@example.com');
- },
- () => done()
- ]);
-
-});
diff --git a/erpnext/non_profit/doctype/grant_application/test_grant_application.js b/erpnext/non_profit/doctype/grant_application/test_grant_application.js
deleted file mode 100644
index 47230a5..0000000
--- a/erpnext/non_profit/doctype/grant_application/test_grant_application.js
+++ /dev/null
@@ -1,30 +0,0 @@
-/* eslint-disable */
-// rename this file from _test_[name] to test_[name] to activate
-// and remove above this line
-
-QUnit.test("test: Grant Application", function (assert) {
- let done = assert.async();
-
- // number of asserts
- assert.expect(4);
-
- frappe.run_serially([
- // insert a new Member
- () => frappe.tests.make('Grant Application', [
- // values to be set
- {applicant_name: 'Test Organization'},
- {contact_person:'Test Applicant'},
- {email: 'test@example.com'},
- {grant_description:'Test message'},
- {amount: 150000}
- ]),
- () => {
- assert.equal(cur_frm.doc.applicant_name, 'Test Organization');
- assert.equal(cur_frm.doc.contact_person, 'Test Applicant');
- assert.equal(cur_frm.doc.email, 'test@example.com');
- assert.equal(cur_frm.doc.amount, 150000);
- },
- () => done()
- ]);
-
-});
diff --git a/erpnext/non_profit/doctype/member/test_member.js b/erpnext/non_profit/doctype/member/test_member.js
deleted file mode 100644
index f7cca97..0000000
--- a/erpnext/non_profit/doctype/member/test_member.js
+++ /dev/null
@@ -1,26 +0,0 @@
-/* eslint-disable */
-// rename this file from _test_[name] to test_[name] to activate
-// and remove above this line
-
-QUnit.test("test: Member", function (assert) {
- let done = assert.async();
-
- // number of asserts
- assert.expect(2);
-
- frappe.run_serially([
- // insert a new Member
- () => frappe.tests.make('Member', [
- // values to be set
- {member_name: 'Test Member'},
- {membership_type: 'Gold'},
- {email: 'test@example.com'}
- ]),
- () => {
- assert.equal(cur_frm.doc.membership_type, 'Gold');
- assert.equal(cur_frm.doc.email, 'test@example.com');
- },
- () => done()
- ]);
-
-});
diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py
index beb38e2..f9b295a 100644
--- a/erpnext/non_profit/doctype/membership/membership.py
+++ b/erpnext/non_profit/doctype/membership/membership.py
@@ -409,7 +409,7 @@
def set_expired_status():
frappe.db.sql("""
UPDATE
- `tabMembership` SET `status` = 'Expired'
+ `tabMembership` SET `membership_status` = 'Expired'
WHERE
- `status` not in ('Cancelled') AND `to_date` < %s
+ `membership_status` not in ('Cancelled') AND `to_date` < %s
""", (nowdate()))
diff --git a/erpnext/non_profit/doctype/membership_type/test_membership_type.js b/erpnext/non_profit/doctype/membership_type/test_membership_type.js
deleted file mode 100644
index 6440df8..0000000
--- a/erpnext/non_profit/doctype/membership_type/test_membership_type.js
+++ /dev/null
@@ -1,25 +0,0 @@
-/* eslint-disable */
-// rename this file from _test_[name] to test_[name] to activate
-// and remove above this line
-
-QUnit.test("test: Membership Type", function (assert) {
- let done = assert.async();
-
- // number of asserts
- assert.expect(2);
-
- frappe.run_serially([
- // insert a new Member
- () => frappe.tests.make('Membership Type', [
- // values to be set
- {membership_type: 'Gold'},
- {amount:50000}
- ]),
- () => {
- assert.equal(cur_frm.doc.membership_type, 'Gold');
- assert.equal(cur_frm.doc.amount, '50000');
- },
- () => done()
- ]);
-
-});
diff --git a/erpnext/non_profit/doctype/volunteer/test_volunteer.js b/erpnext/non_profit/doctype/volunteer/test_volunteer.js
deleted file mode 100644
index 45eb281..0000000
--- a/erpnext/non_profit/doctype/volunteer/test_volunteer.js
+++ /dev/null
@@ -1,34 +0,0 @@
-/* eslint-disable */
-// rename this file from _test_[name] to test_[name] to activate
-// and remove above this line
-
-QUnit.test("test: Volunteer", function (assert) {
- let done = assert.async();
-
- // number of asserts
- assert.expect(4);
-
- frappe.run_serially([
- // insert a new Member
- () => frappe.tests.make('Volunteer', [
- // values to be set
- {volunteer_name: 'Test Volunteer'},
- {volunteer_type:'Test Work'},
- {email:'test@example.com'},
- {'availability': 'Weekends'},
- {volunteer_skills:[
- [
- {'volunteer_skills': 'Fundraiser'},
- ]
- ]},
- ]),
- () => {
- assert.equal(cur_frm.doc.volunteer_name, 'Test Volunteer');
- assert.equal(cur_frm.doc.volunteer_type, 'Test Work');
- assert.equal(cur_frm.doc.email, 'test@example.com');
- assert.equal(cur_frm.doc.availability, 'Weekends');
- },
- () => done()
- ]);
-
-});
diff --git a/erpnext/non_profit/doctype/volunteer_type/test_volunteer_type.js b/erpnext/non_profit/doctype/volunteer_type/test_volunteer_type.js
deleted file mode 100644
index 08baaf0..0000000
--- a/erpnext/non_profit/doctype/volunteer_type/test_volunteer_type.js
+++ /dev/null
@@ -1,27 +0,0 @@
-/* eslint-disable */
-// rename this file from _test_[name] to test_[name] to activate
-// and remove above this line
-
-QUnit.test("test: Volunteer Type", function (assert) {
- let done = assert.async();
-
- // number of asserts
- assert.expect(2);
-
- frappe.run_serially([
- // insert a new Member
- () => {
- return frappe.tests.make('Volunteer Type', [
- // values to be set
- {__newname: 'Test Work'},
- {amount: 500}
- ]);
- },
- () => {
- assert.equal(cur_frm.doc.name, 'Test Work');
- assert.equal(cur_frm.doc.amount, 500);
- },
- () => done()
- ]);
-
-});
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 147c02c..d2d1927 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -278,6 +278,8 @@
erpnext.patches.v13_0.add_custom_field_for_south_africa #2
erpnext.patches.v13_0.update_recipient_email_digest
erpnext.patches.v13_0.shopify_deprecation_warning
+erpnext.patches.v13_0.remove_bad_selling_defaults
+erpnext.patches.v13_0.trim_whitespace_from_serial_nos
erpnext.patches.v13_0.migrate_stripe_api
erpnext.patches.v13_0.reset_clearance_date_for_intracompany_payment_entries
erpnext.patches.v13_0.einvoicing_deprecation_warning
@@ -286,8 +288,7 @@
erpnext.patches.v14_0.delete_einvoicing_doctypes
erpnext.patches.v13_0.custom_fields_for_taxjar_integration #08-11-2021
erpnext.patches.v13_0.set_operation_time_based_on_operating_cost
-erpnext.patches.v13_0.validate_options_for_data_field
-erpnext.patches.v13_0.create_gst_payment_entry_fields
+erpnext.patches.v13_0.create_gst_payment_entry_fields #27-11-2021
erpnext.patches.v14_0.delete_shopify_doctypes
erpnext.patches.v13_0.fix_invoice_statuses
erpnext.patches.v13_0.create_website_items #30-09-2021
@@ -310,6 +311,7 @@
erpnext.patches.v13_0.enable_scheduler_job_for_item_reposting
erpnext.patches.v13_0.requeue_failed_reposts
erpnext.patches.v13_0.update_job_card_status
+erpnext.patches.v13_0.enable_uoms
erpnext.patches.v12_0.update_production_plan_status
erpnext.patches.v13_0.healthcare_deprecation_warning
erpnext.patches.v13_0.item_naming_series_not_mandatory
@@ -319,3 +321,14 @@
erpnext.patches.v14_0.delete_hub_doctypes
erpnext.patches.v13_0.create_ksa_vat_custom_fields
erpnext.patches.v13_0.fetch_thumbnail_in_website_items
+erpnext.patches.v14_0.rename_ongoing_status_in_sla_documents
+erpnext.patches.v14_0.migrate_crm_settings
+erpnext.patches.v13_0.rename_ksa_qr_field
+erpnext.patches.v13_0.disable_ksa_print_format_for_others # 16-12-2021
+erpnext.patches.v14_0.add_default_exit_questionnaire_notification_template
+erpnext.patches.v13_0.update_tax_category_for_rcm
+execute:frappe.delete_doc_if_exists('Workspace', 'ERPNext Integrations Settings')
+erpnext.patches.v14_0.set_payroll_cost_centers
+erpnext.patches.v13_0.agriculture_deprecation_warning
+erpnext.patches.v14_0.delete_agriculture_doctypes
+erpnext.patches.v13_0.update_exchange_rate_settings
diff --git a/erpnext/patches/v11_0/add_default_dispatch_notification_template.py b/erpnext/patches/v11_0/add_default_dispatch_notification_template.py
index 08006ad..c7771a5 100644
--- a/erpnext/patches/v11_0/add_default_dispatch_notification_template.py
+++ b/erpnext/patches/v11_0/add_default_dispatch_notification_template.py
@@ -22,4 +22,5 @@
delivery_settings = frappe.get_doc("Delivery Settings")
delivery_settings.dispatch_template = _("Dispatch Notification")
+ delivery_settings.flags.ignore_links = True
delivery_settings.save()
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 d157aad..d4fbded 100644
--- a/erpnext/patches/v12_0/create_itc_reversal_custom_fields.py
+++ b/erpnext/patches/v12_0/create_itc_reversal_custom_fields.py
@@ -97,6 +97,8 @@
'itc_central_tax': 0,
'itc_cess_amount': 0
})
+ if not gst_accounts:
+ continue
if d.account_head in gst_accounts.get('igst_account'):
amount_map[d.parent]['itc_integrated_tax'] += d.amount
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 1e230a7..36fe18d 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
@@ -7,6 +7,7 @@
def execute():
frappe.reload_doc("setup", "doctype", "target_detail")
+ frappe.reload_doc("core", "doctype", "prepared_report")
for d in ['Sales Person', 'Sales Partner', 'Territory']:
frappe.db.sql("""
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 37d850f..132f3bd 100644
--- a/erpnext/patches/v12_0/update_bom_in_so_mr.py
+++ b/erpnext/patches/v12_0/update_bom_in_so_mr.py
@@ -6,7 +6,7 @@
frappe.reload_doc("selling", "doctype", "sales_order_item")
for doctype in ["Sales Order", "Material Request"]:
- condition = " and child_doc.stock_qty > child_doc.produced_qty"
+ condition = " and child_doc.stock_qty > child_doc.produced_qty and doc.per_delivered < 100"
if doctype == "Material Request":
condition = " and doc.per_ordered < 100 and doc.material_request_type = 'Manufacture'"
@@ -15,5 +15,6 @@
child_doc.bom_no = item.default_bom
WHERE
child_doc.item_code = item.name and child_doc.docstatus < 2
+ and child_doc.parent = doc.name
and item.default_bom is not null and item.default_bom != '' {cond}
""".format(doc = doctype, cond = condition))
diff --git a/erpnext/patches/v13_0/add_default_interview_notification_templates.py b/erpnext/patches/v13_0/add_default_interview_notification_templates.py
index 0208ca9..6b5de52 100644
--- a/erpnext/patches/v13_0/add_default_interview_notification_templates.py
+++ b/erpnext/patches/v13_0/add_default_interview_notification_templates.py
@@ -32,4 +32,5 @@
hr_settings = frappe.get_doc('HR Settings')
hr_settings.interview_reminder_template = _('Interview Reminder')
hr_settings.feedback_reminder_notification_template = _('Interview Feedback Reminder')
+ hr_settings.flags.ignore_links = True
hr_settings.save()
diff --git a/erpnext/patches/v13_0/agriculture_deprecation_warning.py b/erpnext/patches/v13_0/agriculture_deprecation_warning.py
new file mode 100644
index 0000000..512444e
--- /dev/null
+++ b/erpnext/patches/v13_0/agriculture_deprecation_warning.py
@@ -0,0 +1,10 @@
+import click
+
+
+def execute():
+
+ click.secho(
+ "Agriculture Domain is moved to a separate app and will be removed from ERPNext in version-14.\n"
+ "Please install the app to continue using the Agriculture domain: https://github.com/frappe/agriculture",
+ fg="yellow",
+ )
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 b3ef5af..5e1df14 100644
--- a/erpnext/patches/v13_0/check_is_income_tax_component.py
+++ b/erpnext/patches/v13_0/check_is_income_tax_component.py
@@ -3,9 +3,9 @@
import frappe
+from frappe.custom.doctype.custom_field.custom_field import create_custom_field
import erpnext
-from erpnext.regional.india.setup import setup
def execute():
@@ -30,7 +30,14 @@
frappe.reload_doc('Regional', 'Report', report)
if erpnext.get_region() == "India":
- setup(patch=True)
+ create_custom_field('Salary Component',
+ dict(fieldname='component_type',
+ label='Component Type',
+ fieldtype='Select',
+ insert_after='description',
+ options='\nProvident Fund\nAdditional Provident Fund\nProvident Fund Loan\nProfessional Tax',
+ depends_on='eval:doc.type == "Deduction"')
+ )
if frappe.db.exists("Salary Component", "Income Tax"):
frappe.db.set_value("Salary Component", "Income Tax", "is_income_tax_component", 1)
diff --git a/erpnext/patches/v13_0/create_gst_payment_entry_fields.py b/erpnext/patches/v13_0/create_gst_payment_entry_fields.py
index 7e6d67c..4166945 100644
--- a/erpnext/patches/v13_0/create_gst_payment_entry_fields.py
+++ b/erpnext/patches/v13_0/create_gst_payment_entry_fields.py
@@ -9,24 +9,29 @@
frappe.reload_doc('accounts', 'doctype', 'advance_taxes_and_charges')
frappe.reload_doc('accounts', 'doctype', 'payment_entry')
- custom_fields = {
- 'Payment Entry': [
- dict(fieldname='gst_section', label='GST Details', fieldtype='Section Break', insert_after='deductions',
- print_hide=1, collapsible=1),
- dict(fieldname='company_address', label='Company Address', fieldtype='Link', insert_after='gst_section',
- print_hide=1, options='Address'),
- dict(fieldname='company_gstin', label='Company GSTIN',
- fieldtype='Data', insert_after='company_address',
- fetch_from='company_address.gstin', print_hide=1, read_only=1),
- dict(fieldname='place_of_supply', label='Place of Supply',
- fieldtype='Data', insert_after='company_gstin',
- print_hide=1, read_only=1),
- dict(fieldname='customer_address', label='Customer Address', fieldtype='Link', insert_after='place_of_supply',
- print_hide=1, options='Address', depends_on = 'eval:doc.party_type == "Customer"'),
- dict(fieldname='customer_gstin', label='Customer GSTIN',
- fieldtype='Data', insert_after='customer_address',
- fetch_from='customer_address.gstin', print_hide=1, read_only=1)
- ]
- }
+ if frappe.db.exists('Company', {'country': 'India'}):
+ custom_fields = {
+ 'Payment Entry': [
+ dict(fieldname='gst_section', label='GST Details', fieldtype='Section Break', insert_after='deductions',
+ print_hide=1, collapsible=1),
+ dict(fieldname='company_address', label='Company Address', fieldtype='Link', insert_after='gst_section',
+ print_hide=1, options='Address'),
+ dict(fieldname='company_gstin', label='Company GSTIN',
+ fieldtype='Data', insert_after='company_address',
+ fetch_from='company_address.gstin', print_hide=1, read_only=1),
+ dict(fieldname='place_of_supply', label='Place of Supply',
+ fieldtype='Data', insert_after='company_gstin',
+ print_hide=1, read_only=1),
+ dict(fieldname='customer_address', label='Customer Address', fieldtype='Link', insert_after='place_of_supply',
+ print_hide=1, options='Address', depends_on = 'eval:doc.party_type == "Customer"'),
+ dict(fieldname='customer_gstin', label='Customer GSTIN',
+ fieldtype='Data', insert_after='customer_address',
+ fetch_from='customer_address.gstin', print_hide=1, read_only=1)
+ ]
+ }
- create_custom_fields(custom_fields, update=True)
\ No newline at end of file
+ create_custom_fields(custom_fields, update=True)
+ else:
+ fields = ['gst_section', 'company_address', 'company_gstin', 'place_of_supply', 'customer_address', 'customer_gstin']
+ for field in fields:
+ frappe.delete_doc_if_exists("Custom Field", f"Payment Entry-{field}")
\ No newline at end of file
diff --git a/erpnext/patches/v13_0/disable_ksa_print_format_for_others.py b/erpnext/patches/v13_0/disable_ksa_print_format_for_others.py
new file mode 100644
index 0000000..aa2a2d3
--- /dev/null
+++ b/erpnext/patches/v13_0/disable_ksa_print_format_for_others.py
@@ -0,0 +1,19 @@
+# Copyright (c) 2020, Wahni Green Technologies and Contributors
+# License: GNU General Public License v3. See license.txt
+
+import frappe
+
+from erpnext.regional.saudi_arabia.setup import add_print_formats
+
+
+def execute():
+ company = frappe.get_all('Company', filters = {'country': 'Saudi Arabia'})
+ if company:
+ add_print_formats()
+ return
+
+ if frappe.db.exists('DocType', 'Print Format'):
+ frappe.reload_doc("regional", "print_format", "ksa_vat_invoice", force=True)
+ frappe.reload_doc("regional", "print_format", "ksa_pos_invoice", force=True)
+ for d in ('KSA VAT Invoice', 'KSA POS Invoice'):
+ frappe.db.set_value("Print Format", d, "disabled", 1)
diff --git a/erpnext/patches/v13_0/enable_uoms.py b/erpnext/patches/v13_0/enable_uoms.py
new file mode 100644
index 0000000..4d3f637
--- /dev/null
+++ b/erpnext/patches/v13_0/enable_uoms.py
@@ -0,0 +1,13 @@
+import frappe
+
+
+def execute():
+ frappe.reload_doc('setup', 'doctype', 'uom')
+
+ uom = frappe.qb.DocType("UOM")
+
+ (frappe.qb
+ .update(uom)
+ .set(uom.enabled, 1)
+ .where(uom.creation >= "2021-10-18") # date when this field was released
+ ).run()
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 e4cb9ae..0f2ac4b 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
@@ -6,10 +6,19 @@
def execute():
- for doctype in ('repost_item_valuation', 'stock_entry_detail', 'purchase_receipt_item',
- 'purchase_invoice_item', 'delivery_note_item', 'sales_invoice_item', 'packed_item'):
- frappe.reload_doc('stock', 'doctype', doctype)
- frappe.reload_doc('buying', 'doctype', 'purchase_receipt_item_supplied')
+ doctypes_to_reload = [
+ ("stock", "repost_item_valuation"),
+ ("stock", "stock_entry_detail"),
+ ("stock", "purchase_receipt_item"),
+ ("stock", "delivery_note_item"),
+ ("stock", "packed_item"),
+ ("accounts", "sales_invoice_item"),
+ ("accounts", "purchase_invoice_item"),
+ ("buying", "purchase_receipt_item_supplied")
+ ]
+
+ for module, doctype in doctypes_to_reload:
+ frappe.reload_doc(module, 'doctype', doctype)
reposting_project_deployed_on = get_creation_time()
posting_date = getdate(reposting_project_deployed_on)
diff --git a/erpnext/patches/v13_0/remove_bad_selling_defaults.py b/erpnext/patches/v13_0/remove_bad_selling_defaults.py
new file mode 100644
index 0000000..5487a6c
--- /dev/null
+++ b/erpnext/patches/v13_0/remove_bad_selling_defaults.py
@@ -0,0 +1,15 @@
+import frappe
+from frappe import _
+
+
+def execute():
+ selling_settings = frappe.get_single("Selling Settings")
+
+ if selling_settings.customer_group in (_("All Customer Groups"), "All Customer Groups"):
+ selling_settings.customer_group = None
+
+ if selling_settings.territory in (_("All Territories"), "All Territories"):
+ selling_settings.territory = None
+
+ selling_settings.flags.ignore_mandatory=True
+ selling_settings.save(ignore_permissions=True)
diff --git a/erpnext/patches/v13_0/rename_ksa_qr_field.py b/erpnext/patches/v13_0/rename_ksa_qr_field.py
new file mode 100644
index 0000000..f4f9b17
--- /dev/null
+++ b/erpnext/patches/v13_0/rename_ksa_qr_field.py
@@ -0,0 +1,32 @@
+# Copyright (c) 2020, Wahni Green Technologies and Contributors
+# License: GNU General Public License v3. See license.txt
+
+import frappe
+from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
+from frappe.model.utils.rename_field import rename_field
+
+
+def execute():
+ company = frappe.get_all('Company', filters = {'country': 'Saudi Arabia'})
+ if not company:
+ return
+
+ if frappe.db.exists('DocType', 'Sales Invoice'):
+ frappe.reload_doc('accounts', 'doctype', 'sales_invoice', force=True)
+
+ # rename_field method assumes that the field already exists or the doc is synced
+ if not frappe.db.has_column('Sales Invoice', 'ksa_einv_qr'):
+ create_custom_fields({
+ 'Sales Invoice': [
+ dict(
+ fieldname='ksa_einv_qr',
+ label='KSA E-Invoicing QR',
+ fieldtype='Attach Image',
+ read_only=1, no_copy=1, hidden=1
+ )
+ ]
+ })
+
+ if frappe.db.has_column('Sales Invoice', 'qr_code'):
+ rename_field('Sales Invoice', 'qr_code', 'ksa_einv_qr')
+ frappe.delete_doc_if_exists("Custom Field", "Sales Invoice-qr_code")
diff --git a/erpnext/patches/v13_0/trim_whitespace_from_serial_nos.py b/erpnext/patches/v13_0/trim_whitespace_from_serial_nos.py
new file mode 100644
index 0000000..8a9633d
--- /dev/null
+++ b/erpnext/patches/v13_0/trim_whitespace_from_serial_nos.py
@@ -0,0 +1,65 @@
+import frappe
+
+from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
+
+
+def execute():
+ broken_sles = frappe.db.sql("""
+ select name, serial_no
+ from `tabStock Ledger Entry`
+ where
+ is_cancelled = 0
+ and (serial_no like %s or serial_no like %s or serial_no like %s or serial_no like %s)
+ """,
+ (
+ " %", # leading whitespace
+ "% ", # trailing whitespace
+ "%\n %", # leading whitespace on newline
+ "% \n%", # trailing whitespace on newline
+ ),
+ as_dict=True,
+ )
+
+ frappe.db.MAX_WRITES_PER_TRANSACTION += len(broken_sles)
+
+ if not broken_sles:
+ return
+
+ broken_serial_nos = set()
+
+ # patch SLEs
+ for sle in broken_sles:
+ serial_no_list = get_serial_nos(sle.serial_no)
+ correct_sr_no = "\n".join(serial_no_list)
+
+ if correct_sr_no == sle.serial_no:
+ continue
+
+ frappe.db.set_value("Stock Ledger Entry", sle.name, "serial_no", correct_sr_no, update_modified=False)
+ broken_serial_nos.update(serial_no_list)
+
+ if not broken_serial_nos:
+ return
+
+ # Patch serial No documents if they don't have purchase info
+ # Purchase info is used for fetching incoming rate
+ broken_sr_no_records = frappe.get_list("Serial No",
+ filters={
+ "status":"Active",
+ "name": ("in", broken_serial_nos),
+ "purchase_document_type": ("is", "not set")
+ },
+ pluck="name",
+ )
+
+ frappe.db.MAX_WRITES_PER_TRANSACTION += len(broken_sr_no_records)
+
+ patch_savepoint = "serial_no_patch"
+ for serial_no in broken_sr_no_records:
+ try:
+ frappe.db.savepoint(patch_savepoint)
+ sn = frappe.get_doc("Serial No", serial_no)
+ sn.update_serial_no_reference()
+ sn.db_update()
+ except Exception:
+ frappe.db.rollback(save_point=patch_savepoint)
diff --git a/erpnext/patches/v13_0/update_exchange_rate_settings.py b/erpnext/patches/v13_0/update_exchange_rate_settings.py
new file mode 100644
index 0000000..b7ec232
--- /dev/null
+++ b/erpnext/patches/v13_0/update_exchange_rate_settings.py
@@ -0,0 +1,10 @@
+import frappe
+
+from erpnext.setup.install import setup_currency_exchange
+
+
+def execute():
+ frappe.reload_doc("accounts", "doctype", "currency_exchange_settings")
+ frappe.reload_doc("accounts", "doctype", "currency_exchange_settings_result")
+ frappe.reload_doc("accounts", "doctype", "currency_exchange_settings_details")
+ setup_currency_exchange()
\ No newline at end of file
diff --git a/erpnext/patches/v13_0/update_tax_category_for_rcm.py b/erpnext/patches/v13_0/update_tax_category_for_rcm.py
new file mode 100644
index 0000000..7af2366
--- /dev/null
+++ b/erpnext/patches/v13_0/update_tax_category_for_rcm.py
@@ -0,0 +1,31 @@
+import frappe
+from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
+
+from erpnext.regional.india import states
+
+
+def execute():
+ company = frappe.get_all('Company', filters = {'country': 'India'})
+ if not company:
+ return
+
+ create_custom_fields({
+ 'Tax Category': [
+ dict(fieldname='is_inter_state', label='Is Inter State',
+ fieldtype='Check', insert_after='disabled', print_hide=1),
+ dict(fieldname='is_reverse_charge', label='Is Reverse Charge', fieldtype='Check',
+ insert_after='is_inter_state', print_hide=1),
+ dict(fieldname='tax_category_column_break', fieldtype='Column Break',
+ insert_after='is_reverse_charge'),
+ dict(fieldname='gst_state', label='Source State', fieldtype='Select',
+ options='\n'.join(states), insert_after='company')
+ ]
+ }, update=True)
+
+ tax_category = frappe.qb.DocType("Tax Category")
+
+ frappe.qb.update(tax_category).set(
+ tax_category.is_reverse_charge, 1
+ ).where(
+ tax_category.name.isin(['Reverse Charge Out-State', 'Reverse Charge In-State'])
+ ).run()
\ No newline at end of file
diff --git a/erpnext/patches/v13_0/validate_options_for_data_field.py b/erpnext/patches/v13_0/validate_options_for_data_field.py
deleted file mode 100644
index ad777b8..0000000
--- a/erpnext/patches/v13_0/validate_options_for_data_field.py
+++ /dev/null
@@ -1,26 +0,0 @@
-# Copyright (c) 2021, Frappe and Contributors
-# License: GNU General Public License v3. See license.txt
-
-
-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/v14_0/add_default_exit_questionnaire_notification_template.py b/erpnext/patches/v14_0/add_default_exit_questionnaire_notification_template.py
new file mode 100644
index 0000000..120182a
--- /dev/null
+++ b/erpnext/patches/v14_0/add_default_exit_questionnaire_notification_template.py
@@ -0,0 +1,28 @@
+import os
+
+import frappe
+from frappe import _
+
+
+def execute():
+ frappe.reload_doc("email", "doctype", "email_template")
+ frappe.reload_doc("hr", "doctype", "hr_settings")
+
+ template = frappe.db.exists("Email Template", _("Exit Questionnaire Notification"))
+ if not template:
+ base_path = frappe.get_app_path("erpnext", "hr", "doctype")
+ response = frappe.read_file(os.path.join(base_path, "exit_interview/exit_questionnaire_notification_template.html"))
+
+ template = frappe.get_doc({
+ "doctype": "Email Template",
+ "name": _("Exit Questionnaire Notification"),
+ "response": response,
+ "subject": _("Exit Questionnaire Notification"),
+ "owner": frappe.session.user,
+ }).insert(ignore_permissions=True)
+ template = template.name
+
+ hr_settings = frappe.get_doc("HR Settings")
+ hr_settings.exit_questionnaire_notification_template = template
+ hr_settings.flags.ignore_links = True
+ hr_settings.save()
diff --git a/erpnext/patches/v14_0/delete_agriculture_doctypes.py b/erpnext/patches/v14_0/delete_agriculture_doctypes.py
new file mode 100644
index 0000000..d7fe832
--- /dev/null
+++ b/erpnext/patches/v14_0/delete_agriculture_doctypes.py
@@ -0,0 +1,19 @@
+import frappe
+
+
+def execute():
+ frappe.delete_doc("Module Def", "Agriculture", ignore_missing=True, force=True)
+
+ frappe.delete_doc("Workspace", "Agriculture", ignore_missing=True, force=True)
+
+ reports = frappe.get_all("Report", {"module": "agriculture", "is_standard": "Yes"}, pluck='name')
+ for report in reports:
+ frappe.delete_doc("Report", report, ignore_missing=True, force=True)
+
+ dashboards = frappe.get_all("Dashboard", {"module": "agriculture", "is_standard": 1}, pluck='name')
+ for dashboard in dashboards:
+ frappe.delete_doc("Dashboard", dashboard, ignore_missing=True, force=True)
+
+ doctypes = frappe.get_all("DocType", {"module": "agriculture", "custom": 0}, pluck='name')
+ for doctype in doctypes:
+ frappe.delete_doc("DocType", doctype, ignore_missing=True)
diff --git a/erpnext/patches/v14_0/migrate_crm_settings.py b/erpnext/patches/v14_0/migrate_crm_settings.py
new file mode 100644
index 0000000..30d3ea0
--- /dev/null
+++ b/erpnext/patches/v14_0/migrate_crm_settings.py
@@ -0,0 +1,16 @@
+import frappe
+
+
+def execute():
+ settings = frappe.db.get_value('Selling Settings', 'Selling Settings', [
+ 'campaign_naming_by',
+ 'close_opportunity_after_days',
+ 'default_valid_till'
+ ], as_dict=True)
+
+ frappe.reload_doc('crm', 'doctype', 'crm_settings')
+ frappe.db.set_value('CRM Settings', 'CRM Settings', {
+ 'campaign_naming_by': settings.campaign_naming_by,
+ 'close_opportunity_after_days': settings.close_opportunity_after_days,
+ 'default_valid_till': settings.default_valid_till
+ })
diff --git a/erpnext/patches/v14_0/rename_ongoing_status_in_sla_documents.py b/erpnext/patches/v14_0/rename_ongoing_status_in_sla_documents.py
new file mode 100644
index 0000000..1cc5f38
--- /dev/null
+++ b/erpnext/patches/v14_0/rename_ongoing_status_in_sla_documents.py
@@ -0,0 +1,27 @@
+import frappe
+
+
+def execute():
+ active_sla_documents = [sla.document_type for sla in frappe.get_all("Service Level Agreement", fields=["document_type"])]
+
+ for doctype in active_sla_documents:
+ doctype = frappe.qb.DocType(doctype)
+ try:
+ frappe.qb.update(
+ doctype
+ ).set(
+ doctype.agreement_status, 'First Response Due'
+ ).where(
+ doctype.first_responded_on.isnull()
+ ).run()
+
+ frappe.qb.update(
+ doctype
+ ).set(
+ doctype.agreement_status, 'Resolution Due'
+ ).where(
+ doctype.agreement_status == 'Ongoing'
+ ).run()
+
+ except Exception:
+ frappe.log_error(title='Failed to Patch SLA Status')
\ No newline at end of file
diff --git a/erpnext/patches/v14_0/set_payroll_cost_centers.py b/erpnext/patches/v14_0/set_payroll_cost_centers.py
new file mode 100644
index 0000000..89b305b
--- /dev/null
+++ b/erpnext/patches/v14_0/set_payroll_cost_centers.py
@@ -0,0 +1,32 @@
+import frappe
+
+
+def execute():
+ frappe.reload_doc('payroll', 'doctype', 'employee_cost_center')
+ frappe.reload_doc('payroll', 'doctype', 'salary_structure_assignment')
+
+ employees = frappe.get_all("Employee", fields=["department", "payroll_cost_center", "name"])
+
+ employee_cost_center = {}
+ for d in employees:
+ cost_center = d.payroll_cost_center
+ if not cost_center and d.department:
+ cost_center = frappe.get_cached_value("Department", d.department, "payroll_cost_center")
+
+ if cost_center:
+ employee_cost_center.setdefault(d.name, cost_center)
+
+ salary_structure_assignments = frappe.get_all("Salary Structure Assignment",
+ filters = {"docstatus": ["!=", 2]},
+ fields=["name", "employee"])
+
+ for d in salary_structure_assignments:
+ cost_center = employee_cost_center.get(d.employee)
+ if cost_center:
+ assignment = frappe.get_doc("Salary Structure Assignment", d.name)
+ if not assignment.get("payroll_cost_centers"):
+ assignment.append("payroll_cost_centers", {
+ "cost_center": cost_center,
+ "percentage": 100
+ })
+ assignment.save()
\ No newline at end of file
diff --git a/erpnext/agriculture/doctype/weather_parameter/__init__.py b/erpnext/payroll/doctype/employee_cost_center/__init__.py
similarity index 100%
rename from erpnext/agriculture/doctype/weather_parameter/__init__.py
rename to erpnext/payroll/doctype/employee_cost_center/__init__.py
diff --git a/erpnext/payroll/doctype/employee_cost_center/employee_cost_center.json b/erpnext/payroll/doctype/employee_cost_center/employee_cost_center.json
new file mode 100644
index 0000000..8fed9f7
--- /dev/null
+++ b/erpnext/payroll/doctype/employee_cost_center/employee_cost_center.json
@@ -0,0 +1,43 @@
+{
+ "actions": [],
+ "creation": "2021-12-23 12:44:38.389283",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "cost_center",
+ "percentage"
+ ],
+ "fields": [
+ {
+ "allow_on_submit": 1,
+ "fieldname": "cost_center",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Cost Center",
+ "options": "Cost Center",
+ "reqd": 1
+ },
+ {
+ "allow_on_submit": 1,
+ "fieldname": "percentage",
+ "fieldtype": "Int",
+ "in_list_view": 1,
+ "label": "Percentage (%)",
+ "non_negative": 1,
+ "reqd": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-12-23 17:39:03.410924",
+ "modified_by": "Administrator",
+ "module": "Payroll",
+ "name": "Employee Cost Center",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/erpnext/payroll/doctype/employee_cost_center/employee_cost_center.py b/erpnext/payroll/doctype/employee_cost_center/employee_cost_center.py
new file mode 100644
index 0000000..6c5be97
--- /dev/null
+++ b/erpnext/payroll/doctype/employee_cost_center/employee_cost_center.py
@@ -0,0 +1,9 @@
+# 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 EmployeeCostCenter(Document):
+ pass
diff --git a/erpnext/payroll/doctype/gratuity/gratuity.py b/erpnext/payroll/doctype/gratuity/gratuity.py
index f563c08..476990a 100644
--- a/erpnext/payroll/doctype/gratuity/gratuity.py
+++ b/erpnext/payroll/doctype/gratuity/gratuity.py
@@ -193,7 +193,7 @@
sal_slip = get_last_salary_slip(employee)
if not sal_slip:
frappe.throw(_("No Salary Slip is found for Employee: {0}").format(bold(employee)))
- component_and_amounts = frappe.get_list("Salary Detail",
+ component_and_amounts = frappe.get_all("Salary Detail",
filters={
"docstatus": 1,
'parent': sal_slip,
diff --git a/erpnext/payroll/doctype/gratuity/test_gratuity.py b/erpnext/payroll/doctype/gratuity/test_gratuity.py
index 78355ca..93cba06 100644
--- a/erpnext/payroll/doctype/gratuity/test_gratuity.py
+++ b/erpnext/payroll/doctype/gratuity/test_gratuity.py
@@ -48,7 +48,7 @@
self.assertEqual(floor(experience), gratuity.current_work_experience)
#amount Calculation
- component_amount = frappe.get_list("Salary Detail",
+ component_amount = frappe.get_all("Salary Detail",
filters={
"docstatus": 1,
'parent': sal_slip,
@@ -84,7 +84,7 @@
self.assertEqual(floor(experience), gratuity.current_work_experience)
#amount Calculation
- component_amount = frappe.get_list("Salary Detail",
+ component_amount = frappe.get_all("Salary Detail",
filters={
"docstatus": 1,
'parent': sal_slip,
diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
index 84c59a2..ed3fa5b 100644
--- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
+++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
@@ -7,6 +7,7 @@
from frappe import _
from frappe.desk.reportview import get_filters_cond, get_match_cond
from frappe.model.document import Document
+from frappe.query_builder.functions import Coalesce
from frappe.utils import (
DATE_FORMAT,
add_days,
@@ -157,11 +158,20 @@
Returns list of salary slips based on selected criteria
"""
- ss_list = frappe.db.sql("""
- select t1.name, t1.salary_structure, t1.payroll_cost_center from `tabSalary Slip` t1
- where t1.docstatus = %s and t1.start_date >= %s and t1.end_date <= %s and t1.payroll_entry = %s
- and (t1.journal_entry is null or t1.journal_entry = "") and ifnull(salary_slip_based_on_timesheet,0) = %s
- """, (ss_status, self.start_date, self.end_date, self.name, self.salary_slip_based_on_timesheet), as_dict=as_dict)
+ ss = frappe.qb.DocType("Salary Slip")
+ ss_list = (
+ frappe.qb.from_(ss)
+ .select(ss.name, ss.salary_structure)
+ .where(
+ (ss.docstatus == ss_status)
+ & (ss.start_date >= self.start_date)
+ & (ss.end_date <= self.end_date)
+ & (ss.payroll_entry == self.name)
+ & ((ss.journal_entry.isnull()) | (ss.journal_entry == ""))
+ & (Coalesce(ss.salary_slip_based_on_timesheet, 0) == self.salary_slip_based_on_timesheet)
+ )
+ ).run(as_dict=as_dict)
+
return ss_list
@frappe.whitelist()
@@ -190,13 +200,20 @@
def get_salary_components(self, component_type):
salary_slips = self.get_sal_slip_list(ss_status = 1, as_dict = True)
+
if salary_slips:
- salary_components = frappe.db.sql("""
- select ssd.salary_component, ssd.amount, ssd.parentfield, ss.payroll_cost_center
- from `tabSalary Slip` ss, `tabSalary Detail` ssd
- where ss.name = ssd.parent and ssd.parentfield = '%s' and ss.name in (%s)
- """ % (component_type, ', '.join(['%s']*len(salary_slips))),
- tuple([d.name for d in salary_slips]), as_dict=True)
+ ss = frappe.qb.DocType("Salary Slip")
+ ssd = frappe.qb.DocType("Salary Detail")
+ salary_components = (
+ frappe.qb.from_(ss)
+ .join(ssd)
+ .on(ss.name == ssd.parent)
+ .select(ssd.salary_component, ssd.amount, ssd.parentfield, ss.salary_structure, ss.employee)
+ .where(
+ (ssd.parentfield == component_type)
+ & (ss.name.isin(tuple([d.name for d in salary_slips])))
+ )
+ ).run(as_dict=True)
return salary_components
@@ -204,18 +221,49 @@
salary_components = self.get_salary_components(component_type)
if salary_components:
component_dict = {}
+ self.employee_cost_centers = {}
for item in salary_components:
+ employee_cost_centers = self.get_payroll_cost_centers_for_employee(item.employee, item.salary_structure)
+
add_component_to_accrual_jv_entry = True
if component_type == "earnings":
- is_flexible_benefit, only_tax_impact = frappe.db.get_value("Salary Component", item['salary_component'], ['is_flexible_benefit', 'only_tax_impact'])
+ is_flexible_benefit, only_tax_impact = \
+ frappe.get_cached_value("Salary Component",item['salary_component'], ['is_flexible_benefit', 'only_tax_impact'])
if is_flexible_benefit == 1 and only_tax_impact ==1:
add_component_to_accrual_jv_entry = False
+
if add_component_to_accrual_jv_entry:
- component_dict[(item.salary_component, item.payroll_cost_center)] \
- = component_dict.get((item.salary_component, item.payroll_cost_center), 0) + flt(item.amount)
+ for cost_center, percentage in employee_cost_centers.items():
+ amount_against_cost_center = flt(item.amount) * percentage / 100
+ component_dict[(item.salary_component, cost_center)] \
+ = component_dict.get((item.salary_component, cost_center), 0) + amount_against_cost_center
+
account_details = self.get_account(component_dict = component_dict)
return account_details
+ def get_payroll_cost_centers_for_employee(self, employee, salary_structure):
+ if not self.employee_cost_centers.get(employee):
+ ss_assignment_name = frappe.db.get_value("Salary Structure Assignment",
+ {"employee": employee, "salary_structure": salary_structure, "docstatus": 1}, 'name')
+
+ if ss_assignment_name:
+ cost_centers = dict(frappe.get_all("Employee Cost Center", {"parent": ss_assignment_name},
+ ["cost_center", "percentage"], as_list=1))
+ if not cost_centers:
+ default_cost_center, department = frappe.get_cached_value("Employee", employee, ["payroll_cost_center", "department"])
+ if not default_cost_center and department:
+ default_cost_center = frappe.get_cached_value("Department", department, "payroll_cost_center")
+ if not default_cost_center:
+ default_cost_center = self.cost_center
+
+ cost_centers = {
+ default_cost_center: 100
+ }
+
+ self.employee_cost_centers.setdefault(employee, cost_centers)
+
+ return self.employee_cost_centers.get(employee, {})
+
def get_account(self, component_dict = None):
account_dict = {}
for key, amount in component_dict.items():
@@ -350,23 +398,24 @@
currencies = []
multi_currency = 0
company_currency = erpnext.get_company_currency(self.company)
+ accounting_dimensions = get_accounting_dimensions() or []
exchange_rate, amount = self.get_amount_and_exchange_rate_for_journal_entry(self.payment_account, je_payment_amount, company_currency, currencies)
- accounts.append({
+ accounts.append(self.update_accounting_dimensions({
"account": self.payment_account,
"bank_account": self.bank_account,
"credit_in_account_currency": flt(amount, precision),
"exchange_rate": flt(exchange_rate),
- })
+ }, accounting_dimensions))
exchange_rate, amount = self.get_amount_and_exchange_rate_for_journal_entry(payroll_payable_account, je_payment_amount, company_currency, currencies)
- accounts.append({
+ accounts.append(self.update_accounting_dimensions({
"account": payroll_payable_account,
"debit_in_account_currency": flt(amount, precision),
"exchange_rate": flt(exchange_rate),
"reference_type": self.doctype,
"reference_name": self.name
- })
+ }, accounting_dimensions))
if len(currencies) > 1:
multi_currency = 1
diff --git a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.js b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.js
deleted file mode 100644
index d24f243..0000000
--- a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.js
+++ /dev/null
@@ -1,62 +0,0 @@
-QUnit.module('HR');
-
-QUnit.test("test: Payroll Entry", function (assert) {
- assert.expect(5);
- let done = assert.async();
- let employees, docname;
-
- frappe.run_serially([
- () => {
- return frappe.tests.make('Payroll Entry', [
- {company: 'For Testing'},
- {posting_date: frappe.datetime.add_days(frappe.datetime.nowdate(), 0)},
- {payroll_frequency: 'Monthly'},
- {cost_center: 'Main - '+frappe.get_abbr(frappe.defaults.get_default("Company"))}
- ]);
- },
-
- () => frappe.timeout(1),
- () => {
- assert.equal(cur_frm.doc.company, 'For Testing');
- assert.equal(cur_frm.doc.posting_date, frappe.datetime.add_days(frappe.datetime.nowdate(), 0));
- assert.equal(cur_frm.doc.cost_center, 'Main - FT');
- },
- () => frappe.click_button('Get Employee Details'),
- () => {
- employees = cur_frm.doc.employees.length;
- docname = cur_frm.doc.name;
- },
-
- () => frappe.click_button('Submit'),
- () => frappe.timeout(1),
- () => frappe.click_button('Yes'),
- () => frappe.timeout(5),
-
- () => frappe.click_button('View Salary Slip'),
- () => frappe.timeout(2),
- () => assert.equal(cur_list.data.length, employees),
-
- () => frappe.set_route('Form', 'Payroll Entry', docname),
- () => frappe.timeout(2),
- () => frappe.click_button('Submit Salary Slip'),
- () => frappe.click_button('Yes'),
- () => frappe.timeout(5),
-
- () => frappe.click_button('Close'),
- () => frappe.timeout(1),
-
- () => frappe.click_button('View Salary Slip'),
- () => frappe.timeout(2),
- () => {
- let count = 0;
- for(var i = 0; i < employees; i++) {
- if(cur_list.data[i].docstatus == 1){
- count++;
- }
- }
- assert.equal(count, employees, "Salary Slip submitted for all employees");
- },
-
- () => done()
- ]);
-});
diff --git a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py
index c6f3897..4f097fa 100644
--- a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py
+++ b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py
@@ -120,8 +120,7 @@
employee1 = make_employee("test_employee1@example.com", payroll_cost_center="_Test Cost Center - _TC",
department="cc - _TC", company="_Test Company")
- employee2 = make_employee("test_employee2@example.com", payroll_cost_center="_Test Cost Center 2 - _TC",
- department="cc - _TC", company="_Test Company")
+ employee2 = make_employee("test_employee2@example.com", department="cc - _TC", company="_Test Company")
if not frappe.db.exists("Account", "_Test Payroll Payable - _TC"):
create_account(account_name="_Test Payroll Payable",
@@ -132,8 +131,26 @@
frappe.db.set_value("Company", "_Test Company", "default_payroll_payable_account",
"_Test Payroll Payable - _TC")
currency=frappe.db.get_value("Company", "_Test Company", "default_currency")
+
make_salary_structure("_Test Salary Structure 1", "Monthly", employee1, company="_Test Company", currency=currency, test_tax=False)
- make_salary_structure("_Test Salary Structure 2", "Monthly", employee2, company="_Test Company", currency=currency, test_tax=False)
+ ss = make_salary_structure("_Test Salary Structure 2", "Monthly", employee2, company="_Test Company", currency=currency, test_tax=False)
+
+ # update cost centers in salary structure assignment for employee2
+ ssa = frappe.db.get_value("Salary Structure Assignment",
+ {"employee": employee2, "salary_structure": ss.name, "docstatus": 1}, 'name')
+
+ ssa_doc = frappe.get_doc("Salary Structure Assignment", ssa)
+ ssa_doc.payroll_cost_centers = []
+ ssa_doc.append("payroll_cost_centers", {
+ "cost_center": "_Test Cost Center - _TC",
+ "percentage": 60
+ })
+ ssa_doc.append("payroll_cost_centers", {
+ "cost_center": "_Test Cost Center 2 - _TC",
+ "percentage": 40
+ })
+
+ ssa_doc.save()
dates = get_start_end_dates('Monthly', nowdate())
if not frappe.db.get_value("Salary Slip", {"start_date": dates.start_date, "end_date": dates.end_date}):
@@ -148,10 +165,10 @@
""", je)
expected_je = (
('_Test Payroll Payable - _TC', 'Main - _TC', 0.0, 155600.0),
- ('Salary - _TC', '_Test Cost Center - _TC', 78000.0, 0.0),
- ('Salary - _TC', '_Test Cost Center 2 - _TC', 78000.0, 0.0),
- ('Salary Deductions - _TC', '_Test Cost Center - _TC', 0.0, 200.0),
- ('Salary Deductions - _TC', '_Test Cost Center 2 - _TC', 0.0, 200.0)
+ ('Salary - _TC', '_Test Cost Center - _TC', 124800.0, 0.0),
+ ('Salary - _TC', '_Test Cost Center 2 - _TC', 31200.0, 0.0),
+ ('Salary Deductions - _TC', '_Test Cost Center - _TC', 0.0, 320.0),
+ ('Salary Deductions - _TC', '_Test Cost Center 2 - _TC', 0.0, 80.0)
)
self.assertEqual(je_entries, expected_je)
diff --git a/erpnext/payroll/doctype/payroll_entry/test_set_salary_components.js b/erpnext/payroll/doctype/payroll_entry/test_set_salary_components.js
deleted file mode 100644
index 092cbd8..0000000
--- a/erpnext/payroll/doctype/payroll_entry/test_set_salary_components.js
+++ /dev/null
@@ -1,61 +0,0 @@
-QUnit.module('HR');
-
-QUnit.test("test: Set Salary Components", function (assert) {
- assert.expect(5);
- let done = assert.async();
-
- frappe.run_serially([
- () => frappe.set_route('Form', 'Salary Component', 'Leave Encashment'),
- () => {
- var row = frappe.model.add_child(cur_frm.doc, "Salary Component Account", "accounts");
- row.company = 'For Testing';
- row.account = 'Salary - FT';
- },
-
- () => cur_frm.save(),
- () => frappe.timeout(2),
- () => assert.equal(cur_frm.doc.accounts[0].account, 'Salary - FT'),
-
- () => frappe.set_route('Form', 'Salary Component', 'Basic'),
- () => {
- var row = frappe.model.add_child(cur_frm.doc, "Salary Component Account", "accounts");
- row.company = 'For Testing';
- row.account = 'Salary - FT';
- },
-
- () => cur_frm.save(),
- () => frappe.timeout(2),
- () => assert.equal(cur_frm.doc.accounts[0].account, 'Salary - FT'),
-
- () => frappe.set_route('Form', 'Salary Component', 'Income Tax'),
- () => {
- var row = frappe.model.add_child(cur_frm.doc, "Salary Component Account", "accounts");
- row.company = 'For Testing';
- row.account = 'Salary - FT';
- },
-
- () => cur_frm.save(),
- () => frappe.timeout(2),
- () => assert.equal(cur_frm.doc.accounts[0].account, 'Salary - FT'),
-
- () => frappe.set_route('Form', 'Salary Component', 'Arrear'),
- () => {
- var row = frappe.model.add_child(cur_frm.doc, "Salary Component Account", "accounts");
- row.company = 'For Testing';
- row.account = 'Salary - FT';
- },
-
- () => cur_frm.save(),
- () => frappe.timeout(2),
- () => assert.equal(cur_frm.doc.accounts[0].account, 'Salary - FT'),
-
- () => frappe.set_route('Form', 'Company', 'For Testing'),
- () => cur_frm.set_value('default_payroll_payable_account', 'Payroll Payable - FT'),
- () => cur_frm.save(),
- () => frappe.timeout(2),
- () => assert.equal(cur_frm.doc.default_payroll_payable_account, 'Payroll Payable - FT'),
-
- () => done()
-
- ]);
-});
diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.json b/erpnext/payroll/doctype/salary_slip/salary_slip.json
index 7a80e69..4e40e13 100644
--- a/erpnext/payroll/doctype/salary_slip/salary_slip.json
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.json
@@ -12,7 +12,6 @@
"department",
"designation",
"branch",
- "payroll_cost_center",
"column_break1",
"status",
"journal_entry",
@@ -463,15 +462,6 @@
"read_only": 1
},
{
- "fetch_from": "employee.payroll_cost_center",
- "fetch_if_empty": 1,
- "fieldname": "payroll_cost_center",
- "fieldtype": "Link",
- "label": "Payroll Cost Center",
- "options": "Cost Center",
- "read_only": 1
- },
- {
"fieldname": "mode_of_payment",
"fieldtype": "Select",
"label": "Mode Of Payment",
@@ -647,7 +637,7 @@
"idx": 9,
"is_submittable": 1,
"links": [],
- "modified": "2021-10-08 11:47:47.098248",
+ "modified": "2021-12-23 11:47:47.098248",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Salary Slip",
diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py
index 05af09e..f33443d 100644
--- a/erpnext/payroll/doctype/salary_slip/salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py
@@ -932,18 +932,23 @@
def get_future_recurring_additional_amount(self, additional_salary, monthly_additional_amount):
future_recurring_additional_amount = 0
to_date = frappe.db.get_value("Additional Salary", additional_salary, 'to_date')
+
# future month count excluding current
- future_recurring_period = (getdate(to_date).month - getdate(self.start_date).month)
+ from_date, to_date = getdate(self.start_date), getdate(to_date)
+ future_recurring_period = ((to_date.year - from_date.year) * 12) + (to_date.month - from_date.month)
+
if future_recurring_period > 0:
future_recurring_additional_amount = monthly_additional_amount * future_recurring_period # Used earning.additional_amount to consider the amount for the full month
return future_recurring_additional_amount
def get_amount_based_on_payment_days(self, row, joining_date, relieving_date):
amount, additional_amount = row.amount, row.additional_amount
+ timesheet_component = frappe.db.get_value("Salary Structure", self.salary_structure, "salary_component")
+
if (self.salary_structure and
cint(row.depends_on_payment_days) and cint(self.total_working_days)
and not (row.additional_salary and row.default_amount) # to identify overwritten additional salary
- and (not self.salary_slip_based_on_timesheet or
+ and (row.salary_component != timesheet_component or
getdate(self.start_date) < joining_date or
(relieving_date and getdate(self.end_date) > relieving_date)
)):
@@ -952,7 +957,7 @@
amount = flt((flt(row.default_amount) * flt(self.payment_days)
/ cint(self.total_working_days)), row.precision("amount")) + additional_amount
- elif not self.payment_days and not self.salary_slip_based_on_timesheet and cint(row.depends_on_payment_days):
+ elif not self.payment_days and row.salary_component != timesheet_component and cint(row.depends_on_payment_days):
amount, additional_amount = 0, 0
elif not row.amount:
amount = flt(row.default_amount) + flt(row.additional_amount)
@@ -1030,7 +1035,8 @@
data.update({"annual_taxable_earning": annual_taxable_earning})
tax_amount = 0
for slab in tax_slab.slabs:
- if slab.condition and not self.eval_tax_slab_condition(slab.condition, data):
+ cond = cstr(slab.condition).strip()
+ if cond and not self.eval_tax_slab_condition(cond, data):
continue
if not slab.to_amount and annual_taxable_earning >= slab.from_amount:
tax_amount += (annual_taxable_earning - slab.from_amount + 1) * slab.percent_deduction *.01
@@ -1136,15 +1142,17 @@
})
def make_loan_repayment_entry(self):
+ payroll_payable_account = get_payroll_payable_account(self.company, self.payroll_entry)
for loan in self.loans:
- repayment_entry = create_repayment_entry(loan.loan, self.employee,
- self.company, self.posting_date, loan.loan_type, "Regular Payment", loan.interest_amount,
- loan.principal_amount, loan.total_payment)
+ if loan.total_payment:
+ repayment_entry = create_repayment_entry(loan.loan, self.employee,
+ self.company, self.posting_date, loan.loan_type, "Regular Payment", loan.interest_amount,
+ loan.principal_amount, loan.total_payment, payroll_payable_account=payroll_payable_account)
- repayment_entry.save()
- repayment_entry.submit()
+ repayment_entry.save()
+ repayment_entry.submit()
- frappe.db.set_value("Salary Slip Loan", loan.name, "loan_repayment_entry", repayment_entry.name)
+ frappe.db.set_value("Salary Slip Loan", loan.name, "loan_repayment_entry", repayment_entry.name)
def cancel_loan_repayment_entry(self):
for loan in self.loans:
@@ -1378,3 +1386,11 @@
],
as_dict=1,
)
+
+def get_payroll_payable_account(company, payroll_entry):
+ if payroll_entry:
+ payroll_payable_account = frappe.db.get_value('Payroll Entry', payroll_entry, 'payroll_payable_account')
+ else:
+ payroll_payable_account = frappe.db.get_value('Company', company, 'default_payroll_payable_account')
+
+ return payroll_payable_account
\ No newline at end of file
diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.js b/erpnext/payroll/doctype/salary_slip/test_salary_slip.js
deleted file mode 100644
index a47eba1..0000000
--- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.js
+++ /dev/null
@@ -1,55 +0,0 @@
-QUnit.test("test salary slip", function(assert) {
- assert.expect(6);
- let done = assert.async();
- let employee_name;
-
- let salary_slip = (ename) => {
- frappe.run_serially([
- () => frappe.db.get_value('Employee', {'employee_name': ename}, 'name'),
- (r) => {
- employee_name = r.message.name;
- },
- () => {
- // Creating a salary slip for a employee
- frappe.tests.make('Salary Slip', [
- { employee: employee_name}
- ]);
- },
- () => frappe.timeout(3),
- () => {
- // To check if all the calculations are correctly done
- if(ename === 'Test Employee 1')
- {
- assert.ok(cur_frm.doc.gross_pay==24000,
- 'Gross amount for first employee is correctly calculated');
- assert.ok(cur_frm.doc.total_deduction==4800,
- 'Deduction amount for first employee is correctly calculated');
- assert.ok(cur_frm.doc.net_pay==19200,
- 'Net amount for first employee is correctly calculated');
- }
- if(ename === 'Test Employee 3')
- {
- assert.ok(cur_frm.doc.gross_pay==28800,
- 'Gross amount for second employee is correctly calculated');
- assert.ok(cur_frm.doc.total_deduction==5760,
- 'Deduction amount for second employee is correctly calculated');
- assert.ok(cur_frm.doc.net_pay==23040,
- 'Net amount for second employee is correctly calculated');
- }
- },
- ]);
- };
- frappe.run_serially([
- () => salary_slip('Test Employee 1'),
- () => frappe.timeout(6),
- () => salary_slip('Test Employee 3'),
- () => frappe.timeout(5),
- () => frappe.set_route('List', 'Salary Slip', 'List'),
- () => frappe.timeout(2),
- () => {$('.list-row-checkbox').click();},
- () => frappe.timeout(2),
- () => frappe.click_button('Delete'),
- () => frappe.click_button('Yes'),
- () => done()
- ]);
-});
diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
index e4618c3..c0e005a 100644
--- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
@@ -134,6 +134,58 @@
frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave")
+ def test_payment_days_in_salary_slip_based_on_timesheet(self):
+ from erpnext.hr.doctype.attendance.attendance import mark_attendance
+ from erpnext.projects.doctype.timesheet.test_timesheet import (
+ make_salary_structure_for_timesheet,
+ make_timesheet,
+ )
+ from erpnext.projects.doctype.timesheet.timesheet import (
+ make_salary_slip as make_salary_slip_for_timesheet,
+ )
+
+ # Payroll based on attendance
+ frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Attendance")
+
+ emp = make_employee("test_employee_timesheet@salary.com", company="_Test Company")
+ frappe.db.set_value("Employee", emp, {"relieving_date": None, "status": "Active"})
+
+ # mark attendance
+ month_start_date = get_first_day(nowdate())
+ month_end_date = get_last_day(nowdate())
+
+ first_sunday = frappe.db.sql("""
+ select holiday_date from `tabHoliday`
+ where parent = 'Salary Slip Test Holiday List'
+ and holiday_date between %s and %s
+ order by holiday_date
+ """, (month_start_date, month_end_date))[0][0]
+
+ mark_attendance(emp, add_days(first_sunday, 1), 'Absent', ignore_validate=True) # counted as absent
+
+ # salary structure based on timesheet
+ make_salary_structure_for_timesheet(emp)
+ timesheet = make_timesheet(emp, simulate=True, is_billable=1)
+ salary_slip = make_salary_slip_for_timesheet(timesheet.name)
+ salary_slip.start_date = month_start_date
+ salary_slip.end_date = month_end_date
+ salary_slip.save()
+ salary_slip.submit()
+ salary_slip.reload()
+
+ no_of_days = self.get_no_of_days()
+ days_in_month = no_of_days[0]
+ no_of_holidays = no_of_days[1]
+
+ self.assertEqual(salary_slip.payment_days, days_in_month - no_of_holidays - 1)
+
+ # gross pay calculation based on attendance (payment days)
+ gross_pay = 78100 - ((78000 / (days_in_month - no_of_holidays)) * flt(salary_slip.leave_without_pay + salary_slip.absent_days))
+
+ self.assertEqual(salary_slip.gross_pay, flt(gross_pay, 2))
+
+ frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave")
+
def test_component_amount_dependent_on_another_payment_days_based_component(self):
from erpnext.hr.doctype.attendance.attendance import mark_attendance
from erpnext.payroll.doctype.salary_structure.test_salary_structure import (
@@ -328,7 +380,7 @@
make_salary_structure("Test Loan Repayment Salary Structure", "Monthly", employee=applicant, currency='INR',
payroll_period=payroll_period)
- frappe.db.sql("delete from tabLoan")
+ frappe.db.sql("delete from tabLoan where applicant = 'test_loan_repayment_salary_slip@salary.com'")
loan = create_loan(applicant, "Car Loan", 11000, "Repay Over Number of Periods", 20, posting_date=add_months(nowdate(), -1))
loan.repay_from_salary = 1
loan.submit()
diff --git a/erpnext/payroll/doctype/salary_structure/salary_structure.py b/erpnext/payroll/doctype/salary_structure/salary_structure.py
index ae83c04..4cbf948 100644
--- a/erpnext/payroll/doctype/salary_structure/salary_structure.py
+++ b/erpnext/payroll/doctype/salary_structure/salary_structure.py
@@ -167,15 +167,12 @@
def postprocess(source, target):
if employee:
employee_details = frappe.db.get_value("Employee", employee,
- ["employee_name", "branch", "designation", "department", "payroll_cost_center"], as_dict=1)
+ ["employee_name", "branch", "designation", "department"], as_dict=1)
target.employee = employee
target.employee_name = employee_details.employee_name
target.branch = employee_details.branch
target.designation = employee_details.designation
target.department = employee_details.department
- target.payroll_cost_center = employee_details.payroll_cost_center
- if not target.payroll_cost_center and target.department:
- target.payroll_cost_center = frappe.db.get_value("Department", target.department, "payroll_cost_center")
target.run_method('process_salary_structure', for_preview=for_preview)
diff --git a/erpnext/payroll/doctype/salary_structure/test_salary_structure.js b/erpnext/payroll/doctype/salary_structure/test_salary_structure.js
deleted file mode 100644
index 542fa50..0000000
--- a/erpnext/payroll/doctype/salary_structure/test_salary_structure.js
+++ /dev/null
@@ -1,78 +0,0 @@
-QUnit.test("test Salary Structure", function(assert) {
- assert.expect(7);
- let done = assert.async();
- let employee_name1;
-
- frappe.run_serially([
- () => frappe.db.get_value('Employee', {'employee_name': "Test Employee 1"}, 'name',
- (r) => {
- employee_name1 = r.name;
- }
- ),
- () => frappe.timeout(5),
- () => frappe.db.get_value('Employee', {'employee_name': "Test Employee 3"}, 'name',
- (r) => {
- // Creating Salary Structure for employees);
- return frappe.tests.make('Salary Structure', [
- { __newname: 'Test Salary Structure'},
- { company: 'For Testing'},
- { payroll_frequency: 'Monthly'},
- { employees: [
- [
- {employee: employee_name1},
- {from_date: '2017-07-01'},
- {base: 25000}
- ],
- [
- {employee: r.name},
- {from_date: '2017-07-01'},
- {base: 30000}
- ]
- ]},
- { earnings: [
- [
- {salary_component: 'Basic'},
- {formula: 'base * .80'}
- ],
- [
- {salary_component: 'Leave Encashment'},
- {formula: 'B * .20'}
- ]
- ]},
- { deductions: [
- [
- {salary_component: 'Income Tax'},
- {formula: '(B+LE) * .20'}
- ]
- ]},
- { payment_account: 'CASH - FT'},
- ]);
- }
- ),
- () => frappe.timeout(15),
- () => {
- // To check if all the fields are correctly set
- assert.ok(cur_frm.doc.employees[0].employee_name=='Test Employee 1',
- 'Employee 1 name correctly set');
-
- assert.ok(cur_frm.doc.employees[1].employee_name=='Test Employee 3',
- 'Employee 2 name correctly set');
-
- assert.ok(cur_frm.doc.employees[0].base==25000,
- 'Base value for first employee is correctly set');
-
- assert.ok(cur_frm.doc.employees[1].base==30000,
- 'Base value for second employee is correctly set');
-
- assert.ok(cur_frm.doc.earnings[0].formula.includes('base * .80'),
- 'Formula for earnings as Basic is correctly set');
-
- assert.ok(cur_frm.doc.earnings[1].formula.includes('B * .20'),
- 'Formula for earnings as Leave Encashment is correctly set');
-
- assert.ok(cur_frm.doc.deductions[0].formula.includes('(B+LE) * .20'),
- 'Formula for deductions as Income Tax is correctly set');
- },
- () => done()
- ]);
-});
diff --git a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.js b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.js
index 6cd897e..220bfbf 100644
--- a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.js
+++ b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.js
@@ -40,28 +40,29 @@
}
}
});
+
+ frm.set_query("cost_center", "payroll_cost_centers", function() {
+ return {
+ filters: {
+ "company": frm.doc.company,
+ "is_group": 0
+ }
+ };
+ });
},
employee: function(frm) {
- if(frm.doc.employee){
+ if (frm.doc.employee) {
frappe.call({
- method: "frappe.client.get_value",
- args:{
- doctype: "Employee",
- fieldname: "company",
- filters:{
- name: frm.doc.employee
- }
- },
+ method: "set_payroll_cost_centers",
+ doc: frm.doc,
callback: function(data) {
- if(data.message){
- frm.set_value("company", data.message.company);
- }
+ refresh_field("payroll_cost_centers");
}
});
}
- else{
- frm.set_value("company", null);
+ else {
+ frm.set_value("payroll_cost_centers", []);
}
},
diff --git a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.json b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.json
index c8b98e5..197ab5f 100644
--- a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.json
+++ b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.json
@@ -22,7 +22,9 @@
"base",
"column_break_9",
"variable",
- "amended_from"
+ "amended_from",
+ "section_break_17",
+ "payroll_cost_centers"
],
"fields": [
{
@@ -90,7 +92,8 @@
},
{
"fieldname": "section_break_7",
- "fieldtype": "Section Break"
+ "fieldtype": "Section Break",
+ "label": "Base & Variable"
},
{
"fieldname": "base",
@@ -141,14 +144,29 @@
"fieldtype": "Link",
"label": "Payroll Payable Account",
"options": "Account"
+ },
+ {
+ "collapsible": 1,
+ "depends_on": "employee",
+ "fieldname": "section_break_17",
+ "fieldtype": "Section Break",
+ "label": "Payroll Cost Centers"
+ },
+ {
+ "allow_on_submit": 1,
+ "fieldname": "payroll_cost_centers",
+ "fieldtype": "Table",
+ "label": "Cost Centers",
+ "options": "Employee Cost Center"
}
],
"is_submittable": 1,
"links": [],
- "modified": "2021-03-31 22:44:46.267974",
+ "modified": "2021-12-23 17:28:09.794444",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Salary Structure Assignment",
+ "naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
@@ -193,6 +211,7 @@
],
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"title_field": "employee_name",
"track_changes": 1
}
\ No newline at end of file
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 e1ff9ca..8359478 100644
--- a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py
+++ b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py
@@ -5,7 +5,7 @@
import frappe
from frappe import _
from frappe.model.document import Document
-from frappe.utils import getdate
+from frappe.utils import flt, getdate
class DuplicateAssignment(frappe.ValidationError): pass
@@ -15,6 +15,10 @@
self.validate_dates()
self.validate_income_tax_slab()
self.set_payroll_payable_account()
+ if not self.get("payroll_cost_centers"):
+ self.set_payroll_cost_centers()
+
+ self.validate_cost_center_distribution()
def validate_dates(self):
joining_date, relieving_date = frappe.db.get_value("Employee", self.employee,
@@ -51,6 +55,30 @@
"Company", self.company, "default_currency"), "is_group": 0})
self.payroll_payable_account = payroll_payable_account
+ @frappe.whitelist()
+ def set_payroll_cost_centers(self):
+ self.payroll_cost_centers = []
+ default_payroll_cost_center = self.get_payroll_cost_center()
+ if default_payroll_cost_center:
+ self.append("payroll_cost_centers", {
+ "cost_center": default_payroll_cost_center,
+ "percentage": 100
+ })
+
+ def get_payroll_cost_center(self):
+ payroll_cost_center = frappe.db.get_value("Employee", self.employee, "payroll_cost_center")
+ if not payroll_cost_center and self.department:
+ payroll_cost_center = frappe.db.get_value("Department", self.department, "payroll_cost_center")
+
+ return payroll_cost_center
+
+ def validate_cost_center_distribution(self):
+ if self.get("payroll_cost_centers"):
+ total_percentage = sum([flt(d.percentage) for d in self.get("payroll_cost_centers", [])])
+ if total_percentage != 100:
+ frappe.throw(_("Total percentage against cost centers should be 100"))
+
+
def get_assigned_salary_structure(employee, on_date):
if not employee or not on_date:
return None
@@ -64,6 +92,7 @@
})
return salary_structure[0][0] if salary_structure else None
+
@frappe.whitelist()
def get_employee_currency(employee):
employee_currency = frappe.db.get_value('Salary Structure Assignment', {'employee': employee}, 'currency')
diff --git a/erpnext/projects/doctype/activity_type/test_activity_type.js b/erpnext/projects/doctype/activity_type/test_activity_type.js
deleted file mode 100644
index 62be972..0000000
--- a/erpnext/projects/doctype/activity_type/test_activity_type.js
+++ /dev/null
@@ -1,21 +0,0 @@
-QUnit.test("test: Activity Type", function (assert) {
- // number of asserts
- assert.expect(1);
- let done = assert.async();
-
- frappe.run_serially([
- // insert a new Activity Type
- () => frappe.set_route("List", "Activity Type", "List"),
- () => frappe.new_doc("Activity Type"),
- () => frappe.timeout(1),
- () => frappe.quick_entry.dialog.$wrapper.find('.edit-full').click(),
- () => frappe.timeout(1),
- () => cur_frm.set_value("activity_type", "Test Activity"),
- () => frappe.click_button('Save'),
- () => frappe.timeout(1),
- () => {
- assert.equal(cur_frm.doc.name,"Test Activity");
- },
- () => done()
- ]);
-});
diff --git a/erpnext/projects/doctype/project/project.js b/erpnext/projects/doctype/project/project.js
index 31460f6..4f19bbd 100644
--- a/erpnext/projects/doctype/project/project.js
+++ b/erpnext/projects/doctype/project/project.js
@@ -59,22 +59,16 @@
frm.trigger('show_dashboard');
}
- frm.events.set_buttons(frm);
+ frm.trigger("set_custom_buttons");
},
- set_buttons: function(frm) {
+ set_custom_buttons: function(frm) {
if (!frm.is_new()) {
frm.add_custom_button(__('Duplicate Project with Tasks'), () => {
frm.events.create_duplicate(frm);
- });
+ }, __("Actions"));
- frm.add_custom_button(__('Completed'), () => {
- frm.events.set_status(frm, 'Completed');
- }, __('Set Status'));
-
- frm.add_custom_button(__('Cancelled'), () => {
- frm.events.set_status(frm, 'Cancelled');
- }, __('Set Status'));
+ frm.trigger("set_project_status_button");
if (frappe.model.can_read("Task")) {
@@ -83,7 +77,7 @@
"project": frm.doc.name
};
frappe.set_route("List", "Task", "Gantt");
- });
+ }, __("View"));
frm.add_custom_button(__("Kanban Board"), () => {
frappe.call('erpnext.projects.doctype.project.project.create_kanban_board_if_not_exists', {
@@ -91,13 +85,35 @@
}).then(() => {
frappe.set_route('List', 'Task', 'Kanban', frm.doc.project_name);
});
- });
+ }, __("View"));
}
}
},
+ set_project_status_button: function(frm) {
+ frm.add_custom_button(__('Set Project Status'), () => {
+ let d = new frappe.ui.Dialog({
+ "title": __("Set Project Status"),
+ "fields": [
+ {
+ "fieldname": "status",
+ "fieldtype": "Select",
+ "label": "Status",
+ "reqd": 1,
+ "options": "Completed\nCancelled",
+ },
+ ],
+ primary_action: function() {
+ frm.events.set_status(frm, d.get_values().status);
+ d.hide();
+ },
+ primary_action_label: __("Set Project Status")
+ }).show();
+ }, __("Actions"));
+ },
+
create_duplicate: function(frm) {
return new Promise(resolve => {
frappe.prompt('Project Name', (data) => {
@@ -117,7 +133,9 @@
set_status: function(frm, status) {
frappe.confirm(__('Set Project and all Tasks to status {0}?', [status.bold()]), () => {
frappe.xcall('erpnext.projects.doctype.project.project.set_project_status',
- {project: frm.doc.name, status: status}).then(() => { /* page will auto reload */ });
+ {project: frm.doc.name, status: status}).then(() => {
+ frm.reload_doc();
+ });
});
},
diff --git a/erpnext/projects/doctype/task/task.py b/erpnext/projects/doctype/task/task.py
index 9b1ea04..8fa0538 100755
--- a/erpnext/projects/doctype/task/task.py
+++ b/erpnext/projects/doctype/task/task.py
@@ -102,7 +102,7 @@
frappe.throw(_("Completed On cannot be greater than Today"))
def update_depends_on(self):
- depends_on_tasks = self.depends_on_tasks or ""
+ depends_on_tasks = ""
for d in self.depends_on:
if d.task and d.task not in depends_on_tasks:
depends_on_tasks += d.task + ","
diff --git a/erpnext/projects/doctype/task/test_task.py b/erpnext/projects/doctype/task/test_task.py
index a0ac7c1..5f5b519 100644
--- a/erpnext/projects/doctype/task/test_task.py
+++ b/erpnext/projects/doctype/task/test_task.py
@@ -78,11 +78,11 @@
return frappe.db.get_value("ToDo",
filters={"reference_type": task.doctype, "reference_name": task.name,
"description": "Close this task"},
- fieldname=("owner", "status"), as_dict=True)
+ fieldname=("allocated_to", "status"), as_dict=True)
assign()
todo = get_owner_and_status()
- self.assertEqual(todo.owner, "test@example.com")
+ self.assertEqual(todo.allocated_to, "test@example.com")
self.assertEqual(todo.status, "Open")
# assignment should be
@@ -90,7 +90,7 @@
task.status = "Completed"
task.save()
todo = get_owner_and_status()
- self.assertEqual(todo.owner, "test@example.com")
+ self.assertEqual(todo.allocated_to, "test@example.com")
self.assertEqual(todo.status, "Closed")
def test_overdue(self):
diff --git a/erpnext/projects/doctype/task/tests/test_task.js b/erpnext/projects/doctype/task/tests/test_task.js
deleted file mode 100644
index 8a1a5bf..0000000
--- a/erpnext/projects/doctype/task/tests/test_task.js
+++ /dev/null
@@ -1,24 +0,0 @@
-/* eslint-disable */
-// rename this file from _test_[name] to test_[name] to activate
-// and remove above this line
-
-QUnit.test("test: Task", function (assert) {
- let done = assert.async();
-
- // number of asserts
- assert.expect(2);
-
- frappe.run_serially([
- // insert a new Task
- () => frappe.tests.make('Task', [
- // values to be set
- {subject: 'new task'}
- ]),
- () => {
- assert.equal(cur_frm.doc.status, 'Open');
- assert.equal(cur_frm.doc.priority, 'Low');
- },
- () => done()
- ]);
-
-});
diff --git a/erpnext/projects/doctype/task/tests/test_task_tree.js b/erpnext/projects/doctype/task/tests/test_task_tree.js
deleted file mode 100644
index 27dccbf..0000000
--- a/erpnext/projects/doctype/task/tests/test_task_tree.js
+++ /dev/null
@@ -1,88 +0,0 @@
-/* eslint-disable */
-// rename this file from _test_[name] to test_[name] to activate
-// and remove above this line
-
-QUnit.test("test: Task Tree", function (assert) {
- let done = assert.async();
-
- // number of asserts
- assert.expect(4);
-
- frappe.run_serially([
- // insert a new Task
- () => frappe.set_route('Tree', 'Task'),
- () => frappe.timeout(0.5),
-
- // Checking adding child without selecting any Node
- () => frappe.tests.click_button('New'),
- () => frappe.timeout(0.5),
- () => {assert.equal($(`.msgprint`).text(), "Select a group node first.", "Error message success");},
- () => frappe.tests.click_button('Close'),
- () => frappe.timeout(0.5),
-
- // Creating child nodes
- () => frappe.tests.click_link('All Tasks'),
- () => frappe.map_group.make('Test-1'),
- () => frappe.map_group.make('Test-3', 1),
- () => frappe.timeout(1),
- () => frappe.tests.click_link('Test-3'),
- () => frappe.map_group.make('Test-4', 0),
-
- // Checking Edit button
- () => frappe.timeout(0.5),
- () => frappe.tests.click_link('Test-1'),
- () => frappe.tests.click_button('Edit'),
- () => frappe.timeout(1),
- () => frappe.db.get_value('Task', {'subject': 'Test-1'}, 'name'),
- (task) => {assert.deepEqual(frappe.get_route(), ["Form", "Task", task.message.name], "Edit route checks");},
-
- // Deleting child Node
- () => frappe.set_route('Tree', 'Task'),
- () => frappe.timeout(0.5),
- () => frappe.tests.click_link('Test-1'),
- () => frappe.tests.click_button('Delete'),
- () => frappe.timeout(0.5),
- () => frappe.tests.click_button('Yes'),
-
- // Deleting Group Node that has child nodes in it
- () => frappe.timeout(0.5),
- () => frappe.tests.click_link('Test-3'),
- () => frappe.tests.click_button('Delete'),
- () => frappe.timeout(0.5),
- () => frappe.tests.click_button('Yes'),
- () => frappe.timeout(1),
- () => {assert.equal(cur_dialog.title, 'Message', 'Error thrown correctly');},
- () => frappe.tests.click_button('Close'),
-
- // Add multiple child tasks
- () => frappe.tests.click_link('Test-3'),
- () => frappe.timeout(0.5),
- () => frappe.click_button('Add Multiple'),
- () => frappe.timeout(1),
- () => cur_dialog.set_value('tasks', 'Test-6\nTest-7'),
- () => frappe.timeout(0.5),
- () => frappe.click_button('Submit'),
- () => frappe.timeout(2),
- () => frappe.click_button('Expand All'),
- () => frappe.timeout(1),
- () => {
- let count = $(`a:contains("Test-6"):visible`).length + $(`a:contains("Test-7"):visible`).length;
- assert.equal(count, 2, "Multiple Tasks added successfully");
- },
-
- () => done()
- ]);
-});
-
-frappe.map_group = {
- make:function(subject, is_group = 0){
- return frappe.run_serially([
- () => frappe.click_button('Add Child'),
- () => frappe.timeout(1),
- () => cur_dialog.set_value('is_group', is_group),
- () => cur_dialog.set_value('subject', subject),
- () => frappe.click_button('Create New'),
- () => frappe.timeout(1.5)
- ]);
- }
-};
diff --git a/erpnext/projects/doctype/timesheet/test_timesheet.py b/erpnext/projects/doctype/timesheet/test_timesheet.py
index d59cc01..148d8ba 100644
--- a/erpnext/projects/doctype/timesheet/test_timesheet.py
+++ b/erpnext/projects/doctype/timesheet/test_timesheet.py
@@ -34,10 +34,6 @@
for dt in ["Salary Slip", "Salary Structure", "Salary Structure Assignment", "Timesheet"]:
frappe.db.sql("delete from `tab%s`" % dt)
- if not frappe.db.exists("Salary Component", "Timesheet Component"):
- frappe.get_doc({"doctype": "Salary Component", "salary_component": "Timesheet Component"}).insert()
-
-
def test_timesheet_billing_amount(self):
emp = make_employee("test_employee_6@salary.com")
@@ -160,6 +156,9 @@
salary_structure_name = "Timesheet Salary Structure Test"
frequency = "Monthly"
+ if not frappe.db.exists("Salary Component", "Timesheet Component"):
+ frappe.get_doc({"doctype": "Salary Component", "salary_component": "Timesheet Component"}).insert()
+
salary_structure = make_salary_structure(salary_structure_name, frequency, company=company, dont_submit=True)
salary_structure.salary_component = "Timesheet Component"
salary_structure.salary_slip_based_on_timesheet = 1
diff --git a/erpnext/projects/report/project_profitability/test_project_profitability.py b/erpnext/projects/report/project_profitability/test_project_profitability.py
index 2cf9d38..1eb3d0d 100644
--- a/erpnext/projects/report/project_profitability/test_project_profitability.py
+++ b/erpnext/projects/report/project_profitability/test_project_profitability.py
@@ -1,7 +1,7 @@
import unittest
import frappe
-from frappe.utils import add_days, getdate, nowdate
+from frappe.utils import add_days, getdate
from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.projects.doctype.timesheet.test_timesheet import (
@@ -13,21 +13,27 @@
class TestProjectProfitability(unittest.TestCase):
-
def setUp(self):
+ frappe.db.sql('delete from `tabTimesheet`')
emp = make_employee('test_employee_9@salary.com', company='_Test Company')
+
if not frappe.db.exists('Salary Component', 'Timesheet Component'):
frappe.get_doc({'doctype': 'Salary Component', 'salary_component': 'Timesheet Component'}).insert()
+
make_salary_structure_for_timesheet(emp, company='_Test Company')
- self.timesheet = make_timesheet(emp, simulate = True, is_billable=1)
+ date = getdate()
+
+ self.timesheet = make_timesheet(emp, is_billable=1)
self.salary_slip = make_salary_slip(self.timesheet.name)
- holidays = self.salary_slip.get_holidays_for_employee(nowdate(), nowdate())
+ self.salary_slip.start_date = self.timesheet.start_date
+
+ holidays = self.salary_slip.get_holidays_for_employee(date, date)
if holidays:
frappe.db.set_value('Payroll Settings', None, 'include_holidays_in_total_working_days', 1)
self.salary_slip.submit()
self.sales_invoice = make_sales_invoice(self.timesheet.name, '_Test Item', '_Test Customer')
- self.sales_invoice.due_date = nowdate()
+ self.sales_invoice.due_date = date
self.sales_invoice.submit()
frappe.db.set_value('HR Settings', None, 'standard_working_hours', 8)
@@ -36,8 +42,8 @@
def test_project_profitability(self):
filters = {
'company': '_Test Company',
- 'start_date': add_days(getdate(), -3),
- 'end_date': getdate()
+ 'start_date': add_days(self.timesheet.start_date, -3),
+ 'end_date': self.timesheet.start_date
}
report = execute(filters)
@@ -63,6 +69,4 @@
self.assertEqual(fractional_cost, row.fractional_cost)
def tearDown(self):
- frappe.get_doc("Sales Invoice", self.sales_invoice.name).cancel()
- frappe.get_doc("Salary Slip", self.salary_slip.name).cancel()
- frappe.get_doc("Timesheet", self.timesheet.name).cancel()
+ frappe.db.rollback()
diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js
index 86dadd3..d696ef5 100644
--- a/erpnext/public/js/controllers/buying.js
+++ b/erpnext/public/js/controllers/buying.js
@@ -165,45 +165,33 @@
}
qty(doc, cdt, cdn) {
- var item = frappe.get_doc(cdt, cdn);
if ((doc.doctype == "Purchase Receipt") || (doc.doctype == "Purchase Invoice" && (doc.update_stock || doc.is_return))) {
- frappe.model.round_floats_in(item, ["qty", "received_qty"]);
-
- if(!doc.is_return && this.validate_negative_quantity(cdt, cdn, item, ["qty", "received_qty"])){ return }
-
- if(!item.rejected_qty && item.qty) {
- item.received_qty = item.qty;
- }
-
- frappe.model.round_floats_in(item, ["qty", "received_qty"]);
- item.rejected_qty = flt(item.received_qty - item.qty, precision("rejected_qty", item));
- item.received_stock_qty = flt(item.conversion_factor, precision("conversion_factor", item)) * flt(item.received_qty);
+ this.calculate_received_qty(doc, cdt, cdn)
}
super.qty(doc, cdt, cdn);
}
+ rejected_qty(doc, cdt, cdn) {
+ this.calculate_received_qty(doc, cdt, cdn)
+ }
+
+ calculate_received_qty(doc, cdt, cdn){
+ var item = frappe.get_doc(cdt, cdn);
+ frappe.model.round_floats_in(item, ["qty", "rejected_qty"]);
+
+ if(!doc.is_return && this.validate_negative_quantity(cdt, cdn, item, ["qty", "rejected_qty"])){ return }
+
+ let received_qty = flt(item.qty + item.rejected_qty, precision("received_qty", item));
+ let received_stock_qty = flt(item.conversion_factor, precision("conversion_factor", item)) * flt(received_qty);
+
+ frappe.model.set_value(cdt, cdn, "received_qty", received_qty);
+ frappe.model.set_value(cdt, cdn, "received_stock_qty", received_stock_qty);
+ }
+
batch_no(doc, cdt, cdn) {
super.batch_no(doc, cdt, cdn);
}
- received_qty(doc, cdt, cdn) {
- this.calculate_accepted_qty(doc, cdt, cdn)
- }
-
- rejected_qty(doc, cdt, cdn) {
- this.calculate_accepted_qty(doc, cdt, cdn)
- }
-
- calculate_accepted_qty(doc, cdt, cdn){
- var item = frappe.get_doc(cdt, cdn);
- frappe.model.round_floats_in(item, ["received_qty", "rejected_qty"]);
-
- if(!doc.is_return && this.validate_negative_quantity(cdt, cdn, item, ["received_qty", "rejected_qty"])){ return }
-
- item.qty = flt(item.received_qty - item.rejected_qty, precision("qty", item));
- this.qty(doc, cdt, cdn);
- }
-
validate_negative_quantity(cdt, cdn, item, fieldnames){
if(!item || !fieldnames) { return }
diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js
index b5a6d8f..ae0e2a3 100644
--- a/erpnext/public/js/controllers/taxes_and_totals.js
+++ b/erpnext/public/js/controllers/taxes_and_totals.js
@@ -81,6 +81,7 @@
this.initialize_taxes();
this.determine_exclusive_rate();
this.calculate_net_total();
+ this.calculate_shipping_charges();
this.calculate_taxes();
this.manipulate_grand_total_for_inclusive_tax();
this.calculate_totals();
@@ -113,6 +114,8 @@
if ((!item.qty) && me.frm.doc.is_return) {
item.amount = flt(item.rate * -1, precision("amount", item));
+ } else if ((!item.qty) && me.frm.doc.is_debit_note) {
+ item.amount = flt(item.rate, precision("amount", item));
} else {
item.amount = flt(item.rate * item.qty, precision("amount", item));
}
@@ -266,8 +269,13 @@
me.frm.doc.net_total += item.net_amount;
me.frm.doc.base_net_total += item.base_net_amount;
});
+ }
+ calculate_shipping_charges() {
frappe.model.round_floats_in(this.frm.doc, ["total", "base_total", "net_total", "base_net_total"]);
+ if (frappe.meta.get_docfield(this.frm.doc.doctype, "shipping_rule", this.frm.doc.name)) {
+ this.shipping_rule();
+ }
}
add_taxes_from_item_tax_template(item_tax_map) {
@@ -704,14 +712,15 @@
frappe.model.round_floats_in(this.frm.doc, ["grand_total", "total_advance", "write_off_amount"]);
if(in_list(["Sales Invoice", "POS Invoice", "Purchase Invoice"], this.frm.doc.doctype)) {
- var grand_total = this.frm.doc.rounded_total || this.frm.doc.grand_total;
+ let grand_total = this.frm.doc.rounded_total || this.frm.doc.grand_total;
+ let base_grand_total = this.frm.doc.base_rounded_total || this.frm.doc.base_grand_total;
if(this.frm.doc.party_account_currency == this.frm.doc.currency) {
var total_amount_to_pay = flt((grand_total - this.frm.doc.total_advance
- this.frm.doc.write_off_amount), precision("grand_total"));
} else {
var total_amount_to_pay = flt(
- (flt(grand_total*this.frm.doc.conversion_rate, precision("grand_total"))
+ (flt(base_grand_total, precision("base_grand_total"))
- this.frm.doc.total_advance - this.frm.doc.base_write_off_amount),
precision("base_grand_total")
);
@@ -742,14 +751,15 @@
}
set_total_amount_to_default_mop() {
- var grand_total = this.frm.doc.rounded_total || this.frm.doc.grand_total;
+ let grand_total = this.frm.doc.rounded_total || this.frm.doc.grand_total;
+ let base_grand_total = this.frm.doc.base_rounded_total || this.frm.doc.base_grand_total;
if(this.frm.doc.party_account_currency == this.frm.doc.currency) {
var total_amount_to_pay = flt((grand_total - this.frm.doc.total_advance
- this.frm.doc.write_off_amount), precision("grand_total"));
} else {
var total_amount_to_pay = flt(
- (flt(grand_total*this.frm.doc.conversion_rate, precision("grand_total"))
+ (flt(base_grand_total, precision("base_grand_total"))
- this.frm.doc.total_advance - this.frm.doc.base_write_off_amount),
precision("base_grand_total")
);
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 63fd8a1..3791741 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -680,7 +680,7 @@
var item = frappe.get_doc(cdt, cdn);
frappe.model.round_floats_in(item, ["price_list_rate", "discount_percentage"]);
- // check if child doctype is Sales Order Item/Qutation Item and calculate the rate
+ // check if child doctype is Sales Order Item/Quotation Item and calculate the rate
if (in_list(["Quotation Item", "Sales Order Item", "Delivery Note Item", "Sales Invoice Item", "POS Invoice Item", "Purchase Invoice Item", "Purchase Order Item", "Purchase Receipt Item"]), cdt)
this.apply_pricing_rule_on_item(item);
else
@@ -1085,16 +1085,8 @@
return this.frm.call({
doc: this.frm.doc,
method: "apply_shipping_rule",
- callback: function(r) {
- if(!r.exc) {
- me.calculate_taxes_and_totals();
- }
- }
}).fail(() => this.frm.set_value('shipping_rule', ''));
}
- else {
- me.calculate_taxes_and_totals();
- }
}
set_margin_amount_based_on_currency(exchange_rate) {
@@ -1114,7 +1106,7 @@
$.each(this.frm.doc.taxes || [], function(i, d) {
if(d.charge_type == "Actual") {
frappe.model.set_value(d.doctype, d.name, "tax_amount",
- flt(d.tax_amount) / flt(exchange_rate));
+ flt(d.base_tax_amount) / flt(exchange_rate));
}
});
}
@@ -1590,25 +1582,27 @@
_set_values_for_item_list(children) {
var me = this;
- var price_list_rate_changed = false;
var items_rule_dict = {};
for(var i=0, l=children.length; i<l; i++) {
- var d = children[i];
+ var d = children[i] ;
+ let item_row = frappe.get_doc(d.doctype, d.name);
var existing_pricing_rule = frappe.model.get_value(d.doctype, d.name, "pricing_rules");
for(var k in d) {
var v = d[k];
if (["doctype", "name"].indexOf(k)===-1) {
if(k=="price_list_rate") {
- if(flt(v) != flt(d.price_list_rate)) price_list_rate_changed = true;
+ item_row['rate'] = v;
}
if (k !== 'free_item_data') {
- frappe.model.set_value(d.doctype, d.name, k, v);
+ item_row[k] = v;
}
}
}
+ frappe.model.round_floats_in(item_row, ["price_list_rate", "discount_percentage"]);
+
// if pricing rule set as blank from an existing value, apply price_list
if(!me.frm.doc.ignore_pricing_rule && existing_pricing_rule && !d.pricing_rules) {
me.apply_price_list(frappe.get_doc(d.doctype, d.name));
@@ -1625,9 +1619,10 @@
}
}
+ me.frm.refresh_field('items');
me.apply_rule_on_other_items(items_rule_dict);
- if(!price_list_rate_changed) me.calculate_taxes_and_totals();
+ me.calculate_taxes_and_totals();
}
apply_rule_on_other_items(args) {
diff --git a/erpnext/public/js/queries.js b/erpnext/public/js/queries.js
index b635adc..b7d880a 100644
--- a/erpnext/public/js/queries.js
+++ b/erpnext/public/js/queries.js
@@ -83,6 +83,13 @@
};
},
+ dispatch_address_query: function(doc) {
+ return {
+ query: 'frappe.contacts.doctype.address.address.address_query',
+ filters: { link_doctype: 'Company', link_name: doc.company || '' }
+ };
+ },
+
supplier_filter: function(doc) {
if(!doc.supplier) {
frappe.throw(__("Please set {0}", [__(frappe.meta.get_label(doc.doctype, "supplier", doc.name))]));
diff --git a/erpnext/public/js/setup_wizard.js b/erpnext/public/js/setup_wizard.js
index 38e1eb5..e746ce9 100644
--- a/erpnext/public/js/setup_wizard.js
+++ b/erpnext/public/js/setup_wizard.js
@@ -27,7 +27,6 @@
{ "label": __("Manufacturing"), "value": "Manufacturing" },
{ "label": __("Retail"), "value": "Retail" },
{ "label": __("Services"), "value": "Services" },
- { "label": __("Agriculture (beta)"), "value": "Agriculture" },
{ "label": __("Healthcare (beta)"), "value": "Healthcare" },
{ "label": __("Non Profit (beta)"), "value": "Non Profit" }
], reqd: 1
diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js
index 0323a42..9339c5d 100755
--- a/erpnext/public/js/utils.js
+++ b/erpnext/public/js/utils.js
@@ -84,6 +84,10 @@
});
},
+ route_to_pending_reposts: (args) => {
+ frappe.set_route('List', 'Repost Item Valuation', args);
+ },
+
proceed_save_with_reminders_frequency_change: () => {
frappe.ui.hide_open_dialog();
@@ -426,12 +430,9 @@
qty = row.qty;
}
row[item_field] = d.alternate_item;
- frm.script_manager.trigger(item_field, row.doctype, row.name)
- .then(() => {
- frappe.model.set_value(row.doctype, row.name, 'qty', qty);
- frappe.model.set_value(row.doctype, row.name,
- opts.original_item_field, d.item_code);
- });
+ frappe.model.set_value(row.doctype, row.name, 'qty', qty);
+ frappe.model.set_value(row.doctype, row.name, opts.original_item_field, d.item_code);
+ frm.trigger(item_field, row.doctype, row.name);
});
refresh_field(opts.child_docname);
@@ -751,9 +752,13 @@
}
frappe.form.link_formatters['Employee'] = function(value, doc) {
- if(doc && doc.employee_name && doc.employee_name !== value) {
- return value? value + ': ' + doc.employee_name: doc.employee_name;
+ if (doc && value && doc.employee_name && doc.employee_name !== value && doc.employee === value) {
+ return value + ': ' + doc.employee_name;
+ } else if (!value && doc.doctype && doc.employee_name) {
+ // format blank value in child table
+ return doc.employee;
} else {
+ // if value is blank in report view or project name and name are the same, return as is
return value;
}
}
@@ -827,7 +832,7 @@
refresh: function(frm) {
if (frm.doc.status !== 'Closed' && frm.doc.service_level_agreement
- && frm.doc.agreement_status === 'Ongoing') {
+ && ['First Response Due', 'Resolution Due'].includes(frm.doc.agreement_status)) {
frappe.call({
'method': 'frappe.client.get',
args: {
@@ -880,9 +885,11 @@
function set_time_to_resolve_and_response(frm, apply_sla_for_resolution) {
frm.dashboard.clear_headline();
- let time_to_respond = get_status(frm.doc.response_by_variance);
- if (!frm.doc.first_responded_on && frm.doc.agreement_status === 'Ongoing') {
+ let time_to_respond;
+ if (!frm.doc.first_responded_on) {
time_to_respond = get_time_left(frm.doc.response_by, frm.doc.agreement_status);
+ } else {
+ time_to_respond = get_status(frm.doc.response_by, frm.doc.first_responded_on);
}
let alert = `
@@ -895,9 +902,11 @@
if (apply_sla_for_resolution) {
- let time_to_resolve = get_status(frm.doc.resolution_by_variance);
- if (!frm.doc.resolution_date && frm.doc.agreement_status === 'Ongoing') {
+ let time_to_resolve;
+ if (!frm.doc.resolution_date) {
time_to_resolve = get_time_left(frm.doc.resolution_by, frm.doc.agreement_status);
+ } else {
+ time_to_resolve = get_status(frm.doc.resolution_by, frm.doc.resolution_date);
}
alert += `
@@ -920,8 +929,9 @@
return {'diff_display': diff_display, 'indicator': indicator};
}
-function get_status(variance) {
- if (variance > 0) {
+function get_status(expected, actual) {
+ const time_left = moment(expected).diff(moment(actual));
+ if (time_left >= 0) {
return {'diff_display': 'Fulfilled', 'indicator': 'green'};
} else {
return {'diff_display': 'Failed', 'indicator': 'red'};
diff --git a/erpnext/public/scss/point-of-sale.scss b/erpnext/public/scss/point-of-sale.scss
index 1677e9b..7a3854c 100644
--- a/erpnext/public/scss/point-of-sale.scss
+++ b/erpnext/public/scss/point-of-sale.scss
@@ -495,6 +495,11 @@
font-size: var(--text-md);
}
+ > .item-qty-total-container {
+ @extend .net-total-container;
+ padding: 5px 0px 0px 0px;
+ }
+
> .taxes-container {
display: none;
flex-direction: column;
diff --git a/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py b/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py
index 0f0c0b9..e12e3d7 100644
--- a/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py
+++ b/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py
@@ -103,6 +103,45 @@
gst_settings.round_off_gst_values = 1
gst_settings.save()
+ def test_gst_category_auto_update(self):
+ if not frappe.db.exists("Customer", "_Test GST Customer With GSTIN"):
+ customer = frappe.get_doc({
+ "customer_group": "_Test Customer Group",
+ "customer_name": "_Test GST Customer With GSTIN",
+ "customer_type": "Individual",
+ "doctype": "Customer",
+ "territory": "_Test Territory"
+ }).insert()
+
+ self.assertEqual(customer.gst_category, 'Unregistered')
+
+ if not frappe.db.exists('Address', '_Test GST Category-1-Billing'):
+ address = frappe.get_doc({
+ "address_line1": "_Test Address Line 1",
+ "address_title": "_Test GST Category-1",
+ "address_type": "Billing",
+ "city": "_Test City",
+ "state": "Test State",
+ "country": "India",
+ "doctype": "Address",
+ "is_primary_address": 1,
+ "phone": "+91 0000000000",
+ "gstin": "29AZWPS7135H1ZG",
+ "gst_state": "Karnataka",
+ "gst_state_number": "29"
+ }).insert()
+
+ address.append("links", {
+ "link_doctype": "Customer",
+ "link_name": "_Test GST Customer With GSTIN"
+ })
+
+ address.save()
+
+ customer.load_from_db()
+ self.assertEqual(customer.gst_category, 'Registered Regular')
+
+
def make_sales_invoice():
si = create_sales_invoice(company="_Test Company GST",
customer = '_Test GST Customer',
diff --git a/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.py b/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.py
index 9a72410..0f08978 100644
--- a/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.py
+++ b/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.py
@@ -82,7 +82,6 @@
memberships = frappe.db.get_all('Membership', {
'member': self.member,
'from_date': ['between', (fiscal_year.year_start_date, fiscal_year.year_end_date)],
- 'to_date': ['between', (fiscal_year.year_start_date, fiscal_year.year_end_date)],
'membership_status': ('!=', 'Cancelled')
}, ['from_date', 'amount', 'name', 'invoice', 'payment_id'], order_by='from_date')
diff --git a/erpnext/regional/doctype/uae_vat_settings/uae_vat_settings.js b/erpnext/regional/doctype/uae_vat_settings/uae_vat_settings.js
index 07a9301..6653141 100644
--- a/erpnext/regional/doctype/uae_vat_settings/uae_vat_settings.js
+++ b/erpnext/regional/doctype/uae_vat_settings/uae_vat_settings.js
@@ -2,7 +2,13 @@
// For license information, please see license.txt
frappe.ui.form.on('UAE VAT Settings', {
- // refresh: function(frm) {
-
- // }
+ onload: function(frm) {
+ frm.set_query('account', 'uae_vat_accounts', function() {
+ return {
+ filters: {
+ 'company': frm.doc.company
+ }
+ };
+ });
+ }
});
diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py
index 316bb6b..c0dcb70 100644
--- a/erpnext/regional/india/setup.py
+++ b/erpnext/regional/india/setup.py
@@ -277,8 +277,10 @@
inter_state_gst_field = [
dict(fieldname='is_inter_state', label='Is Inter State',
fieldtype='Check', insert_after='disabled', print_hide=1),
+ dict(fieldname='is_reverse_charge', label='Is Reverse Charge', fieldtype='Check',
+ insert_after='is_inter_state', print_hide=1),
dict(fieldname='tax_category_column_break', fieldtype='Column Break',
- insert_after='is_inter_state'),
+ insert_after='is_reverse_charge'),
dict(fieldname='gst_state', label='Source State', fieldtype='Select',
options='\n'.join(states), insert_after='company')
]
@@ -791,7 +793,7 @@
accounts = [dict(company=company, account=tds_account)]
try:
- fiscal_year_details = get_fiscal_year(today(), verbose=0, company=company)
+ fiscal_year_details = get_fiscal_year(today(), verbose=0)
except FiscalYearError:
pass
diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py
index 54d592a..d443f9c 100644
--- a/erpnext/regional/india/utils.py
+++ b/erpnext/regional/india/utils.py
@@ -67,18 +67,19 @@
frappe.throw(_("Invalid PAN No. The input you've entered doesn't match the format of PAN."))
def validate_tax_category(doc, method):
- if doc.get('gst_state') and frappe.db.get_value('Tax Category', {'gst_state': doc.gst_state, 'is_inter_state': doc.is_inter_state}):
+ if doc.get('gst_state') and frappe.db.get_value('Tax Category', {'gst_state': doc.gst_state, 'is_inter_state': doc.is_inter_state,
+ 'is_reverse_charge': doc.is_reverse_charge}):
if doc.is_inter_state:
frappe.throw(_("Inter State tax category for GST State {0} already exists").format(doc.gst_state))
else:
frappe.throw(_("Intra State tax category for GST State {0} already exists").format(doc.gst_state))
def update_gst_category(doc, method):
- if hasattr(doc, 'gst_category'):
- for link in doc.links:
- if link.link_doctype in ['Customer', 'Supplier']:
- if doc.get('gstin'):
- frappe.db.set_value(link.link_doctype, {'name': link.link_name, 'gst_category': 'Unregistered'}, 'gst_category', 'Registered Regular')
+ for link in doc.links:
+ if link.link_doctype in ['Customer', 'Supplier']:
+ meta = frappe.get_meta(link.link_doctype)
+ if doc.get('gstin') and meta.has_field('gst_category'):
+ frappe.db.set_value(link.link_doctype, {'name': link.link_name, 'gst_category': 'Unregistered'}, 'gst_category', 'Registered Regular')
def set_gst_state_and_state_number(doc):
if not doc.gst_state:
@@ -206,26 +207,17 @@
if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"):
master_doctype = "Sales Taxes and Charges Template"
- get_tax_template_based_on_category(master_doctype, company, party_details)
-
- if party_details.get('taxes_and_charges'):
- return party_details
-
- if not party_details.company_gstin:
- return party_details
+ tax_template_by_category = get_tax_template_based_on_category(master_doctype, company, party_details)
elif doctype in ("Purchase Invoice", "Purchase Order", "Purchase Receipt"):
master_doctype = "Purchase Taxes and Charges Template"
- get_tax_template_based_on_category(master_doctype, company, party_details)
+ tax_template_by_category = get_tax_template_based_on_category(master_doctype, company, party_details)
- if party_details.get('taxes_and_charges'):
- return party_details
-
- if not party_details.supplier_gstin:
- return party_details
+ if tax_template_by_category:
+ party_details['taxes_and_charges'] = tax_template_by_category
+ return party_details
if not party_details.place_of_supply: return party_details
-
if not party_details.company_gstin: return party_details
if ((doctype in ("Sales Invoice", "Delivery Note", "Sales Order") and party_details.company_gstin
@@ -237,6 +229,7 @@
if not default_tax:
return party_details
+
party_details["taxes_and_charges"] = default_tax
party_details.taxes = get_taxes_and_charges(master_doctype, default_tax)
@@ -268,13 +261,11 @@
default_tax = frappe.db.get_value(master_doctype, {'company': company, 'tax_category': party_details.get('tax_category')},
'name')
- if default_tax:
- party_details["taxes_and_charges"] = default_tax
- party_details.taxes = get_taxes_and_charges(master_doctype, default_tax)
+ return default_tax
def get_tax_template(master_doctype, company, is_inter_state, state_code):
tax_categories = frappe.get_all('Tax Category', fields = ['name', 'is_inter_state', 'gst_state'],
- filters = {'is_inter_state': is_inter_state})
+ filters = {'is_inter_state': is_inter_state, 'is_reverse_charge': 0})
default_tax = ''
@@ -569,17 +560,17 @@
}
item_data_attrs = ['sgstRate', 'cgstRate', 'igstRate', 'cessRate', 'cessNonAdvol']
hsn_wise_charges, hsn_taxable_amount = get_itemised_tax_breakup_data(doc, account_wise=True, hsn_wise=hsn_wise)
- for hsn_code, taxable_amount in hsn_taxable_amount.items():
+ for item_or_hsn, taxable_amount in hsn_taxable_amount.items():
item_data = frappe._dict()
- if not hsn_code:
+ if not item_or_hsn:
frappe.throw(_('GST HSN Code does not exist for one or more items'))
- item_data.hsnCode = int(hsn_code)
+ item_data.hsnCode = int(item_or_hsn) if hsn_wise else item_or_hsn
item_data.taxableAmount = taxable_amount
item_data.qtyUnit = ""
for attr in item_data_attrs:
item_data[attr] = 0
- for account, tax_detail in hsn_wise_charges.get(hsn_code, {}).items():
+ for account, tax_detail in hsn_wise_charges.get(item_or_hsn, {}).items():
account_type = gst_accounts.get(account, '')
for tax_acc, attrs in tax_map.items():
if account_type == tax_acc:
@@ -847,13 +838,11 @@
doc.get('items')[item_count - 1].taxable_value += diff
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"):
# if the Depreciation Schedule is being prepared for the first time
if not asset.flags.increase_in_asset_life:
- depreciation_amount = (flt(asset.gross_purchase_amount) - flt(asset.opening_accumulated_depreciation) -
- flt(row.expected_value_after_useful_life)) / depreciation_left
+ depreciation_amount = (flt(asset.gross_purchase_amount) -
+ flt(row.expected_value_after_useful_life)) / flt(row.total_number_of_depreciations)
# if the Depreciation Schedule is being modified after Asset Repair
else:
diff --git a/erpnext/agriculture/__init__.py b/erpnext/regional/print_format/ksa_pos_invoice/__init__.py
similarity index 100%
copy from erpnext/agriculture/__init__.py
copy to erpnext/regional/print_format/ksa_pos_invoice/__init__.py
diff --git a/erpnext/regional/print_format/ksa_pos_invoice/ksa_pos_invoice.json b/erpnext/regional/print_format/ksa_pos_invoice/ksa_pos_invoice.json
new file mode 100644
index 0000000..c2a3092
--- /dev/null
+++ b/erpnext/regional/print_format/ksa_pos_invoice/ksa_pos_invoice.json
@@ -0,0 +1,32 @@
+{
+ "absolute_value": 0,
+ "align_labels_right": 0,
+ "creation": "2021-12-07 13:25:05.424827",
+ "css": "",
+ "custom_format": 1,
+ "default_print_language": "en",
+ "disabled": 1,
+ "doc_type": "POS Invoice",
+ "docstatus": 0,
+ "doctype": "Print Format",
+ "font_size": 0,
+ "html": "<style>\n\t.print-format table, .print-format tr, \n\t.print-format td, .print-format div, .print-format p {\n\t\tline-height: 150%;\n\t\tvertical-align: middle;\n\t}\n\t@media screen {\n\t\t.print-format {\n\t\t\twidth: 4in;\n\t\t\tpadding: 0.25in;\n\t\t\tmin-height: 8in;\n\t\t}\n\t}\n</style>\n\n{% if letter_head %}\n {{ letter_head }}\n{% endif %}\n\n<p class=\"text-center\" style=\"margin-bottom: 1rem\">\n\t{{ doc.company }}<br>\n\t<b>{{ doc.select_print_heading or _(\"Invoice\") }}</b><br>\n\t<img src={{doc.ksa_einv_qr}}>\n</p>\n<p>\n\t<b>{{ _(\"Receipt No\") }}:</b> {{ doc.name }}<br>\n\t<b>{{ _(\"Cashier\") }}:</b> {{ doc.owner }}<br>\n\t<b>{{ _(\"Customer\") }}:</b> {{ doc.customer_name }}<br>\n\t<b>{{ _(\"Date\") }}:</b> {{ doc.get_formatted(\"posting_date\") }}<br>\n\t<b>{{ _(\"Time\") }}:</b> {{ doc.get_formatted(\"posting_time\") }}<br>\n</p>\n\n<hr>\n<table class=\"table table-condensed\">\n\t<thead>\n\t\t<tr>\n\t\t\t<th width=\"40%\">{{ _(\"Item\") }}</th>\n\t\t\t<th width=\"25%\" class=\"text-right\">{{ _(\"Qty\") }}</th>\n\t\t\t<th width=\"35%\" class=\"text-right\">{{ _(\"Amount\") }}</th>\n\t\t</tr>\n\t</thead>\n\t<tbody>\n\t\t{%- for item in doc.items -%}\n\t\t<tr>\n\t\t\t<td>\n\t\t\t\t{{ item.item_code }}\n\t\t\t\t{%- if item.item_name != item.item_code -%}\n\t\t\t\t\t<br>{{ item.item_name }}\n\t\t\t\t{%- endif -%}\n\t\t\t\t{%- if item.serial_no -%}\n\t\t\t\t\t<br><b>{{ _(\"SR.No\") }}:</b><br>\n\t\t\t\t\t{{ item.serial_no | replace(\"\\n\", \", \") }}\n\t\t\t\t{%- endif -%}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">{{ item.qty }}</td>\n\t\t\t<td class=\"text-right\">{{ item.get_formatted(\"net_amount\") }}</td>\n\t\t</tr>\n\t\t{%- endfor -%}\n\t</tbody>\n</table>\n<table class=\"table table-condensed no-border\">\n\t<tbody>\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 60%\">\n\t\t\t\t{{ _(\"Total Excl. Tax\") }}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"net_total\", doc) }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- for row in doc.taxes -%}\n\t\t\t<tr>\n\t\t\t\t<td class=\"text-right\" style=\"width: 60%\">\n\t\t\t\t {% if '%' in row.description %}\n\t\t\t\t\t {{ row.description }}\n\t\t\t\t\t{% else %}\n\t\t\t\t\t {{ row.description }}@{{ row.rate }}%\n\t\t\t\t\t{% endif %}\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ row.get_formatted(\"tax_amount\", doc) }}\n\t\t\t\t</td>\n\t\t\t<tr>\n\t\t{%- endfor -%}\n\n\t\t{%- if doc.discount_amount -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 60%\">\n\t\t\t\t{{ _(\"Discount\") }}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"discount_amount\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- endif -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 60%\">\n\t\t\t\t<b>{{ _(\"Grand Total\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"grand_total\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- if doc.rounded_total -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 60%\">\n\t\t\t\t<b>{{ _(\"Rounded Total\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"rounded_total\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- endif -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 60%\">\n\t\t\t\t<b>{{ _(\"Paid Amount\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"paid_amount\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- if doc.change_amount -%}\n\t\t\t<tr>\n\t\t\t\t<td class=\"text-right\" style=\"width: 60%\">\n\t\t\t\t\t<b>{{ _(\"Change Amount\") }}</b>\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ doc.get_formatted(\"change_amount\") }}\n\t\t\t\t</td>\n\t\t\t</tr>\n\t\t{%- endif -%}\n\t</tbody>\n</table>\n<hr>\n<p>{{ doc.terms or \"\" }}</p>\n<p class=\"text-center\">{{ _(\"Thank you, please visit again.\") }}</p>",
+ "idx": 0,
+ "line_breaks": 0,
+ "margin_bottom": 0.0,
+ "margin_left": 0.0,
+ "margin_right": 0.0,
+ "margin_top": 0.0,
+ "modified": "2021-12-08 10:25:01.930885",
+ "modified_by": "Administrator",
+ "module": "Regional",
+ "name": "KSA POS Invoice",
+ "owner": "Administrator",
+ "page_number": "Hide",
+ "print_format_builder": 0,
+ "print_format_builder_beta": 0,
+ "print_format_type": "Jinja",
+ "raw_printing": 0,
+ "show_section_headings": 0,
+ "standard": "Yes"
+}
\ No newline at end of file
diff --git a/erpnext/regional/print_format/ksa_vat_invoice/ksa_vat_invoice.json b/erpnext/regional/print_format/ksa_vat_invoice/ksa_vat_invoice.json
index 36d6536..6b64d47 100644
--- a/erpnext/regional/print_format/ksa_vat_invoice/ksa_vat_invoice.json
+++ b/erpnext/regional/print_format/ksa_vat_invoice/ksa_vat_invoice.json
@@ -2,22 +2,22 @@
"absolute_value": 0,
"align_labels_right": 0,
"creation": "2021-10-29 22:46:26.039023",
- "css": ".qr-code{\n float:right;\n}\n\n.invoice-heading {\n margin: 0;\n}\n\n.ksa-invoice-table {\n border: 1px solid #888a8e;\n border-collapse: collapse;\n width: 100%;\n margin: 20px 0;\n color: #888a8e;\n font-size: 16px;\n}\n\n.ksa-invoice-table.two-columns td:nth-child(2) {\n direction: rtl;\n}\n\n.ksa-invoice-table th {\n border: 1px solid #888a8e;\n max-width: 50%;\n background-color: #265e4a !important;\n color: #fff;\n padding: 8px;\n}\n\n.ksa-invoice-table td {\n padding: 5px;\n border: 1px solid #888a8e;\n max-width: 50%;\n}\n\n.ksa-invoice-table thead,\n.ksa-invoice-table tfoot {\n text-transform: uppercase;\n}\n\n.qr-rtl {\n direction: rtl;\n}\n\n.qr-flex{\n display: flex;\n justify-content: space-between;\n}",
+ "css": ".qr-code{\n float:right;\n}\n\n.invoice-heading {\n margin: 0;\n}\n\n.ksa-invoice-table {\n border: 1px solid #888a8e;\n border-collapse: collapse;\n width: 100%;\n margin: 20px 0;\n font-size: 16px;\n}\n\n.ksa-invoice-table.two-columns td:nth-child(2) {\n direction: rtl;\n}\n\n.ksa-invoice-table th {\n border: 1px solid #888a8e;\n max-width: 50%;\n padding: 8px;\n}\n\n.ksa-invoice-table td {\n padding: 5px;\n border: 1px solid #888a8e;\n max-width: 50%;\n}\n\n.ksa-invoice-table thead,\n.ksa-invoice-table tfoot {\n text-transform: uppercase;\n}\n\n.qr-rtl {\n direction: rtl;\n}\n\n.qr-flex{\n display: flex;\n justify-content: space-between;\n}",
"custom_format": 1,
"default_print_language": "en",
- "disabled": 0,
+ "disabled": 1,
"doc_type": "Sales Invoice",
"docstatus": 0,
"doctype": "Print Format",
"font_size": 14,
- "html": "<div class=\"ksa-vat-format\">\n <div class=\"qr-flex\">\n <div style=\"qr-flex: 1\">\n <h2 class=\"invoice-heading\">TAX INVOICE</h2>\n <h2 class=\"invoice-heading\">\u0641\u0627\u062a\u0648\u0631\u0629 \u0636\u0631\u064a\u0628\u064a\u0629</h2>\n </div>\n \n <img class=\"qr-code\" src={{doc.qr_code}}>\n </div>\n {% set company = frappe.get_doc(\"Company\", doc.company)%}\n {% if (doc.company_address) %}\n {% set supplier_address_doc = frappe.get_doc('Address', doc.company_address) %}\n {% endif %}\n \n {% if(doc.customer_address) %}\n {% set customer_address = frappe.get_doc('Address', doc.customer_address ) %}\n {% endif %}\n \n {% if(doc.shipping_address_name) %}\n {% set customer_shipping_address = frappe.get_doc('Address', doc.shipping_address_name ) %}\n {% endif %} \n \n <table class=\"ksa-invoice-table two-columns\">\n <thead>\n <tr>\n <th>{{ company.name }}</th>\n <th style=\"text-align: right;\">{{ company.company_name_in_arabic }}</th>\n </tr>\n </thead>\n\n <tbody>\n <!-- Invoice Info -->\n <tr>\n <td>Invoice#: {{doc.name}}</td>\n <td>\u0631\u0642\u0645 \u0627\u0644\u0641\u0627\u062a\u0648\u0631\u0629: {{doc.name}}</td>\n </tr>\n <tr>\n <td>Invoice Date: {{doc.posting_date}}</td>\n <td>\u062a\u0627\u0631\u064a\u062e \u0627\u0644\u0641\u0627\u062a\u0648\u0631\u0629: {{doc.posting_date}}</td>\n </tr>\n <tr>\n <td>Date of Supply:{{doc.posting_date}}</td>\n <td>\u062a\u0627\u0631\u064a\u062e \u0627\u0644\u062a\u0648\u0631\u064a\u062f: {{doc.posting_date}}</td>\n </tr>\n \n <!--Supplier Info -->\n <tr>\n <td>Supplier:</td>\n <td>\u0627\u0644\u0645\u0648\u0631\u062f:</td>\n </tr>\n\t\t{% if (company.tax_id) %}\n <tr>\n <td>Supplier Tax Identification Number:</td>\n <td>\u0631\u0642\u0645 \u0627\u0644\u062a\u0639\u0631\u064a\u0641 \u0627\u0644\u0636\u0631\u064a\u0628\u064a \u0644\u0644\u0645\u0648\u0631\u062f:</td>\n </tr>\n <tr>\n <td>{{ company.tax_id }}</td>\n <td>{{ company.tax_id }}</td>\n </tr>\n {% endif %}\n <tr>\n <td>{{ company.name }}</td>\n <td>{{ company.company_name_in_arabic }} </td>\n </tr>\n \n \n {% if(supplier_address_doc) %}\n <tr>\n <td>{{ supplier_address_doc.address_line1}} </td>\n <td>{{ supplier_address_doc.address_in_arabic}} </td>\n </tr>\n <tr>\n <td>Phone: {{ supplier_address_doc.phone }}</td>\n <td>\u0647\u0627\u062a\u0641: {{ supplier_address_doc.phone }}</td>\n </tr>\n <tr>\n <td>Email: {{ supplier_address_doc.email_id }}</td>\n <td>\u0628\u0631\u064a\u062f \u0627\u0644\u0643\u062a\u0631\u0648\u0646\u064a: {{ supplier_address_doc.email_id }}</td>\n </tr>\n {% endif %}\n \n <!-- Customer Info -->\n <tr>\n <td>CUSTOMER:</td>\n <td>\u0639\u0645\u064a\u0644:</td>\n </tr>\n\t\t{% set customer_tax_id = frappe.db.get_value('Customer', doc.customer, 'tax_id') %}\n\t\t{% if customer_tax_id %}\n <tr>\n <td>Customer Tax Identification Number:</td>\n <td>\u0631\u0642\u0645 \u0627\u0644\u062a\u0639\u0631\u064a\u0641 \u0627\u0644\u0636\u0631\u064a\u0628\u064a \u0644\u0644\u0639\u0645\u064a\u0644:</td>\n </tr>\n <tr>\n <td>{{ customer_tax_id }}</td>\n <td>{{ customer_tax_id }}</td>\n </tr>\n {% endif %}\n <tr>\n <td> {{ doc.customer }}</td>\n <td> {{ doc.customer_name_in_arabic }} </td>\n </tr>\n \n {% if(customer_address) %}\n <tr>\n <td>{{ customer_address.address_line1}} </td>\n <td>{{ customer_address.address_in_arabic}} </td>\n </tr>\n {% endif %}\n \n {% if(customer_shipping_address) %}\n <tr>\n <td>SHIPPING ADDRESS:</td>\n <td>\u0639\u0646\u0648\u0627\u0646 \u0627\u0644\u0634\u062d\u0646:</td>\n </tr>\n \n <tr>\n <td>{{ customer_shipping_address.address_line1}} </td>\n <td>{{ customer_shipping_address.address_in_arabic}} </td>\n </tr>\n {% endif %}\n \n\t\t{% if(doc.po_no) %}\n <tr>\n <td>OTHER INFORMATION</td>\n <td>\u0645\u0639\u0644\u0648\u0645\u0627\u062a \u0623\u062e\u0631\u0649</td>\n </tr>\n \n <tr>\n <td>Purchase Order Number: {{ doc.po_no }}</td>\n <td>\u0631\u0642\u0645 \u0623\u0645\u0631 \u0627\u0644\u0634\u0631\u0627\u0621: {{ doc.po_no }}</td>\n </tr>\n {% endif %}\n \n <tr>\n <td>Payment Due Date: {{ doc.due_date}} </td>\n <td>\u062a\u0627\u0631\u064a\u062e \u0627\u0633\u062a\u062d\u0642\u0627\u0642 \u0627\u0644\u062f\u0641\u0639: {{ doc.due_date}}</td>\n </tr>\n </tbody>\n </table>\n\n <!--Dynamic Colspan for total row columns-->\n {% set col = namespace(one = 2, two = 1) %}\n {% set length = doc.taxes | length %}\n {% set length = length / 2 | round %}\n {% set col.one = col.one + length %}\n {% set col.two = col.two + length %}\n \n {%- if(doc.taxes | length % 2 > 0 ) -%}\n {% set col.two = col.two + 1 %}\n {% endif %}\n \n <!-- Items -->\n {% set total = namespace(amount = 0) %}\n <table class=\"ksa-invoice-table\">\n <thead>\n <tr>\n <th>Nature of goods or services <br />\u0637\u0628\u064a\u0639\u0629 \u0627\u0644\u0633\u0644\u0639 \u0623\u0648 \u0627\u0644\u062e\u062f\u0645\u0627\u062a</th>\n <th>\n Unit price <br />\n \u0633\u0639\u0631 \u0627\u0644\u0648\u062d\u062f\u0629\n </th>\n <th>\n Quantity <br />\n \u0627\u0644\u0643\u0645\u064a\u0629\n </th>\n <th>\n Taxable Amount <br />\n \u0627\u0644\u0645\u0628\u0644\u063a \u0627\u0644\u062e\u0627\u0636\u0639 \u0644\u0644\u0636\u0631\u064a\u0628\u0629\n </th>\n \n {% for row in doc.taxes %}\n <th style=\"min-width: 130px\">{{row.description}}</th>\n {% endfor %}\n \n <th>\n Total <br />\n \u0627\u0644\u0645\u062c\u0645\u0648\u0639\n </th>\n </tr>\n </thead>\n <tbody>\n {%- for item in doc.items -%}\n {% set total.amount = item.amount %}\n <tr>\n <td>{{ item.item_code }}</td>\n <td>{{ item.get_formatted(\"rate\") }}</td>\n <td>{{ item.qty }}</td>\n <td>{{ item.get_formatted(\"amount\") }}</td>\n {% for row in doc.taxes %}\n {% set data_object = json.loads(row.item_wise_tax_detail) %}\n <td>\n <div class=\"qr-flex\">\n {%- if(data_object[item.item_code][0])-%}\n <span>{{ frappe.format(data_object[item.item_code][0], {'fieldtype': 'Percent'}) }}</span>\n {%- endif -%}\n <span>\n {%- if(data_object[item.item_code][1])-%}\n {{ frappe.format(data_object[item.item_code][1], {'fieldtype': 'Currency'}) }}</span>\n {% set total.amount = total.amount + data_object[item.item_code][1] %}\n {%- endif -%}\n </div>\n </td>\n {% endfor %}\n <td>{{ frappe.format(total.amount, {'fieldtype': 'Currency'}) }}</td>\n </tr>\n {%- endfor -%}\n </tbody>\n <tfoot>\n <tr>\n <td>\n {{ doc.get_formatted(\"total\") }} <br />\n {{ doc.get_formatted(\"total_taxes_and_charges\") }}\n </td>\n \n <td colspan={{ col.one }} class=\"qr-rtl\">\n \u0627\u0644\u0625\u062c\u0645\u0627\u0644\u064a \u0628\u0627\u0633\u062a\u062b\u0646\u0627\u0621 \u0636\u0631\u064a\u0628\u0629 \u0627\u0644\u0642\u064a\u0645\u0629 \u0627\u0644\u0645\u0636\u0627\u0641\u0629\n <br />\n \u0625\u062c\u0645\u0627\u0644\u064a \u0636\u0631\u064a\u0628\u0629 \u0627\u0644\u0642\u064a\u0645\u0629 \u0627\u0644\u0645\u0636\u0627\u0641\u0629\n </td>\n <td colspan={{ col.two }}>\n Total (Excluding VAT)\n <br />\n Total VAT\n </td>\n <td>\n {{ doc.get_formatted(\"total\") }} <br />\n {{ doc.get_formatted(\"total_taxes_and_charges\") }}\n </td>\n </tr>\n <tr>\n <td>{{ doc.get_formatted(\"grand_total\") }}</td>\n <td colspan={{ col.one }} class=\"qr-rtl\">\n \u0625\u062c\u0645\u0627\u0644\u064a \u0627\u0644\u0645\u0628\u0644\u063a \u0627\u0644\u0645\u0633\u062a\u062d\u0642</td>\n <td colspan={{ col.two }}>Total Amount Due</td>\n <td>{{ doc.get_formatted(\"grand_total\") }}</td>\n </tr>\n </tfoot>\n </table>\n\n\t{%- if doc.terms -%}\n <p>\n {{doc.terms}}\n </p>\n\t{%- endif -%}\n</div>\n",
+ "html": "<div class=\"ksa-vat-format\">\n <div class=\"qr-flex\">\n <div style=\"qr-flex: 1\">\n <h2 class=\"invoice-heading\">TAX INVOICE</h2>\n <h2 class=\"invoice-heading\">\u0641\u0627\u062a\u0648\u0631\u0629 \u0636\u0631\u064a\u0628\u064a\u0629</h2>\n </div>\n \n <img class=\"qr-code\" src={{doc.ksa_einv_qr}}>\n </div>\n {% set company = frappe.get_doc(\"Company\", doc.company)%}\n {% if (doc.company_address) %}\n {% set supplier_address_doc = frappe.get_doc('Address', doc.company_address) %}\n {% endif %}\n \n {% if(doc.customer_address) %}\n {% set customer_address = frappe.get_doc('Address', doc.customer_address ) %}\n {% endif %}\n \n {% if(doc.shipping_address_name) %}\n {% set customer_shipping_address = frappe.get_doc('Address', doc.shipping_address_name ) %}\n {% endif %} \n \n <table class=\"ksa-invoice-table two-columns\">\n <thead>\n <tr>\n <th>{{ company.name }}</th>\n <th style=\"text-align: right;\">{{ company.company_name_in_arabic }}</th>\n </tr>\n </thead>\n\n <tbody>\n <!-- Invoice Info -->\n <tr>\n <td>Invoice#: {{doc.name}}</td>\n <td>\u0631\u0642\u0645 \u0627\u0644\u0641\u0627\u062a\u0648\u0631\u0629: {{doc.name}}</td>\n </tr>\n <tr>\n <td>Invoice Date: {{doc.posting_date}}</td>\n <td>\u062a\u0627\u0631\u064a\u062e \u0627\u0644\u0641\u0627\u062a\u0648\u0631\u0629: {{doc.posting_date}}</td>\n </tr>\n <tr>\n <td>Date of Supply:{{doc.posting_date}}</td>\n <td>\u062a\u0627\u0631\u064a\u062e \u0627\u0644\u062a\u0648\u0631\u064a\u062f: {{doc.posting_date}}</td>\n </tr>\n \n <!--Supplier Info -->\n <tr>\n <td>Supplier:</td>\n <td>\u0627\u0644\u0645\u0648\u0631\u062f:</td>\n </tr>\n\t\t{% if (company.tax_id) %}\n <tr>\n <td>Supplier Tax Identification Number:</td>\n <td>\u0631\u0642\u0645 \u0627\u0644\u062a\u0639\u0631\u064a\u0641 \u0627\u0644\u0636\u0631\u064a\u0628\u064a \u0644\u0644\u0645\u0648\u0631\u062f:</td>\n </tr>\n <tr>\n <td>{{ company.tax_id }}</td>\n <td>{{ company.tax_id }}</td>\n </tr>\n {% endif %}\n <tr>\n <td>{{ company.name }}</td>\n <td>{{ company.company_name_in_arabic }} </td>\n </tr>\n \n \n {% if(supplier_address_doc) %}\n <tr>\n <td>{{ supplier_address_doc.address_line1}} </td>\n <td>{{ supplier_address_doc.address_in_arabic}} </td>\n </tr>\n <tr>\n <td>Phone: {{ supplier_address_doc.phone }}</td>\n <td>\u0647\u0627\u062a\u0641: {{ supplier_address_doc.phone }}</td>\n </tr>\n <tr>\n <td>Email: {{ supplier_address_doc.email_id }}</td>\n <td>\u0628\u0631\u064a\u062f \u0627\u0644\u0643\u062a\u0631\u0648\u0646\u064a: {{ supplier_address_doc.email_id }}</td>\n </tr>\n {% endif %}\n \n <!-- Customer Info -->\n <tr>\n <td>CUSTOMER:</td>\n <td>\u0639\u0645\u064a\u0644:</td>\n </tr>\n\t\t{% set customer_tax_id = frappe.db.get_value('Customer', doc.customer, 'tax_id') %}\n\t\t{% if customer_tax_id %}\n <tr>\n <td>Customer Tax Identification Number:</td>\n <td>\u0631\u0642\u0645 \u0627\u0644\u062a\u0639\u0631\u064a\u0641 \u0627\u0644\u0636\u0631\u064a\u0628\u064a \u0644\u0644\u0639\u0645\u064a\u0644:</td>\n </tr>\n <tr>\n <td>{{ customer_tax_id }}</td>\n <td>{{ customer_tax_id }}</td>\n </tr>\n {% endif %}\n <tr>\n <td> {{ doc.customer }}</td>\n <td> {{ doc.customer_name_in_arabic }} </td>\n </tr>\n \n {% if(customer_address) %}\n <tr>\n <td>{{ customer_address.address_line1}} </td>\n <td>{{ customer_address.address_in_arabic}} </td>\n </tr>\n {% endif %}\n \n {% if(customer_shipping_address) %}\n <tr>\n <td>SHIPPING ADDRESS:</td>\n <td>\u0639\u0646\u0648\u0627\u0646 \u0627\u0644\u0634\u062d\u0646:</td>\n </tr>\n \n <tr>\n <td>{{ customer_shipping_address.address_line1}} </td>\n <td>{{ customer_shipping_address.address_in_arabic}} </td>\n </tr>\n {% endif %}\n \n\t\t{% if(doc.po_no) %}\n <tr>\n <td>OTHER INFORMATION</td>\n <td>\u0645\u0639\u0644\u0648\u0645\u0627\u062a \u0623\u062e\u0631\u0649</td>\n </tr>\n \n <tr>\n <td>Purchase Order Number: {{ doc.po_no }}</td>\n <td>\u0631\u0642\u0645 \u0623\u0645\u0631 \u0627\u0644\u0634\u0631\u0627\u0621: {{ doc.po_no }}</td>\n </tr>\n {% endif %}\n \n <tr>\n <td>Payment Due Date: {{ doc.due_date}} </td>\n <td>\u062a\u0627\u0631\u064a\u062e \u0627\u0633\u062a\u062d\u0642\u0627\u0642 \u0627\u0644\u062f\u0641\u0639: {{ doc.due_date}}</td>\n </tr>\n </tbody>\n </table>\n\n <!--Dynamic Colspan for total row columns-->\n {% set col = namespace(one = 2, two = 1) %}\n {% set length = doc.taxes | length %}\n {% set length = length / 2 | round %}\n {% set col.one = col.one + length %}\n {% set col.two = col.two + length %}\n \n {%- if(doc.taxes | length % 2 > 0 ) -%}\n {% set col.two = col.two + 1 %}\n {% endif %}\n \n <!-- Items -->\n {% set total = namespace(amount = 0) %}\n <table class=\"ksa-invoice-table\">\n <thead>\n <tr>\n <th>Nature of goods or services <br />\u0637\u0628\u064a\u0639\u0629 \u0627\u0644\u0633\u0644\u0639 \u0623\u0648 \u0627\u0644\u062e\u062f\u0645\u0627\u062a</th>\n <th>\n Unit price <br />\n \u0633\u0639\u0631 \u0627\u0644\u0648\u062d\u062f\u0629\n </th>\n <th>\n Quantity <br />\n \u0627\u0644\u0643\u0645\u064a\u0629\n </th>\n <th>\n Taxable Amount <br />\n \u0627\u0644\u0645\u0628\u0644\u063a \u0627\u0644\u062e\u0627\u0636\u0639 \u0644\u0644\u0636\u0631\u064a\u0628\u0629\n </th>\n \n {% for row in doc.taxes %}\n <th style=\"min-width: 130px\">{{row.description}}</th>\n {% endfor %}\n \n <th>\n Total <br />\n \u0627\u0644\u0645\u062c\u0645\u0648\u0639\n </th>\n </tr>\n </thead>\n <tbody>\n {%- for item in doc.items -%}\n {% set total.amount = item.amount %}\n <tr>\n <td>{{ item.item_code or item.item_name }}</td>\n <td>{{ item.get_formatted(\"rate\") }}</td>\n <td>{{ item.qty }}</td>\n <td>{{ item.get_formatted(\"amount\") }}</td>\n {% for row in doc.taxes %}\n {% set data_object = json.loads(row.item_wise_tax_detail) %}\n {% set key = item.item_code or item.item_name %}\n {% set tax_amount = frappe.utils.flt(data_object[key][1]/doc.conversion_rate, row.precision('tax_amount')) %}\n <td>\n <div class=\"qr-flex\">\n {%- if(data_object[key][0])-%}\n <span>{{ frappe.format(data_object[key][0], {'fieldtype': 'Percent'}) }}</span>\n {%- endif -%}\n <span>\n {%- if(data_object[key][1])-%}\n {{ frappe.format_value(tax_amount, currency=doc.currency) }}</span>\n {% set total.amount = total.amount + tax_amount %}\n {%- endif -%}\n </div>\n </td>\n {% endfor %}\n <td>{{ frappe.format_value(frappe.utils.flt(total.amount, doc.precision('total_taxes_and_charges')), currency=doc.currency) }}</td>\n </tr>\n {%- endfor -%}\n </tbody>\n <tfoot>\n <tr>\n <td>\n {{ doc.get_formatted(\"total\") }} <br />\n {{ doc.get_formatted(\"total_taxes_and_charges\") }}\n </td>\n \n <td colspan={{ col.one }} class=\"qr-rtl\">\n \u0627\u0644\u0625\u062c\u0645\u0627\u0644\u064a \u0628\u0627\u0633\u062a\u062b\u0646\u0627\u0621 \u0636\u0631\u064a\u0628\u0629 \u0627\u0644\u0642\u064a\u0645\u0629 \u0627\u0644\u0645\u0636\u0627\u0641\u0629\n <br />\n \u0625\u062c\u0645\u0627\u0644\u064a \u0636\u0631\u064a\u0628\u0629 \u0627\u0644\u0642\u064a\u0645\u0629 \u0627\u0644\u0645\u0636\u0627\u0641\u0629\n </td>\n <td colspan={{ col.two }}>\n Total (Excluding VAT)\n <br />\n Total VAT\n </td>\n <td>\n {{ doc.get_formatted(\"total\") }} <br />\n {{ doc.get_formatted(\"total_taxes_and_charges\") }}\n </td>\n </tr>\n <tr>\n <td>{{ doc.get_formatted(\"grand_total\") }}</td>\n <td colspan={{ col.one }} class=\"qr-rtl\">\n \u0625\u062c\u0645\u0627\u0644\u064a \u0627\u0644\u0645\u0628\u0644\u063a \u0627\u0644\u0645\u0633\u062a\u062d\u0642</td>\n <td colspan={{ col.two }}>Total Amount Due</td>\n <td>{{ doc.get_formatted(\"grand_total\") }}</td>\n </tr>\n </tfoot>\n </table>\n\n\t{%- if doc.terms -%}\n <p>\n {{doc.terms}}\n </p>\n\t{%- endif -%}\n</div>\n",
"idx": 0,
"line_breaks": 0,
"margin_bottom": 15.0,
"margin_left": 15.0,
"margin_right": 15.0,
"margin_top": 15.0,
- "modified": "2021-11-08 09:19:18.660806",
+ "modified": "2021-12-07 13:43:38.018593",
"modified_by": "Administrator",
"module": "Regional",
"name": "KSA VAT Invoice",
diff --git a/erpnext/regional/report/eway_bill/eway_bill.py b/erpnext/regional/report/eway_bill/eway_bill.py
index 91a4767..f3fe5e8 100644
--- a/erpnext/regional/report/eway_bill/eway_bill.py
+++ b/erpnext/regional/report/eway_bill/eway_bill.py
@@ -106,14 +106,14 @@
row.update({'ship_to_state': row.to_state})
def set_taxes(row, filters):
- taxes = frappe.get_list("Sales Taxes and Charges",
+ taxes = frappe.get_all("Sales Taxes and Charges",
filters={
'parent': row.dn_id
},
fields=('item_wise_tax_detail', 'account_head'))
account_list = ["cgst_account", "sgst_account", "igst_account", "cess_account"]
- taxes_list = frappe.get_list("GST Account",
+ taxes_list = frappe.get_all("GST Account",
filters={
"parent": "GST Settings",
"company": filters.company
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 e03ad37..1c1335e 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
@@ -114,9 +114,11 @@
items = frappe.db.sql("""
select
- `tabSales Invoice Item`.name, `tabSales Invoice Item`.base_price_list_rate,
- `tabSales Invoice Item`.gst_hsn_code, `tabSales Invoice Item`.stock_qty,
- `tabSales Invoice Item`.stock_uom, `tabSales Invoice Item`.base_net_amount,
+ `tabSales Invoice Item`.gst_hsn_code,
+ `tabSales Invoice Item`.stock_uom,
+ sum(`tabSales Invoice Item`.stock_qty) as stock_qty,
+ sum(`tabSales Invoice Item`.base_net_amount) as base_net_amount,
+ sum(`tabSales Invoice Item`.base_price_list_rate) as base_price_list_rate,
`tabSales Invoice Item`.parent, `tabSales Invoice Item`.item_code,
`tabGST HSN Code`.description
from `tabSales Invoice`, `tabSales Invoice Item`, `tabGST HSN Code`
@@ -124,6 +126,8 @@
and `tabSales Invoice`.docstatus = 1
and `tabSales Invoice Item`.gst_hsn_code is not NULL
and `tabSales Invoice Item`.gst_hsn_code = `tabGST HSN Code`.name %s %s
+ group by
+ `tabSales Invoice Item`.parent, `tabSales Invoice Item`.item_code
""" % (conditions, match_conditions), filters, as_dict=1)
diff --git a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/test_hsn_wise_summary_of_outward_supplies.py b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/test_hsn_wise_summary_of_outward_supplies.py
new file mode 100644
index 0000000..86dc458
--- /dev/null
+++ b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/test_hsn_wise_summary_of_outward_supplies.py
@@ -0,0 +1,89 @@
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+
+from unittest import TestCase
+
+import frappe
+
+from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
+from erpnext.regional.doctype.gstr_3b_report.test_gstr_3b_report import (
+ make_company as setup_company,
+)
+from erpnext.regional.doctype.gstr_3b_report.test_gstr_3b_report import (
+ make_customers as setup_customers,
+)
+from erpnext.regional.doctype.gstr_3b_report.test_gstr_3b_report import (
+ set_account_heads as setup_gst_settings,
+)
+from erpnext.regional.report.hsn_wise_summary_of_outward_supplies.hsn_wise_summary_of_outward_supplies import (
+ execute as run_report,
+)
+from erpnext.stock.doctype.item.test_item import make_item
+
+
+class TestHSNWiseSummaryReport(TestCase):
+ @classmethod
+ def setUpClass(cls):
+ setup_company()
+ setup_customers()
+ setup_gst_settings()
+ make_item("Golf Car", properties={ "gst_hsn_code": "999900" })
+
+ @classmethod
+ def tearDownClass(cls):
+ frappe.db.rollback()
+
+ def test_hsn_summary_for_invoice_with_duplicate_items(self):
+ si = create_sales_invoice(
+ company="_Test Company GST",
+ customer = "_Test GST Customer",
+ currency = "INR",
+ warehouse = "Finished Goods - _GST",
+ debit_to = "Debtors - _GST",
+ income_account = "Sales - _GST",
+ expense_account = "Cost of Goods Sold - _GST",
+ cost_center = "Main - _GST",
+ do_not_save=1
+ )
+
+ si.items = []
+ si.append("items", {
+ "item_code": "Golf Car",
+ "gst_hsn_code": "999900",
+ "qty": "1",
+ "rate": "120",
+ "cost_center": "Main - _GST"
+ })
+ si.append("items", {
+ "item_code": "Golf Car",
+ "gst_hsn_code": "999900",
+ "qty": "1",
+ "rate": "140",
+ "cost_center": "Main - _GST"
+ })
+ si.append("taxes", {
+ "charge_type": "On Net Total",
+ "account_head": "Output Tax IGST - _GST",
+ "cost_center": "Main - _GST",
+ "description": "IGST @ 18.0",
+ "rate": 18
+ })
+ si.posting_date = "2020-11-17"
+ si.submit()
+ si.reload()
+
+ [columns, data] = run_report(filters=frappe._dict({
+ "company": "_Test Company GST",
+ "gst_hsn_code": "999900",
+ "company_gstin": si.company_gstin,
+ "from_date": si.posting_date,
+ "to_date": si.posting_date
+ }))
+
+ filtered_rows = list(filter(lambda row: row['gst_hsn_code'] == "999900", data))
+ self.assertTrue(filtered_rows)
+
+ hsn_row = filtered_rows[0]
+ self.assertEquals(hsn_row['stock_qty'], 2.0)
+ self.assertEquals(hsn_row['total_amount'], 306.8)
diff --git a/erpnext/regional/report/ksa_vat/ksa_vat.py b/erpnext/regional/report/ksa_vat/ksa_vat.py
index b41b2b0..cc26bd7 100644
--- a/erpnext/regional/report/ksa_vat/ksa_vat.py
+++ b/erpnext/regional/report/ksa_vat/ksa_vat.py
@@ -20,25 +20,35 @@
"fieldname": "title",
"label": _("Title"),
"fieldtype": "Data",
- "width": 300
+ "width": 300,
},
{
"fieldname": "amount",
"label": _("Amount (SAR)"),
"fieldtype": "Currency",
+ "options": "currency",
"width": 150,
},
{
"fieldname": "adjustment_amount",
"label": _("Adjustment (SAR)"),
"fieldtype": "Currency",
+ "options": "currency",
"width": 150,
},
{
"fieldname": "vat_amount",
"label": _("VAT Amount (SAR)"),
"fieldtype": "Currency",
+ "options": "currency",
"width": 150,
+ },
+ {
+ "fieldname": "currency",
+ "label": _("Currency"),
+ "fieldtype": "Currency",
+ "width": 150,
+ "hidden": 1
}
]
@@ -47,6 +57,8 @@
# Validate if vat settings exist
company = filters.get('company')
+ company_currency = frappe.get_cached_value('Company', company, "default_currency")
+
if frappe.db.exists('KSA VAT Setting', company) is None:
url = get_url_to_list('KSA VAT Setting')
frappe.msgprint(_('Create <a href="{}">KSA VAT Setting</a> for this company').format(url))
@@ -55,7 +67,7 @@
ksa_vat_setting = frappe.get_doc('KSA VAT Setting', company)
# Sales Heading
- append_data(data, 'VAT on Sales', '', '', '')
+ append_data(data, 'VAT on Sales', '', '', '', company_currency)
grand_total_taxable_amount = 0
grand_total_taxable_adjustment_amount = 0
@@ -67,7 +79,7 @@
# Adding results to data
append_data(data, vat_setting.title, total_taxable_amount,
- total_taxable_adjustment_amount, total_tax)
+ total_taxable_adjustment_amount, total_tax, company_currency)
grand_total_taxable_amount += total_taxable_amount
grand_total_taxable_adjustment_amount += total_taxable_adjustment_amount
@@ -75,13 +87,13 @@
# Sales Grand Total
append_data(data, 'Grand Total', grand_total_taxable_amount,
- grand_total_taxable_adjustment_amount, grand_total_tax)
+ grand_total_taxable_adjustment_amount, grand_total_tax, company_currency)
# Blank Line
- append_data(data, '', '', '', '')
+ append_data(data, '', '', '', '', company_currency)
# Purchase Heading
- append_data(data, 'VAT on Purchases', '', '', '')
+ append_data(data, 'VAT on Purchases', '', '', '', company_currency)
grand_total_taxable_amount = 0
grand_total_taxable_adjustment_amount = 0
@@ -93,7 +105,7 @@
# Adding results to data
append_data(data, vat_setting.title, total_taxable_amount,
- total_taxable_adjustment_amount, total_tax)
+ total_taxable_adjustment_amount, total_tax, company_currency)
grand_total_taxable_amount += total_taxable_amount
grand_total_taxable_adjustment_amount += total_taxable_adjustment_amount
@@ -101,7 +113,7 @@
# Purchase Grand Total
append_data(data, 'Grand Total', grand_total_taxable_amount,
- grand_total_taxable_adjustment_amount, grand_total_tax)
+ grand_total_taxable_adjustment_amount, grand_total_tax, company_currency)
return data
@@ -147,9 +159,10 @@
-def append_data(data, title, amount, adjustment_amount, vat_amount):
+def append_data(data, title, amount, adjustment_amount, vat_amount, company_currency):
"""Returns data with appended value."""
- data.append({"title": _(title), "amount": amount, "adjustment_amount": adjustment_amount, "vat_amount": vat_amount})
+ data.append({"title": _(title), "amount": amount, "adjustment_amount": adjustment_amount, "vat_amount": vat_amount,
+ "currency": company_currency})
def get_tax_amount(item_code, account_head, doctype, parent):
if doctype == 'Sales Invoice':
diff --git a/erpnext/regional/report/vat_audit_report/vat_audit_report.py b/erpnext/regional/report/vat_audit_report/vat_audit_report.py
index 5a281a4..17e5064 100644
--- a/erpnext/regional/report/vat_audit_report/vat_audit_report.py
+++ b/erpnext/regional/report/vat_audit_report/vat_audit_report.py
@@ -41,7 +41,7 @@
return self.columns, self.data
def get_sa_vat_accounts(self):
- self.sa_vat_accounts = frappe.get_list("South Africa VAT Account",
+ self.sa_vat_accounts = frappe.get_all("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")
diff --git a/erpnext/regional/saudi_arabia/setup.py b/erpnext/regional/saudi_arabia/setup.py
index 38a089c..2e31c03 100644
--- a/erpnext/regional/saudi_arabia/setup.py
+++ b/erpnext/regional/saudi_arabia/setup.py
@@ -3,7 +3,7 @@
import frappe
from frappe.permissions import add_permission, update_permission_property
-from erpnext.regional.united_arab_emirates.setup import make_custom_fields as uae_custom_fields, add_print_formats
+from erpnext.regional.united_arab_emirates.setup import make_custom_fields as uae_custom_fields
from erpnext.regional.saudi_arabia.wizard.operations.setup_ksa_vat_setting import create_ksa_vat_setting
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
@@ -13,6 +13,16 @@
add_permissions()
make_custom_fields()
+def add_print_formats():
+ frappe.reload_doc("regional", "print_format", "detailed_tax_invoice", force=True)
+ frappe.reload_doc("regional", "print_format", "simplified_tax_invoice", force=True)
+ frappe.reload_doc("regional", "print_format", "tax_invoice", force=True)
+ frappe.reload_doc("regional", "print_format", "ksa_vat_invoice", force=True)
+ frappe.reload_doc("regional", "print_format", "ksa_pos_invoice", force=True)
+
+ for d in ('Simplified Tax Invoice', 'Detailed Tax Invoice', 'Tax Invoice', 'KSA VAT Invoice', 'KSA POS Invoice'):
+ frappe.db.set_value("Print Format", d, "disabled", 0)
+
def add_permissions():
"""Add Permissions for KSA VAT Setting."""
add_permission('KSA VAT Setting', 'All', 0)
@@ -33,8 +43,16 @@
custom_fields = {
'Sales Invoice': [
dict(
- fieldname='qr_code',
- label='QR Code',
+ fieldname='ksa_einv_qr',
+ label='KSA E-Invoicing QR',
+ fieldtype='Attach Image',
+ read_only=1, no_copy=1, hidden=1
+ )
+ ],
+ 'POS Invoice': [
+ dict(
+ fieldname='ksa_einv_qr',
+ label='KSA E-Invoicing QR',
fieldtype='Attach Image',
read_only=1, no_copy=1, hidden=1
)
diff --git a/erpnext/regional/saudi_arabia/utils.py b/erpnext/regional/saudi_arabia/utils.py
index cc6c0af..a03c3f0 100644
--- a/erpnext/regional/saudi_arabia/utils.py
+++ b/erpnext/regional/saudi_arabia/utils.py
@@ -1,77 +1,149 @@
import io
import os
+from base64 import b64encode
import frappe
+from frappe import _
+from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
+from frappe.utils.data import add_to_date, get_time, getdate
from pyqrcode import create as qr_create
from erpnext import get_region
-def create_qr_code(doc, method):
- """Create QR Code after inserting Sales Inv
- """
-
+def create_qr_code(doc, method=None):
region = get_region(doc.company)
if region not in ['Saudi Arabia']:
return
- # if QR Code field not present, do nothing
- if not hasattr(doc, 'qr_code'):
- return
+ # if QR Code field not present, create it. Invoices without QR are invalid as per law.
+ if not hasattr(doc, 'ksa_einv_qr'):
+ create_custom_fields({
+ doc.doctype: [
+ dict(
+ fieldname='ksa_einv_qr',
+ label='KSA E-Invoicing QR',
+ fieldtype='Attach Image',
+ read_only=1, no_copy=1, hidden=1
+ )
+ ]
+ })
# Don't create QR Code if it already exists
- qr_code = doc.get("qr_code")
+ qr_code = doc.get("ksa_einv_qr")
if qr_code and frappe.db.exists({"doctype": "File", "file_url": qr_code}):
return
- meta = frappe.get_meta('Sales Invoice')
+ meta = frappe.get_meta(doc.doctype)
- for field in meta.get_image_fields():
- if field.fieldname == 'qr_code':
- # Creating public url to print format
- default_print_format = frappe.db.get_value('Property Setter', dict(property='default_print_format', doc_type=doc.doctype), "value")
+ if "ksa_einv_qr" in [d.fieldname for d in meta.get_image_fields()]:
+ ''' TLV conversion for
+ 1. Seller's Name
+ 2. VAT Number
+ 3. Time Stamp
+ 4. Invoice Amount
+ 5. VAT Amount
+ '''
+ tlv_array = []
+ # Sellers Name
- # System Language
- language = frappe.get_system_settings('language')
+ seller_name = frappe.db.get_value(
+ 'Company',
+ doc.company,
+ 'company_name_in_arabic')
- # creating qr code for the url
- url = f"{ frappe.utils.get_url() }/{ doc.doctype }/{ doc.name }?format={ default_print_format or 'Standard' }&_lang={ language }&key={ doc.get_signature() }"
- qr_image = io.BytesIO()
- url = qr_create(url, error='L')
- url.png(qr_image, scale=2, quiet_zone=1)
+ if not seller_name:
+ frappe.throw(_('Arabic name missing for {} in the company document').format(doc.company))
- # making file
- filename = f"QR-CODE-{doc.name}.png".replace(os.path.sep, "__")
- _file = frappe.get_doc({
- "doctype": "File",
- "file_name": filename,
- "is_private": 0,
- "content": qr_image.getvalue(),
- "attached_to_doctype": doc.get("doctype"),
- "attached_to_name": doc.get("name"),
- "attached_to_field": "qr_code"
- })
+ tag = bytes([1]).hex()
+ length = bytes([len(seller_name.encode('utf-8'))]).hex()
+ value = seller_name.encode('utf-8').hex()
+ tlv_array.append(''.join([tag, length, value]))
- _file.save()
+ # VAT Number
+ tax_id = frappe.db.get_value('Company', doc.company, 'tax_id')
+ if not tax_id:
+ frappe.throw(_('Tax ID missing for {} in the company document').format(doc.company))
- # assigning to document
- doc.db_set('qr_code', _file.file_url)
- doc.notify_update()
+ tag = bytes([2]).hex()
+ length = bytes([len(tax_id)]).hex()
+ value = tax_id.encode('utf-8').hex()
+ tlv_array.append(''.join([tag, length, value]))
- break
+ # Time Stamp
+ posting_date = getdate(doc.posting_date)
+ time = get_time(doc.posting_time)
+ seconds = time.hour * 60 * 60 + time.minute * 60 + time.second
+ time_stamp = add_to_date(posting_date, seconds=seconds)
+ time_stamp = time_stamp.strftime('%Y-%m-%dT%H:%M:%SZ')
+
+ tag = bytes([3]).hex()
+ length = bytes([len(time_stamp)]).hex()
+ value = time_stamp.encode('utf-8').hex()
+ tlv_array.append(''.join([tag, length, value]))
+
+ # Invoice Amount
+ invoice_amount = str(doc.grand_total)
+ tag = bytes([4]).hex()
+ length = bytes([len(invoice_amount)]).hex()
+ value = invoice_amount.encode('utf-8').hex()
+ tlv_array.append(''.join([tag, length, value]))
+
+ # VAT Amount
+ vat_amount = str(doc.total_taxes_and_charges)
+
+ tag = bytes([5]).hex()
+ length = bytes([len(vat_amount)]).hex()
+ value = vat_amount.encode('utf-8').hex()
+ tlv_array.append(''.join([tag, length, value]))
+
+ # Joining bytes into one
+ tlv_buff = ''.join(tlv_array)
+
+ # base64 conversion for QR Code
+ base64_string = b64encode(bytes.fromhex(tlv_buff)).decode()
+
+ qr_image = io.BytesIO()
+ url = qr_create(base64_string, error='L')
+ url.png(qr_image, scale=2, quiet_zone=1)
+
+ name = frappe.generate_hash(doc.name, 5)
+
+ # making file
+ filename = f"QRCode-{name}.png".replace(os.path.sep, "__")
+ _file = frappe.get_doc({
+ "doctype": "File",
+ "file_name": filename,
+ "is_private": 0,
+ "content": qr_image.getvalue(),
+ "attached_to_doctype": doc.get("doctype"),
+ "attached_to_name": doc.get("name"),
+ "attached_to_field": "ksa_einv_qr"
+ })
+
+ _file.save()
+
+ # assigning to document
+ doc.db_set('ksa_einv_qr', _file.file_url)
+ doc.notify_update()
-def delete_qr_code_file(doc, method):
- """Delete QR Code on deleted sales invoice"""
-
+def delete_qr_code_file(doc, method=None):
region = get_region(doc.company)
if region not in ['Saudi Arabia']:
return
- if hasattr(doc, 'qr_code'):
- if doc.get('qr_code'):
+ if hasattr(doc, 'ksa_einv_qr'):
+ if doc.get('ksa_einv_qr'):
file_doc = frappe.get_list('File', {
- 'file_url': doc.get('qr_code')
+ 'file_url': doc.get('ksa_einv_qr')
})
if len(file_doc):
- frappe.delete_doc('File', file_doc[0].name)
\ No newline at end of file
+ frappe.delete_doc('File', file_doc[0].name)
+
+def delete_vat_settings_for_company(doc, method=None):
+ if doc.country != 'Saudi Arabia':
+ return
+
+ if frappe.db.exists('KSA VAT Setting', doc.name):
+ frappe.delete_doc('KSA VAT Setting', doc.name)
diff --git a/erpnext/restaurant/doctype/restaurant/test_restaurant.js b/erpnext/restaurant/doctype/restaurant/test_restaurant.js
deleted file mode 100644
index 8fe4e7b..0000000
--- a/erpnext/restaurant/doctype/restaurant/test_restaurant.js
+++ /dev/null
@@ -1,50 +0,0 @@
-/* eslint-disable */
-// rename this file from _test_[name] to test_[name] to activate
-// and remove above this line
-
-QUnit.test("test: Restaurant", function (assert) {
- let done = assert.async();
-
- // number of asserts
- assert.expect(2);
- let customer = {
- "Test Customer 1": [
- {customer_name: "Test Customer 1"}
- ],
- "Test Customer 2": [
- {customer_name: "Test Customer 2"}
- ]
- };
-
- frappe.run_serially([
- // insert a new Restaurant
- () => frappe.tests.setup_doctype('Customer', customer),
- () => {
- return frappe.tests.make('Restaurant', [
- // values to be set
- {__newname: 'Test Restaurant 1'},
- {company: 'Test Company'},
- {invoice_series_prefix: 'Test-Rest-1-Inv-'},
- {default_customer: 'Test Customer 1'}
- ])
- },
- () => frappe.timeout(3),
- () => {
- assert.equal(cur_frm.doc.company, 'Test Company');
- },
- () => {
- return frappe.tests.make('Restaurant', [
- // values to be set
- {__newname: 'Test Restaurant 2'},
- {company: 'Test Company'},
- {invoice_series_prefix: 'Test-Rest-3-Inv-'},
- {default_customer: 'Test Customer 2'}
- ]);
- },
- () => frappe.timeout(3),
- () => {
- assert.equal(cur_frm.doc.company, 'Test Company');
- },
- () => done()
- ]);
-});
diff --git a/erpnext/restaurant/doctype/restaurant_menu/test_restaurant_menu.js b/erpnext/restaurant/doctype/restaurant_menu/test_restaurant_menu.js
deleted file mode 100644
index f5ab9f0..0000000
--- a/erpnext/restaurant/doctype/restaurant_menu/test_restaurant_menu.js
+++ /dev/null
@@ -1,77 +0,0 @@
-/* eslint-disable */
-// rename this file from _test_[name] to test_[name] to activate
-// and remove above this line
-
-QUnit.test("test: Restaurant Menu", function (assert) {
- let done = assert.async();
-
- let items = {
- "Food Item 1": [
- {item_code: "Food Item 1"},
- {item_group: "Products"},
- {is_stock_item: 1},
- ],
- "Food Item 2": [
- {item_code: "Food Item 2"},
- {item_group: "Products"},
- {is_stock_item: 1},
- ],
- "Food Item 3": [
- {item_code: "Food Item 3"},
- {item_group: "Products"},
- {is_stock_item: 1},
- ]
- };
-
-
- // number of asserts
- assert.expect(0);
-
- frappe.run_serially([
- // insert a new Restaurant Menu
- () => frappe.tests.setup_doctype('Item', items),
- () => {
- return frappe.tests.make("Restaurant Menu", [
- {__newname: 'Restaurant Menu 1'},
- {restaurant: "Test Restaurant 1"},
- {items: [
- [
- {"item": "Food Item 1"},
- {"rate": 100}
- ],
- [
- {"item": "Food Item 2"},
- {"rate": 90}
- ],
- [
- {"item": "Food Item 3"},
- {"rate": 80}
- ]
- ]}
- ]);
- },
- () => frappe.timeout(2),
- () => {
- return frappe.tests.make("Restaurant Menu", [
- {__newname: 'Restaurant Menu 2'},
- {restaurant: "Test Restaurant 2"},
- {items: [
- [
- {"item": "Food Item 1"},
- {"rate": 105}
- ],
- [
- {"item": "Food Item 3"},
- {"rate": 85}
- ]
- ]}
- ]);
- },
- () => frappe.timeout(2),
- () => frappe.set_route('Form', 'Restaurant', 'Test Restaurant 1'),
- () => cur_frm.set_value('active_menu', 'Restaurant Menu 1'),
- () => cur_frm.save(),
- () => done()
- ]);
-
-});
diff --git a/erpnext/restaurant/doctype/restaurant_order_entry/test_restaurant_order_entry.js b/erpnext/restaurant/doctype/restaurant_order_entry/test_restaurant_order_entry.js
deleted file mode 100644
index fec2a21..0000000
--- a/erpnext/restaurant/doctype/restaurant_order_entry/test_restaurant_order_entry.js
+++ /dev/null
@@ -1,53 +0,0 @@
-/* eslint-disable */
-// rename this file from _test_[name] to test_[name] to activate
-// and remove above this line
-
-QUnit.test("test: Restaurant Order Entry", function (assert) {
- let done = assert.async();
-
- // number of asserts
- assert.expect(5);
-
- frappe.run_serially([
- // insert a new Restaurant Order Entry
- () => frappe.set_route('Form', 'Restaurant Settings'),
- () => cur_frm.set_value('default_customer', 'Test Customer 1'),
- () => cur_frm.save(),
- () => frappe.set_route('Form', 'Restaurant Order Entry'),
- () => frappe.click_button('Clear'),
- () => frappe.timeout(2),
- () => cur_frm.set_value('restaurant_table', 'Test-Restaurant-1-01'),
- () => cur_frm.set_value('add_item', 'Food Item 1'),
- () => frappe.timeout(0.5),
- () => {
- var e = $.Event( "keyup", {which: 13} );
- $('input[data-fieldname="add_item"]').trigger(e);
- return frappe.timeout(0.5);
- },
- () => cur_frm.set_value('add_item', 'Food Item 1'),
- () => {
- var e = $.Event( "keyup", {which: 13} );
- $('input[data-fieldname="add_item"]').trigger(e);
- return frappe.timeout(0.5);
- },
- () => cur_frm.set_value('add_item', 'Food Item 2'),
- () => {
- var e = $.Event( "keyup", {which: 13} );
- $('input[data-fieldname="add_item"]').trigger(e);
- return frappe.timeout(0.5);
- },
- () => {
- assert.equal(cur_frm.doc.items[0].item, 'Food Item 1');
- assert.equal(cur_frm.doc.items[0].qty, 2);
- assert.equal(cur_frm.doc.items[1].item, 'Food Item 2');
- assert.equal(cur_frm.doc.items[1].qty, 1);
- },
- () => frappe.click_button('Update'),
- () => frappe.timeout(2),
- () => {
- assert.equal(cur_frm.doc.grand_total, 290);
- }
- () => done()
- ]);
-
-});
diff --git a/erpnext/restaurant/doctype/restaurant_reservation/test_restaurant_reservation.js b/erpnext/restaurant/doctype/restaurant_reservation/test_restaurant_reservation.js
deleted file mode 100644
index eeea5a9..0000000
--- a/erpnext/restaurant/doctype/restaurant_reservation/test_restaurant_reservation.js
+++ /dev/null
@@ -1,27 +0,0 @@
-/* eslint-disable */
-// rename this file from _test_[name] to test_[name] to activate
-// and remove above this line
-
-QUnit.test("test: Restaurant Reservation", function (assert) {
- let done = assert.async();
-
- // number of asserts
- assert.expect(1);
-
- frappe.run_serially([
- // insert a new Restaurant Reservation
- () => frappe.tests.make('Restaurant Reservation', [
- // values to be set
- {restaurant: 'Gokul - JP Nagar'},
- {customer_name: 'test customer'},
- {reservation_time: frappe.datetime.now_date() + " 19:00:00"},
- {no_of_people: 4},
- ]),
- () => {
- assert.equal(cur_frm.doc.reservation_end_time,
- frappe.datetime.now_date() + ' 20:00:00');
- },
- () => done()
- ]);
-
-});
diff --git a/erpnext/restaurant/doctype/restaurant_table/test_restaurant_table.js b/erpnext/restaurant/doctype/restaurant_table/test_restaurant_table.js
deleted file mode 100644
index 16035f0..0000000
--- a/erpnext/restaurant/doctype/restaurant_table/test_restaurant_table.js
+++ /dev/null
@@ -1,41 +0,0 @@
-/* eslint-disable */
-// rename this file from _test_[name] to test_[name] to activate
-// and remove above this line
-
-QUnit.test("test: Restaurant Table", function (assert) {
- let done = assert.async();
-
- // number of asserts
- assert.expect(0);
-
- frappe.run_serially([
- // insert a new Restaurant Table
- () => frappe.tests.make('Restaurant Table', [
- // values to be set
- {restaurant: 'Test Restaurant 1'},
- {no_of_seats: 4},
- ]),
- () => frappe.tests.make('Restaurant Table', [
- // values to be set
- {restaurant: 'Test Restaurant 1'},
- {no_of_seats: 5},
- ]),
- () => frappe.tests.make('Restaurant Table', [
- // values to be set
- {restaurant: 'Test Restaurant 1'},
- {no_of_seats: 2},
- ]),
- () => frappe.tests.make('Restaurant Table', [
- // values to be set
- {restaurant: 'Test Restaurant 1'},
- {no_of_seats: 2},
- ]),
- () => frappe.tests.make('Restaurant Table', [
- // values to be set
- {restaurant: 'Test Restaurant 1'},
- {no_of_seats: 6},
- ]),
- () => done()
- ]);
-
-});
diff --git a/erpnext/selling/doctype/customer/customer.js b/erpnext/selling/doctype/customer/customer.js
index 4b0bbd5..107e4a4 100644
--- a/erpnext/selling/doctype/customer/customer.js
+++ b/erpnext/selling/doctype/customer/customer.js
@@ -134,6 +134,12 @@
frm.trigger("get_customer_group_details");
}, __('Actions'));
+ if (cint(frappe.defaults.get_default("enable_common_party_accounting"))) {
+ frm.add_custom_button(__('Link with Supplier'), function () {
+ frm.trigger('show_party_link_dialog');
+ }, __('Actions'));
+ }
+
// indicator
erpnext.utils.set_party_dashboard_indicators(frm);
@@ -158,5 +164,42 @@
}
});
+ },
+ show_party_link_dialog: function(frm) {
+ const dialog = new frappe.ui.Dialog({
+ title: __('Select a Supplier'),
+ fields: [{
+ fieldtype: 'Link', label: __('Supplier'),
+ options: 'Supplier', fieldname: 'supplier', reqd: 1
+ }],
+ primary_action: function({ supplier }) {
+ frappe.call({
+ method: 'erpnext.accounts.doctype.party_link.party_link.create_party_link',
+ args: {
+ primary_role: 'Customer',
+ primary_party: frm.doc.name,
+ secondary_party: supplier
+ },
+ freeze: true,
+ callback: function() {
+ dialog.hide();
+ frappe.msgprint({
+ message: __('Successfully linked to Supplier'),
+ alert: true
+ });
+ },
+ error: function() {
+ dialog.hide();
+ frappe.msgprint({
+ message: __('Linking to Supplier Failed. Please try again.'),
+ title: __('Linking Failed'),
+ indicator: 'red'
+ });
+ }
+ });
+ },
+ primary_action_label: __('Create Link')
+ });
+ dialog.show();
}
});
diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py
index 3f519a6..40c8a37 100644
--- a/erpnext/selling/doctype/customer/customer.py
+++ b/erpnext/selling/doctype/customer/customer.py
@@ -18,7 +18,11 @@
from frappe.utils import cint, cstr, flt, get_formatted_email, today
from frappe.utils.user import get_users_with_role
-from erpnext.accounts.party import get_dashboard_info, validate_party_accounts
+from erpnext.accounts.party import ( # noqa
+ get_dashboard_info,
+ get_timeline_data,
+ validate_party_accounts,
+)
from erpnext.utilities.transaction_base import TransactionBase
@@ -192,20 +196,19 @@
if not lead.lead_name:
frappe.throw(_("Please mention the Lead Name in Lead {0}").format(self.lead_name))
- if lead.company_name:
- contact_names = frappe.get_all('Dynamic Link', filters={
- "parenttype":"Contact",
- "link_doctype":"Lead",
- "link_name":self.lead_name
- }, fields=["parent as name"])
+ contact_names = frappe.get_all('Dynamic Link', filters={
+ "parenttype":"Contact",
+ "link_doctype":"Lead",
+ "link_name":self.lead_name
+ }, fields=["parent as name"])
- for contact_name in contact_names:
- contact = frappe.get_doc('Contact', contact_name.get('name'))
- if not contact.has_link('Customer', self.name):
- contact.append('links', dict(link_doctype='Customer', link_name=self.name))
- contact.save(ignore_permissions=self.flags.ignore_permissions)
+ for contact_name in contact_names:
+ contact = frappe.get_doc('Contact', contact_name.get('name'))
+ if not contact.has_link('Customer', self.name):
+ contact.append('links', dict(link_doctype='Customer', link_name=self.name))
+ contact.save(ignore_permissions=self.flags.ignore_permissions)
- else:
+ if not contact_names:
lead.lead_name = lead.lead_name.lstrip().split(" ")
lead.first_name = lead.lead_name[0]
lead.last_name = " ".join(lead.lead_name[1:])
@@ -439,11 +442,14 @@
def check_credit_limit(customer, company, ignore_outstanding_sales_order=False, extra_amount=0):
+ credit_limit = get_credit_limit(customer, company)
+ if not credit_limit:
+ return
+
customer_outstanding = get_customer_outstanding(customer, company, ignore_outstanding_sales_order)
if extra_amount > 0:
customer_outstanding += flt(extra_amount)
- credit_limit = get_credit_limit(customer, company)
if credit_limit > 0 and flt(customer_outstanding) > credit_limit:
msgprint(_("Credit limit has been crossed for customer {0} ({1}/{2})")
.format(customer, customer_outstanding, credit_limit))
diff --git a/erpnext/selling/doctype/customer/test_customer.py b/erpnext/selling/doctype/customer/test_customer.py
index 7d6b74d..5301fd0 100644
--- a/erpnext/selling/doctype/customer/test_customer.py
+++ b/erpnext/selling/doctype/customer/test_customer.py
@@ -2,8 +2,6 @@
# License: GNU General Public License v3. See license.txt
-import unittest
-
import frappe
from frappe.test_runner import make_test_records
from frappe.utils import flt
@@ -11,7 +9,7 @@
from erpnext.accounts.party import get_due_date
from erpnext.exceptions import PartyDisabled, PartyFrozen
from erpnext.selling.doctype.customer.customer import get_credit_limit, get_customer_outstanding
-from erpnext.tests.utils import create_test_contact_and_address
+from erpnext.tests.utils import ERPNextTestCase, create_test_contact_and_address
test_ignore = ["Price List"]
test_dependencies = ['Payment Term', 'Payment Terms Template']
@@ -19,7 +17,7 @@
-class TestCustomer(unittest.TestCase):
+class TestCustomer(ERPNextTestCase):
def setUp(self):
if not frappe.get_value('Item', '_Test Item'):
make_test_records('Item')
diff --git a/erpnext/selling/doctype/party_specific_item/test_party_specific_item.py b/erpnext/selling/doctype/party_specific_item/test_party_specific_item.py
index 874a364..b951044 100644
--- a/erpnext/selling/doctype/party_specific_item/test_party_specific_item.py
+++ b/erpnext/selling/doctype/party_specific_item/test_party_specific_item.py
@@ -6,6 +6,7 @@
import frappe
from erpnext.controllers.queries import item_query
+from erpnext.tests.utils import ERPNextTestCase
test_dependencies = ['Item', 'Customer', 'Supplier']
@@ -17,7 +18,7 @@
psi.based_on_value = args.get('based_on_value')
psi.insert()
-class TestPartySpecificItem(unittest.TestCase):
+class TestPartySpecificItem(ERPNextTestCase):
def setUp(self):
self.customer = frappe.get_last_doc("Customer")
self.supplier = frappe.get_last_doc("Supplier")
diff --git a/erpnext/selling/doctype/product_bundle/test_product_bundle.js b/erpnext/selling/doctype/product_bundle/test_product_bundle.js
deleted file mode 100644
index 0dc90ec..0000000
--- a/erpnext/selling/doctype/product_bundle/test_product_bundle.js
+++ /dev/null
@@ -1,35 +0,0 @@
-QUnit.test("test sales order", function(assert) {
- assert.expect(4);
- let done = assert.async();
- frappe.run_serially([
- () => {
- return frappe.tests.make('Product Bundle', [
- {new_item_code: 'Computer'},
- {items: [
- [
- {item_code:'CPU'},
- {qty:1}
- ],
- [
- {item_code:'Screen'},
- {qty:1}
- ],
- [
- {item_code:'Keyboard'},
- {qty:1}
- ]
- ]},
- ]);
- },
- () => cur_frm.save(),
- () => {
- // get_item_details
- assert.ok(cur_frm.doc.items[0].item_code=='CPU', "Item Code correct");
- assert.ok(cur_frm.doc.items[1].item_code=='Screen', "Item Code correct");
- assert.ok(cur_frm.doc.items[2].item_code=='Keyboard', "Item Code correct");
- assert.ok(cur_frm.doc.new_item_code == "Computer", "Parent Item correct");
- },
- () => frappe.timeout(0.3),
- () => done()
- ]);
-});
diff --git a/erpnext/selling/doctype/quotation/quotation.json b/erpnext/selling/doctype/quotation/quotation.json
index ad788e5..ee5b0ea 100644
--- a/erpnext/selling/doctype/quotation/quotation.json
+++ b/erpnext/selling/doctype/quotation/quotation.json
@@ -961,9 +961,7 @@
"idx": 82,
"is_submittable": 1,
"links": [],
- "max_attachments": 1,
- "migration_hash": "75a86a19f062c2257bcbc8e6e31c7f1e",
- "modified": "2021-10-21 12:58:55.514512",
+ "modified": "2021-11-30 01:33:21.106073",
"modified_by": "Administrator",
"module": "Selling",
"name": "Quotation",
diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py
index a12fc55..eebde76 100644
--- a/erpnext/selling/doctype/quotation/quotation.py
+++ b/erpnext/selling/doctype/quotation/quotation.py
@@ -8,6 +8,7 @@
from frappe.utils import flt, getdate, nowdate
from erpnext.controllers.selling_controller import SellingController
+from erpnext.crm.utils import add_link_in_communication, copy_comments
form_grid_templates = {
"items": "templates/form_grid/item_grid.html"
@@ -34,6 +35,16 @@
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
make_packing_list(self)
+ def after_insert(self):
+ if frappe.db.get_single_value("CRM Settings", "carry_forward_communication_and_comments"):
+ if self.opportunity:
+ copy_comments("Opportunity", self.opportunity, self)
+ add_link_in_communication("Opportunity", self.opportunity, self)
+
+ elif self.quotation_to == "Lead" and self.party_name:
+ copy_comments("Lead", self.party_name, self)
+ add_link_in_communication("Lead", self.party_name, self)
+
def validate_valid_till(self):
if self.valid_till and getdate(self.valid_till) < getdate(self.transaction_date):
frappe.throw(_("Valid till date cannot be before transaction date"))
diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py
index 769e066..4357201 100644
--- a/erpnext/selling/doctype/quotation/test_quotation.py
+++ b/erpnext/selling/doctype/quotation/test_quotation.py
@@ -1,15 +1,15 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
-import unittest
-
import frappe
from frappe.utils import add_days, add_months, flt, getdate, nowdate
+from erpnext.tests.utils import ERPNextTestCase
+
test_dependencies = ["Product Bundle"]
-class TestQuotation(unittest.TestCase):
+class TestQuotation(ERPNextTestCase):
def test_make_quotation_without_terms(self):
quotation = make_quotation(do_not_save=1)
self.assertFalse(quotation.get('payment_schedule'))
@@ -302,6 +302,109 @@
enable_calculate_bundle_price(enable=0)
+ def test_product_bundle_price_calculation_for_multiple_product_bundles_when_calculate_bundle_price_is_checked(self):
+ from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
+ from erpnext.stock.doctype.item.test_item import make_item
+
+ make_item("_Test Product Bundle 1", {"is_stock_item": 0})
+ make_item("_Test Product Bundle 2", {"is_stock_item": 0})
+ make_item("_Test Bundle Item 1", {"is_stock_item": 1})
+ make_item("_Test Bundle Item 2", {"is_stock_item": 1})
+ make_item("_Test Bundle Item 3", {"is_stock_item": 1})
+
+ make_product_bundle("_Test Product Bundle 1",
+ ["_Test Bundle Item 1", "_Test Bundle Item 2"])
+ make_product_bundle("_Test Product Bundle 2",
+ ["_Test Bundle Item 2", "_Test Bundle Item 3"])
+
+ enable_calculate_bundle_price()
+
+ item_list = [
+ {
+ "item_code": "_Test Product Bundle 1",
+ "warehouse": "",
+ "qty": 1,
+ "rate": 400,
+ "delivered_by_supplier": 1,
+ "supplier": '_Test Supplier'
+ },
+ {
+ "item_code": "_Test Product Bundle 2",
+ "warehouse": "",
+ "qty": 1,
+ "rate": 400,
+ "delivered_by_supplier": 1,
+ "supplier": '_Test Supplier'
+ }
+ ]
+
+ quotation = make_quotation(item_list=item_list, do_not_submit=1)
+ quotation.packed_items[0].rate = 100
+ quotation.packed_items[1].rate = 200
+ quotation.packed_items[2].rate = 200
+ quotation.packed_items[3].rate = 300
+ quotation.save()
+
+ expected_values = [300, 500]
+
+ for item in quotation.items:
+ self.assertEqual(item.amount, expected_values[item.idx-1])
+
+ enable_calculate_bundle_price(enable=0)
+
+ def test_packed_items_indices_are_reset_when_product_bundle_is_deleted_from_items_table(self):
+ from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
+ from erpnext.stock.doctype.item.test_item import make_item
+
+ make_item("_Test Product Bundle 1", {"is_stock_item": 0})
+ make_item("_Test Product Bundle 2", {"is_stock_item": 0})
+ make_item("_Test Product Bundle 3", {"is_stock_item": 0})
+ make_item("_Test Bundle Item 1", {"is_stock_item": 1})
+ make_item("_Test Bundle Item 2", {"is_stock_item": 1})
+ make_item("_Test Bundle Item 3", {"is_stock_item": 1})
+
+ make_product_bundle("_Test Product Bundle 1",
+ ["_Test Bundle Item 1", "_Test Bundle Item 2"])
+ make_product_bundle("_Test Product Bundle 2",
+ ["_Test Bundle Item 2", "_Test Bundle Item 3"])
+ make_product_bundle("_Test Product Bundle 3",
+ ["_Test Bundle Item 3", "_Test Bundle Item 1"])
+
+ item_list = [
+ {
+ "item_code": "_Test Product Bundle 1",
+ "warehouse": "",
+ "qty": 1,
+ "rate": 400,
+ "delivered_by_supplier": 1,
+ "supplier": '_Test Supplier'
+ },
+ {
+ "item_code": "_Test Product Bundle 2",
+ "warehouse": "",
+ "qty": 1,
+ "rate": 400,
+ "delivered_by_supplier": 1,
+ "supplier": '_Test Supplier'
+ },
+ {
+ "item_code": "_Test Product Bundle 3",
+ "warehouse": "",
+ "qty": 1,
+ "rate": 400,
+ "delivered_by_supplier": 1,
+ "supplier": '_Test Supplier'
+ }
+ ]
+
+ quotation = make_quotation(item_list=item_list, do_not_submit=1)
+ del quotation.items[1]
+ quotation.save()
+
+ for id, item in enumerate(quotation.packed_items):
+ expected_index = id + 1
+ self.assertEqual(item.idx, expected_index)
+
test_records = frappe.get_test_records('Quotation')
def enable_calculate_bundle_price(enable=1):
diff --git a/erpnext/selling/doctype/quotation/tests/test_quotation.js b/erpnext/selling/doctype/quotation/tests/test_quotation.js
deleted file mode 100644
index ad942fe..0000000
--- a/erpnext/selling/doctype/quotation/tests/test_quotation.js
+++ /dev/null
@@ -1,58 +0,0 @@
-QUnit.test("test: quotation", function (assert) {
- assert.expect(12);
- let done = assert.async();
- frappe.run_serially([
- () => {
- return frappe.tests.make("Quotation", [
- {customer: "Test Customer 1"},
- {items: [
- [
- {"item_code": "Test Product 1"},
- {"qty": 5}
- ]]
- },
- {payment_terms_template: '_Test Payment Term Template UI'}
- ]);
- },
- () => {
- // get_item_details
- assert.ok(cur_frm.doc.items[0].item_name == "Test Product 1", "Added Test Product 1");
-
- // calculate_taxes_and_totals
- assert.ok(cur_frm.doc.grand_total === 500, String(cur_frm.doc.grand_total));
- },
- () => cur_frm.set_value("customer_address", "Test1-Billing"),
- () => cur_frm.set_value("shipping_address_name", "Test1-Warehouse"),
- () => cur_frm.set_value("contact_person", "Contact 1-Test Customer 1"),
- () => cur_frm.set_value("currency", "USD"),
- () => frappe.timeout(0.3),
- () => cur_frm.set_value("selling_price_list", "Test-Selling-USD"),
- () => frappe.timeout(0.5),
- () => cur_frm.doc.items[0].rate = 200,
- () => frappe.timeout(0.3),
- () => cur_frm.set_value("tc_name", "Test Term 1"),
- () => cur_frm.set_value("payment_schedule", []),
- () => frappe.timeout(0.5),
- () => cur_frm.save(),
- () => {
- // Check Address and Contact Info
- assert.ok(cur_frm.doc.address_display.includes("Billing Street 1"), "Address Changed");
- assert.ok(cur_frm.doc.shipping_address.includes("Warehouse Street 1"), "Address Changed");
- assert.ok(cur_frm.doc.contact_display == "Contact 1", "Contact info changed");
-
- // Check Currency
- assert.ok(cur_frm.doc.currency == "USD", "Currency Changed");
- assert.ok(cur_frm.doc.selling_price_list == "Test-Selling-USD", "Price List Changed");
- assert.ok(cur_frm.doc.items[0].rate == 200, "Price Changed Manually");
- assert.equal(cur_frm.doc.total, 1000, "New Total Calculated");
-
- // Check Terms and Conditions
- assert.ok(cur_frm.doc.tc_name == "Test Term 1", "Terms and Conditions Checked");
-
- assert.ok(cur_frm.doc.payment_terms_template, "Payment Terms Template is correct");
- assert.ok(cur_frm.doc.payment_schedule.length > 0, "Payment Term Schedule is not empty");
-
- },
- () => done()
- ]);
-});
diff --git a/erpnext/selling/doctype/quotation/tests/test_quotation_submit_cancel_amend.js b/erpnext/selling/doctype/quotation/tests/test_quotation_submit_cancel_amend.js
deleted file mode 100644
index 26a099e..0000000
--- a/erpnext/selling/doctype/quotation/tests/test_quotation_submit_cancel_amend.js
+++ /dev/null
@@ -1,41 +0,0 @@
-QUnit.module('Quotation');
-
-QUnit.test("test quotation submit cancel amend", function(assert) {
- assert.expect(2);
- let done = assert.async();
- frappe.run_serially([
- () => {
- return frappe.tests.make('Quotation', [
- {customer: 'Test Customer 1'},
- {items: [
- [
- {'delivery_date': frappe.datetime.add_days(frappe.defaults.get_default("year_end_date"), 1)},
- {'qty': 5},
- {'item_code': 'Test Product 1'}
- ]
- ]},
- {customer_address: 'Test1-Billing'},
- {shipping_address_name: 'Test1-Shipping'},
- {contact_person: 'Contact 1-Test Customer 1'}
- ]);
- },
- () => cur_frm.save(),
- () => {
- // get_item_details
- assert.ok(cur_frm.doc.items[0].item_name=='Test Product 1', "Item name correct");
- // get uom details
- assert.ok(cur_frm.doc.grand_total== 500, "Grand total correct ");
-
- },
- () => frappe.tests.click_button('Submit'),
- () => frappe.tests.click_button('Yes'),
- () => frappe.timeout(1),
- () => frappe.tests.click_button('Close'),
- () => frappe.tests.click_button('Cancel'),
- () => frappe.tests.click_button('Yes'),
- () => frappe.timeout(0.5),
- () => frappe.tests.click_button('Amend'),
- () => cur_frm.save(),
- () => done()
- ]);
-});
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
deleted file mode 100644
index b59bb05..0000000
--- a/erpnext/selling/doctype/quotation/tests/test_quotation_with_discount_on_grand_total.js
+++ /dev/null
@@ -1,43 +0,0 @@
-QUnit.module('Quotation');
-
-QUnit.test("test quotation with additional discount in grand total", function(assert) {
- assert.expect(2);
- let done = assert.async();
- frappe.run_serially([
- () => {
- return frappe.tests.make('Quotation', [
- {customer: 'Test Customer 1'},
- {items: [
- [
- {'delivery_date': frappe.datetime.add_days(frappe.defaults.get_default("year_end_date"), 1)},
- {'qty': 5},
- {'item_code': 'Test Product 4'},
- ]
- ]},
- {customer_address: 'Test1-Billing'},
- {shipping_address_name: 'Test1-Shipping'},
- {contact_person: 'Contact 1-Test Customer 1'},
- {payment_terms_template: '_Test Payment Term Template UI'}
- ]);
- },
- () => {
- return frappe.tests.set_form_values(cur_frm, [
- {apply_discount_on:'Grand Total'},
- {additional_discount_percentage:10},
- {payment_schedule: []}
- ]);
- },
- () => cur_frm.save(),
- () => {
- // get_item_details
- assert.ok(cur_frm.doc.items[0].item_name=='Test Product 4', "Item name correct");
- // get grand_total details
- assert.ok(cur_frm.doc.grand_total== 450, "Grand total correct ");
-
- },
- () => frappe.tests.click_button('Submit'),
- () => frappe.tests.click_button('Yes'),
- () => frappe.timeout(0.3),
- () => 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
deleted file mode 100644
index f5172fb..0000000
--- a/erpnext/selling/doctype/quotation/tests/test_quotation_with_item_wise_discount.js
+++ /dev/null
@@ -1,37 +0,0 @@
-QUnit.module('Quotation');
-
-QUnit.test("test quotation with item wise discount", function(assert) {
- assert.expect(2);
- let done = assert.async();
- frappe.run_serially([
- () => {
- return frappe.tests.make('Quotation', [
- {customer: 'Test Customer 1'},
- {items: [
- [
- {'delivery_date': frappe.datetime.add_days(frappe.defaults.get_default("year_end_date"), 1)},
- {'qty': 5},
- {'item_code': 'Test Product 4'},
- {'discount_percentage': 10},
- {'margin_type': 'Percentage'}
- ]
- ]},
- {customer_address: 'Test1-Billing'},
- {shipping_address_name: 'Test1-Shipping'},
- {contact_person: 'Contact 1-Test Customer 1'}
- ]);
- },
- () => cur_frm.save(),
- () => {
- // get_item_details
- assert.ok(cur_frm.doc.items[0].item_name=='Test Product 4', "Item name correct");
- // get grand_total details
- assert.ok(cur_frm.doc.grand_total== 450, "Grand total correct ");
-
- },
- () => frappe.tests.click_button('Submit'),
- () => frappe.tests.click_button('Yes'),
- () => frappe.timeout(0.3),
- () => 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
deleted file mode 100644
index 0d34099..0000000
--- a/erpnext/selling/doctype/quotation/tests/test_quotation_with_margin.js
+++ /dev/null
@@ -1,35 +0,0 @@
-QUnit.module('Selling');
-
-QUnit.test("test quotation with margin", function(assert) {
- assert.expect(3);
- let done = assert.async();
- frappe.run_serially([
- () => {
- return frappe.tests.make('Quotation', [
- {customer: 'Test Customer 1'},
- {selling_price_list: 'Test-Selling-USD'},
- {currency: 'USD'},
- {items: [
- [
- {'item_code': 'Test Product 4'},
- {'delivery_date': frappe.datetime.add_days(frappe.defaults.get_default("year_end_date"), 1)},
- {'qty': 1},
- {'margin_type': 'Percentage'},
- {'margin_rate_or_amount': 20}
- ]
- ]}
- ]);
- },
- () => cur_frm.save(),
- () => {
- assert.ok(cur_frm.doc.items[0].rate_with_margin == 240, "Margin rate correct");
- assert.ok(cur_frm.doc.items[0].base_rate_with_margin == cur_frm.doc.conversion_rate * 240, "Base margin rate correct");
- assert.ok(cur_frm.doc.total == 240, "Amount correct");
-
- },
- () => frappe.tests.click_button('Submit'),
- () => frappe.tests.click_button('Yes'),
- () => frappe.timeout(0.3),
- () => 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
deleted file mode 100644
index 84be56f..0000000
--- a/erpnext/selling/doctype/quotation/tests/test_quotation_with_multi_uom.js
+++ /dev/null
@@ -1,38 +0,0 @@
-QUnit.module('Quotation');
-
-QUnit.test("test quotation with multi uom", function(assert) {
- assert.expect(3);
- let done = assert.async();
- frappe.run_serially([
- () => {
- return frappe.tests.make('Quotation', [
- {customer: 'Test Customer 1'},
- {items: [
- [
- {'delivery_date': frappe.datetime.add_days(frappe.defaults.get_default("year_end_date"), 1)},
- {'qty': 5},
- {'item_code': 'Test Product 4'},
- {'uom': 'unit'},
- ]
- ]},
- {customer_address: 'Test1-Billing'},
- {shipping_address_name: 'Test1-Shipping'},
- {contact_person: 'Contact 1-Test Customer 1'}
- ]);
- },
- () => cur_frm.save(),
- () => {
- // get_item_details
- assert.ok(cur_frm.doc.items[0].item_name=='Test Product 4', "Item name correct");
- // get uom details
- assert.ok(cur_frm.doc.items[0].uom=='Unit', "Multi Uom correct");
- // get grand_total details
- assert.ok(cur_frm.doc.grand_total== 5000, "Grand total correct ");
-
- },
- () => frappe.tests.click_button('Submit'),
- () => frappe.tests.click_button('Yes'),
- () => frappe.timeout(0.3),
- () => done()
- ]);
-});
diff --git a/erpnext/selling/doctype/quotation/tests/test_quotation_with_shipping_rule.js b/erpnext/selling/doctype/quotation/tests/test_quotation_with_shipping_rule.js
deleted file mode 100644
index 17c5dd2..0000000
--- a/erpnext/selling/doctype/quotation/tests/test_quotation_with_shipping_rule.js
+++ /dev/null
@@ -1,35 +0,0 @@
-QUnit.module('Quotation');
-
-QUnit.test("test quotation with shipping rule", function(assert) {
- assert.expect(2);
- let done = assert.async();
- frappe.run_serially([
- () => {
- return frappe.tests.make('Quotation', [
- {customer: 'Test Customer 1'},
- {items: [
- [
- {'delivery_date': frappe.datetime.add_days(frappe.defaults.get_default("year_end_date"), 1)},
- {'qty': 5},
- {'item_code': 'Test Product 4'},
- ]
- ]},
- {customer_address: 'Test1-Billing'},
- {shipping_address_name: 'Test1-Shipping'},
- {contact_person: 'Contact 1-Test Customer 1'},
- {shipping_rule:'Next Day Shipping'}
- ]);
- },
- () => cur_frm.save(),
- () => {
- // get_item_details
- assert.ok(cur_frm.doc.items[0].item_name=='Test Product 4', "Item name correct");
- // get grand_total details
- assert.ok(cur_frm.doc.grand_total== 550, "Grand total correct ");
- },
- () => frappe.tests.click_button('Submit'),
- () => frappe.tests.click_button('Yes'),
- () => frappe.timeout(0.3),
- () => 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
deleted file mode 100644
index 5e21f81..0000000
--- a/erpnext/selling/doctype/quotation/tests/test_quotation_with_taxes_and_charges.js
+++ /dev/null
@@ -1,40 +0,0 @@
-QUnit.module('Quotation');
-
-QUnit.test("test quotation with taxes and charges", function(assert) {
- assert.expect(3);
- let done = assert.async();
- frappe.run_serially([
- () => {
- return frappe.tests.make('Quotation', [
- {customer: 'Test Customer 1'},
- {items: [
- [
- {'delivery_date': frappe.datetime.add_days(frappe.defaults.get_default("year_end_date"), 1)},
- {'qty': 5},
- {'item_code': 'Test Product 4'},
- ]
- ]},
- {customer_address: 'Test1-Billing'},
- {shipping_address_name: 'Test1-Shipping'},
- {contact_person: 'Contact 1-Test Customer 1'},
- {taxes_and_charges: 'TEST In State GST - FT'},
- {tc_name: 'Test Term 1'},
- {terms: 'This is Test'}
- ]);
- },
- () => cur_frm.save(),
- () => {
- // get_item_details
- assert.ok(cur_frm.doc.items[0].item_name=='Test Product 4', "Item name correct");
- // get tax details
- assert.ok(cur_frm.doc.taxes_and_charges=='TEST In State GST - FT', "Tax details correct");
- // get tax account head details
- assert.ok(cur_frm.doc.taxes[0].account_head=='CGST - '+frappe.get_abbr(frappe.defaults.get_default('Company')), " Account Head abbr correct");
-
- },
- () => frappe.tests.click_button('Submit'),
- () => frappe.tests.click_button('Yes'),
- () => frappe.timeout(0.3),
- () => done()
- ]);
-});
diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json
index 7c7ed9a..7e99a06 100644
--- a/erpnext/selling/doctype/sales_order/sales_order.json
+++ b/erpnext/selling/doctype/sales_order/sales_order.json
@@ -134,6 +134,7 @@
"sales_team_section_break",
"sales_partner",
"column_break7",
+ "amount_eligible_for_commission",
"commission_rate",
"total_commission",
"section_break1",
@@ -1507,16 +1508,23 @@
"fieldtype": "Small Text",
"label": "Dispatch Address",
"read_only": 1
+ },
+ {
+ "fieldname": "amount_eligible_for_commission",
+ "fieldtype": "Currency",
+ "label": "Amount Eligible for Commission",
+ "read_only": 1
}
],
"icon": "fa fa-file-text",
"idx": 105,
"is_submittable": 1,
"links": [],
- "modified": "2021-09-28 13:09:51.515542",
+ "modified": "2021-10-05 12:16:40.775704",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order",
+ "naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index 47b8ebd..cc95185 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -63,6 +63,8 @@
if not self.billing_status: self.billing_status = 'Not Billed'
if not self.delivery_status: self.delivery_status = 'Not Delivered'
+ self.reset_default_field_value("set_warehouse", "items", "warehouse")
+
def validate_po(self):
# validate p.o date v/s delivery date
if self.po_date and not self.skip_delivery_note:
@@ -925,6 +927,7 @@
"supplier",
"pricing_rules"
],
+ "condition": lambda doc: doc.parent_item in items_to_map
}
}, target_doc, set_missing_values)
@@ -977,6 +980,7 @@
description=i['description']
)).insert()
work_order.set_work_order_operations()
+ work_order.flags.ignore_mandatory = True
work_order.save()
out.append(work_order)
diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py
index 2a0752e..42bc0b7 100644
--- a/erpnext/selling/doctype/sales_order/test_sales_order.py
+++ b/erpnext/selling/doctype/sales_order/test_sales_order.py
@@ -2,7 +2,6 @@
# License: GNU General Public License v3. See license.txt
import json
-import unittest
import frappe
import frappe.permissions
@@ -28,12 +27,14 @@
)
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
+from erpnext.tests.utils import ERPNextTestCase
-class TestSalesOrder(unittest.TestCase):
+class TestSalesOrder(ERPNextTestCase):
@classmethod
def setUpClass(cls):
+ super().setUpClass()
cls.unlink_setting = int(frappe.db.get_value("Accounts Settings", "Accounts Settings",
"unlink_advance_payment_on_cancelation_of_order"))
@@ -42,6 +43,7 @@
# reset config to previous state
frappe.db.set_value("Accounts Settings", "Accounts Settings",
"unlink_advance_payment_on_cancelation_of_order", cls.unlink_setting)
+ super().tearDownClass()
def tearDown(self):
frappe.set_user("Administrator")
diff --git a/erpnext/selling/doctype/sales_order/tests/test_sales_order.js b/erpnext/selling/doctype/sales_order/tests/test_sales_order.js
deleted file mode 100644
index c99f9ef..0000000
--- a/erpnext/selling/doctype/sales_order/tests/test_sales_order.js
+++ /dev/null
@@ -1,68 +0,0 @@
-QUnit.module('Sales Order');
-
-QUnit.test("test sales order", function(assert) {
- assert.expect(12);
- let done = assert.async();
- frappe.run_serially([
- () => {
- return frappe.tests.make('Sales Order', [
- {customer: 'Test Customer 1'},
- {items: [
- [
- {'delivery_date': frappe.datetime.add_days(frappe.defaults.get_default("year_end_date"), 1)},
- {'qty': 5.123},
- {'item_code': 'Test Product 3'},
- ]
- ]},
- {customer_address: 'Test1-Billing'},
- {shipping_address_name: 'Test1-Shipping'},
- {contact_person: 'Contact 1-Test Customer 1'},
- {taxes_and_charges: 'TEST In State GST - FT'},
- {tc_name: 'Test Term 1'},
- {terms: 'This is Test'},
- {payment_terms_template: '_Test Payment Term Template UI'}
- ]);
- },
- () => {
- return frappe.tests.set_form_values(cur_frm, [
- {selling_price_list:'Test-Selling-USD'},
- {currency: 'USD'}
- ]);
- },
- () => frappe.timeout(1.5),
- () => {
- // get_item_details
- assert.ok(cur_frm.doc.items[0].item_name=='Test Product 3', "Item name correct");
- // get tax details
- assert.ok(cur_frm.doc.taxes_and_charges=='TEST In State GST - FT', "Tax details correct");
- // get tax account head details
- assert.ok(cur_frm.doc.taxes[0].account_head=='CGST - '+frappe.get_abbr(frappe.defaults.get_default('Company')), " Account Head abbr correct");
- },
- () => cur_frm.save(),
- () => frappe.timeout(1),
- () => cur_frm.print_doc(),
- () => frappe.timeout(1),
- () => {
- // Payment Terms
- assert.ok(cur_frm.doc.payment_terms_template, "Payment Terms Template is correct");
- assert.ok(cur_frm.doc.payment_schedule.length > 0, "Payment Term Schedule is not empty");
-
- // totals
- assert.ok(cur_frm.doc.items[0].price_list_rate==250, "Item 1 price_list_rate");
- assert.ok(cur_frm.doc.net_total== 1280.75, "net total correct ");
- assert.ok(cur_frm.doc.base_grand_total== flt(1511.29* cur_frm.doc.conversion_rate, precision('base_grand_total')), String(flt(1511.29* cur_frm.doc.conversion_rate, precision('base_grand_total')) + ' ' + cur_frm.doc.base_grand_total));
- assert.ok(cur_frm.doc.grand_total== 1511.29 , "grand total correct ");
- assert.ok(cur_frm.doc.rounded_total== 1511.30, "rounded total correct ");
-
- // print format
- assert.ok($('.btn-print-print').is(':visible'), "Print Format Available");
- frappe.timeout(1);
- assert.ok($(".section-break+ .section-break .column-break:nth-child(1) .data-field:nth-child(1) .value").text().includes("Billing Street 1"), "Print Preview Works As Expected");
- },
- () => cur_frm.print_doc(),
- () => frappe.tests.click_button('Submit'),
- () => frappe.tests.click_button('Yes'),
- () => frappe.timeout(0.3),
- () => done()
- ]);
-});
diff --git a/erpnext/selling/doctype/sales_order/tests/test_sales_order_with_bypass_credit_limit_check.js b/erpnext/selling/doctype/sales_order/tests/test_sales_order_with_bypass_credit_limit_check.js
deleted file mode 100644
index 79d798b..0000000
--- a/erpnext/selling/doctype/sales_order/tests/test_sales_order_with_bypass_credit_limit_check.js
+++ /dev/null
@@ -1,58 +0,0 @@
-QUnit.module('Sales Order');
-
-QUnit.test("test_sales_order_with_bypass_credit_limit_check", function(assert) {
-//#PR : 10861, Author : ashish-greycube & jigneshpshah, Email:mr.ashish.shah@gmail.com
- assert.expect(2);
- let done = assert.async();
- frappe.run_serially([
- () => frappe.new_doc('Customer'),
- () => frappe.timeout(1),
- () => frappe.quick_entry.dialog.$wrapper.find('.edit-full').click(),
- () => frappe.timeout(1),
- () => cur_frm.set_value("customer_name", "Test Customer 10"),
- () => cur_frm.add_child('credit_limits', {
- 'company': cur_frm.doc.company || '_Test Company'
- 'credit_limit': 1000,
- 'bypass_credit_limit_check': 1}),
- // save form
- () => cur_frm.save(),
- () => frappe.timeout(1),
-
- () => frappe.new_doc('Item'),
- () => frappe.timeout(1),
- () => frappe.quick_entry.dialog.$wrapper.find('.edit-full').click(),
- () => frappe.timeout(1),
- () => cur_frm.set_value("item_code", "Test Product 10"),
- () => cur_frm.set_value("item_group", "Products"),
- () => cur_frm.set_value("standard_rate", 100),
- // save form
- () => cur_frm.save(),
- () => frappe.timeout(1),
-
- () => {
- return frappe.tests.make('Sales Order', [
- {customer: 'Test Customer 5'},
- {items: [
- [
- {'delivery_date': frappe.datetime.add_days(frappe.defaults.get_default("year_end_date"), 1)},
- {'qty': 5},
- {'item_code': 'Test Product 10'},
- ]
- ]}
-
- ]);
- },
- () => cur_frm.save(),
- () => frappe.tests.click_button('Submit'),
- () => assert.equal("Confirm", cur_dialog.title,'confirmation for submit'),
- () => frappe.tests.click_button('Yes'),
- () => frappe.timeout(3),
- () => {
-
- assert.ok(cur_frm.doc.status=="To Deliver and Bill", "It is submited. Credit limit is NOT checked for sales order");
-
-
- },
- () => done()
- ]);
-});
diff --git a/erpnext/selling/doctype/sales_order/tests/test_sales_order_with_discount_on_grand_total.js b/erpnext/selling/doctype/sales_order/tests/test_sales_order_with_discount_on_grand_total.js
deleted file mode 100644
index de61a61..0000000
--- a/erpnext/selling/doctype/sales_order/tests/test_sales_order_with_discount_on_grand_total.js
+++ /dev/null
@@ -1,43 +0,0 @@
-QUnit.module('Sales Order');
-
-QUnit.test("test sales order with additional discount in grand total", function(assert) {
- assert.expect(2);
- let done = assert.async();
- frappe.run_serially([
- () => {
- return frappe.tests.make('Sales Order', [
- {customer: 'Test Customer 1'},
- {items: [
- [
- {'delivery_date': frappe.datetime.add_days(frappe.defaults.get_default("year_end_date"), 1)},
- {'qty': 5},
- {'item_code': 'Test Product 4'},
- ]
- ]},
- {customer_address: 'Test1-Billing'},
- {shipping_address_name: 'Test1-Shipping'},
- {contact_person: 'Contact 1-Test Customer 1'},
- {payment_terms_template: '_Test Payment Term Template UI'}
- ]);
- },
- () => {
- return frappe.tests.set_form_values(cur_frm, [
- {apply_discount_on:'Grand Total'},
- {additional_discount_percentage:10},
- {payment_schedule: []}
- ]);
- },
- () => cur_frm.save(),
- () => {
- // get_item_details
- assert.ok(cur_frm.doc.items[0].item_name=='Test Product 4', "Item name correct");
- // get grand_total details
- assert.ok(cur_frm.doc.grand_total== 450, "Grand total correct ");
-
- },
- () => frappe.tests.click_button('Submit'),
- () => frappe.tests.click_button('Yes'),
- () => frappe.timeout(0.3),
- () => done()
- ]);
-});
diff --git a/erpnext/selling/doctype/sales_order/tests/test_sales_order_with_item_wise_discount.js b/erpnext/selling/doctype/sales_order/tests/test_sales_order_with_item_wise_discount.js
deleted file mode 100644
index 2c48108..0000000
--- a/erpnext/selling/doctype/sales_order/tests/test_sales_order_with_item_wise_discount.js
+++ /dev/null
@@ -1,38 +0,0 @@
-QUnit.module('Sales Order');
-
-QUnit.test("test sales order", function(assert) {
- assert.expect(2);
- let done = assert.async();
- frappe.run_serially([
- () => {
- return frappe.tests.make('Sales Order', [
- {customer: 'Test Customer 1'},
- {items: [
- [
- {'delivery_date': frappe.datetime.add_days(frappe.defaults.get_default("year_end_date"), 1)},
- {'qty': 5},
- {'item_code': 'Test Product 4'},
- {'discount_percentage': 10},
- {'margin_type': 'Percentage'}
- ]
- ]},
- {customer_address: 'Test1-Billing'},
- {shipping_address_name: 'Test1-Shipping'},
- {contact_person: 'Contact 1-Test Customer 1'},
- {payment_terms_template: '_Test Payment Term Template UI'}
- ]);
- },
- () => cur_frm.save(),
- () => {
- // get_item_details
- assert.ok(cur_frm.doc.items[0].item_name=='Test Product 4', "Item name correct");
- // get grand_total details
- assert.ok(cur_frm.doc.grand_total== 450, "Grand total correct ");
-
- },
- () => frappe.tests.click_button('Submit'),
- () => frappe.tests.click_button('Yes'),
- () => frappe.timeout(0.3),
- () => done()
- ]);
-});
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
deleted file mode 100644
index 9eebfda..0000000
--- a/erpnext/selling/doctype/sales_order/tests/test_sales_order_with_margin.js
+++ /dev/null
@@ -1,37 +0,0 @@
-QUnit.module('Selling');
-
-QUnit.test("test sales order with margin", function(assert) {
- assert.expect(3);
- let done = assert.async();
- frappe.run_serially([
- () => {
- return frappe.tests.make('Sales Order', [
- {customer:'Test Customer 1'},
- {selling_price_list: 'Test-Selling-USD'},
- {currency: 'USD'},
- {items: [
- [
- {'item_code': 'Test Product 4'},
- {'delivery_date': frappe.datetime.add_days(frappe.defaults.get_default("year_end_date"), 1)},
- {'qty': 1},
- {'margin_type': 'Amount'},
- {'margin_rate_or_amount': 20}
- ]
- ]},
- ]);
- },
-
- () => cur_frm.save(),
- () => {
- // get_rate_details
- assert.ok(cur_frm.doc.items[0].rate_with_margin == 220, "Margin rate correct");
- assert.ok(cur_frm.doc.items[0].base_rate_with_margin == cur_frm.doc.conversion_rate * 220, "Base margin rate correct");
- assert.ok(cur_frm.doc.total == 220, "Amount correct");
- },
-
- () => frappe.tests.click_button('Submit'),
- () => frappe.tests.click_button('Yes'),
- () => frappe.timeout(0.3),
- () => done()
- ]);
-});
diff --git a/erpnext/selling/doctype/sales_order/tests/test_sales_order_with_multi_uom.js b/erpnext/selling/doctype/sales_order/tests/test_sales_order_with_multi_uom.js
deleted file mode 100644
index 84301f5..0000000
--- a/erpnext/selling/doctype/sales_order/tests/test_sales_order_with_multi_uom.js
+++ /dev/null
@@ -1,38 +0,0 @@
-QUnit.module('Sales Order');
-
-QUnit.test("test sales order", function(assert) {
- assert.expect(3);
- let done = assert.async();
- frappe.run_serially([
- () => {
- return frappe.tests.make('Sales Order', [
- {customer: 'Test Customer 1'},
- {items: [
- [
- {'delivery_date': frappe.datetime.add_days(frappe.defaults.get_default("year_end_date"), 1)},
- {'qty': 5},
- {'item_code': 'Test Product 4'},
- {'uom': 'Unit'},
- ]
- ]},
- {customer_address: 'Test1-Billing'},
- {shipping_address_name: 'Test1-Shipping'},
- {contact_person: 'Contact 1-Test Customer 1'}
- ]);
- },
- () => cur_frm.save(),
- () => {
- // get_item_details
- assert.ok(cur_frm.doc.items[0].item_name=='Test Product 4', "Item name correct");
- // get uom details
- assert.ok(cur_frm.doc.items[0].uom=='Unit', "Multi Uom correct");
- // get grand_total details
- assert.ok(cur_frm.doc.grand_total== 5000, "Grand total correct ");
-
- },
- () => frappe.tests.click_button('Submit'),
- () => frappe.tests.click_button('Yes'),
- () => frappe.timeout(0.3),
- () => 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
deleted file mode 100644
index be76c49..0000000
--- a/erpnext/selling/doctype/sales_order/tests/test_sales_order_with_multiple_delivery_date.js
+++ /dev/null
@@ -1,59 +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) {
- assert.expect(2);
- let done = assert.async();
- let delivery_date = frappe.datetime.add_days(frappe.defaults.get_default("year_end_date"), 1);
-
- frappe.run_serially([
- // insert a new Sales Order
- () => {
- return frappe.tests.make('Sales Order', [
- {customer: "Test Customer 1"},
- {delivery_date: delivery_date},
- {order_type: 'Sales'},
- {items: [
- [
- {"item_code": "Test Product 1"},
- {"qty": 5},
- {'rate': 100},
- ]]
- }
- ])
- },
- () => {
- assert.ok(cur_frm.doc.items[0].delivery_date == delivery_date);
- },
- () => frappe.timeout(1),
- // make SO without delivery date in parent,
- // parent delivery date should be set based on final delivery date entered in item
- () => {
- return frappe.tests.make('Sales Order', [
- {customer: "Test Customer 1"},
- {order_type: 'Sales'},
- {items: [
- [
- {"item_code": "Test Product 1"},
- {"qty": 5},
- {'rate': 100},
- {'delivery_date': delivery_date}
- ],
- [
- {"item_code": "Test Product 2"},
- {"qty": 5},
- {'rate': 100},
- {'delivery_date': frappe.datetime.add_days(delivery_date, 5)}
- ]]
- }
- ])
- },
- () => cur_frm.save(),
- () => frappe.timeout(1),
- () => {
- assert.ok(cur_frm.doc.delivery_date == frappe.datetime.add_days(delivery_date, 5));
- },
- () => done()
- ]);
-});
diff --git a/erpnext/selling/doctype/sales_order/tests/test_sales_order_with_pricing_rule.js b/erpnext/selling/doctype/sales_order/tests/test_sales_order_with_pricing_rule.js
deleted file mode 100644
index e91fb01..0000000
--- a/erpnext/selling/doctype/sales_order/tests/test_sales_order_with_pricing_rule.js
+++ /dev/null
@@ -1,34 +0,0 @@
-QUnit.module('Sales Order');
-
-QUnit.test("test sales order with shipping rule", function(assert) {
- assert.expect(2);
- let done = assert.async();
- frappe.run_serially([
- () => {
- return frappe.tests.make('Sales Order', [
- {customer: 'Test Customer 3'},
- {items: [
- [
- {'delivery_date': frappe.datetime.add_days(frappe.defaults.get_default("year_end_date"), 1)},
- {'qty': 5},
- {'item_code': 'Test Product 2'},
- ]
- ]},
- {customer_address: 'Test1-Billing'},
- {shipping_address_name: 'Test1-Shipping'},
- {contact_person: 'Contact 1-Test Customer 1'},
- ]);
- },
- () => cur_frm.save(),
- () => {
- // get_item_details
- assert.ok(cur_frm.doc.items[0].item_name=='Test Product 2', "Item name correct");
- // get grand_total details
- assert.ok(cur_frm.doc.grand_total== 675, "Grand total correct ");
- },
- () => frappe.tests.click_button('Submit'),
- () => frappe.tests.click_button('Yes'),
- () => frappe.timeout(0.3),
- () => done()
- ]);
-});
diff --git a/erpnext/selling/doctype/sales_order/tests/test_sales_order_with_shipping_rule.js b/erpnext/selling/doctype/sales_order/tests/test_sales_order_with_shipping_rule.js
deleted file mode 100644
index 7d1211f..0000000
--- a/erpnext/selling/doctype/sales_order/tests/test_sales_order_with_shipping_rule.js
+++ /dev/null
@@ -1,35 +0,0 @@
-QUnit.module('Sales Order');
-
-QUnit.test("test sales order with shipping rule", function(assert) {
- assert.expect(2);
- let done = assert.async();
- frappe.run_serially([
- () => {
- return frappe.tests.make('Sales Order', [
- {customer: 'Test Customer 1'},
- {items: [
- [
- {'delivery_date': frappe.datetime.add_days(frappe.defaults.get_default("year_end_date"), 1)},
- {'qty': 5},
- {'item_code': 'Test Product 4'},
- ]
- ]},
- {customer_address: 'Test1-Billing'},
- {shipping_address_name: 'Test1-Shipping'},
- {contact_person: 'Contact 1-Test Customer 1'},
- {shipping_rule:'Next Day Shipping'}
- ]);
- },
- () => cur_frm.save(),
- () => {
- // get_item_details
- assert.ok(cur_frm.doc.items[0].item_name=='Test Product 4', "Item name correct");
- // get grand_total details
- assert.ok(cur_frm.doc.grand_total== 550, "Grand total correct ");
- },
- () => frappe.tests.click_button('Submit'),
- () => frappe.tests.click_button('Yes'),
- () => frappe.timeout(0.3),
- () => done()
- ]);
-});
diff --git a/erpnext/selling/doctype/sales_order/tests/test_sales_order_with_taxes_and_charges.js b/erpnext/selling/doctype/sales_order/tests/test_sales_order_with_taxes_and_charges.js
deleted file mode 100644
index a3668ab..0000000
--- a/erpnext/selling/doctype/sales_order/tests/test_sales_order_with_taxes_and_charges.js
+++ /dev/null
@@ -1,40 +0,0 @@
-QUnit.module('Sales Order');
-
-QUnit.test("test sales order with taxes and charges", function(assert) {
- assert.expect(3);
- let done = assert.async();
- frappe.run_serially([
- () => {
- return frappe.tests.make('Sales Order', [
- {customer: 'Test Customer 1'},
- {items: [
- [
- {'delivery_date': frappe.datetime.add_days(frappe.defaults.get_default("year_end_date"), 1)},
- {'qty': 5},
- {'item_code': 'Test Product 4'},
- ]
- ]},
- {customer_address: 'Test1-Billing'},
- {shipping_address_name: 'Test1-Shipping'},
- {contact_person: 'Contact 1-Test Customer 1'},
- {taxes_and_charges: 'TEST In State GST - FT'},
- {tc_name: 'Test Term 1'},
- {terms: 'This is Test'}
- ]);
- },
- () => cur_frm.save(),
- () => {
- // get_item_details
- assert.ok(cur_frm.doc.items[0].item_name=='Test Product 4', "Item name correct");
- // get tax details
- assert.ok(cur_frm.doc.taxes_and_charges=='TEST In State GST - FT', "Tax details correct");
- // get tax account head details
- assert.ok(cur_frm.doc.taxes[0].account_head=='CGST - '+frappe.get_abbr(frappe.defaults.get_default('Company')), " Account Head abbr correct");
-
- },
- () => frappe.tests.click_button('Submit'),
- () => frappe.tests.click_button('Yes'),
- () => frappe.timeout(0.3),
- () => done()
- ]);
-});
diff --git a/erpnext/selling/doctype/sales_order/tests/test_sales_order_without_bypass_credit_limit_check.js b/erpnext/selling/doctype/sales_order/tests/test_sales_order_without_bypass_credit_limit_check.js
deleted file mode 100644
index 8de39f9..0000000
--- a/erpnext/selling/doctype/sales_order/tests/test_sales_order_without_bypass_credit_limit_check.js
+++ /dev/null
@@ -1,62 +0,0 @@
-QUnit.module('Sales Order');
-
-QUnit.test("test_sales_order_without_bypass_credit_limit_check", function(assert) {
-//#PR : 10861, Author : ashish-greycube & jigneshpshah, Email:mr.ashish.shah@gmail.com
- assert.expect(2);
- let done = assert.async();
- frappe.run_serially([
- () => frappe.new_doc('Customer'),
- () => frappe.timeout(1),
- () => frappe.quick_entry.dialog.$wrapper.find('.edit-full').click(),
- () => frappe.timeout(1),
- () => cur_frm.set_value("customer_name", "Test Customer 11"),
- () => cur_frm.add_child('credit_limits', {
- 'credit_limit': 1000,
- 'company': '_Test Company',
- 'bypass_credit_limit_check': 1}),
- // save form
- () => cur_frm.save(),
- () => frappe.timeout(1),
-
- () => frappe.new_doc('Item'),
- () => frappe.timeout(1),
- () => frappe.click_link('Edit in full page'),
- () => cur_frm.set_value("item_code", "Test Product 11"),
- () => cur_frm.set_value("item_group", "Products"),
- () => cur_frm.set_value("standard_rate", 100),
- // save form
- () => cur_frm.save(),
- () => frappe.timeout(1),
-
- () => {
- return frappe.tests.make('Sales Order', [
- {customer: 'Test Customer 11'},
- {items: [
- [
- {'delivery_date': frappe.datetime.add_days(frappe.defaults.get_default("year_end_date"), 1)},
- {'qty': 5},
- {'item_code': 'Test Product 11'},
- ]
- ]}
-
- ]);
- },
- () => cur_frm.save(),
- () => frappe.tests.click_button('Submit'),
- () => assert.equal("Confirm", cur_dialog.title,'confirmation for submit'),
- () => frappe.tests.click_button('Yes'),
- () => frappe.timeout(3),
- () => {
-
- if (cur_dialog.body.innerText.match(/^Credit limit has been crossed for customer.*$/))
- {
- /*Match found */
- assert.ok(true, "Credit Limit crossed message received");
- }
-
-
- },
- () => cur_dialog.cancel(),
- () => done()
- ]);
-});
diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.json b/erpnext/selling/doctype/sales_order_item/sales_order_item.json
index 1e5590e..95f6c4e 100644
--- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json
+++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json
@@ -48,6 +48,7 @@
"pricing_rules",
"stock_uom_rate",
"is_free_item",
+ "grant_commission",
"section_break_24",
"net_rate",
"net_amount",
@@ -789,15 +790,23 @@
"no_copy": 1,
"options": "currency",
"read_only": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "grant_commission",
+ "fieldtype": "Check",
+ "label": "Grant Commission",
+ "read_only": 1
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2021-02-23 01:15:05.803091",
+ "modified": "2021-10-05 12:27:25.014789",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order Item",
+ "naming_rule": "Random",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.json b/erpnext/selling/doctype/selling_settings/selling_settings.json
index c27f1ea..27bc541 100644
--- a/erpnext/selling/doctype/selling_settings/selling_settings.json
+++ b/erpnext/selling/doctype/selling_settings/selling_settings.json
@@ -11,11 +11,6 @@
"customer_group",
"column_break_4",
"territory",
- "crm_settings_section",
- "campaign_naming_by",
- "default_valid_till",
- "column_break_9",
- "close_opportunity_after_days",
"item_price_settings_section",
"selling_price_list",
"maintain_same_rate_action",
@@ -44,13 +39,6 @@
"options": "Customer Name\nNaming Series\nAuto Name"
},
{
- "fieldname": "campaign_naming_by",
- "fieldtype": "Select",
- "in_list_view": 1,
- "label": "Campaign Naming By",
- "options": "Campaign Name\nNaming Series\nAuto Name"
- },
- {
"fieldname": "customer_group",
"fieldtype": "Link",
"in_list_view": 1,
@@ -72,18 +60,6 @@
"options": "Price List"
},
{
- "default": "15",
- "description": "Auto close Opportunity after the no. of days mentioned above",
- "fieldname": "close_opportunity_after_days",
- "fieldtype": "Int",
- "label": "Close Opportunity After Days"
- },
- {
- "fieldname": "default_valid_till",
- "fieldtype": "Data",
- "label": "Default Quotation Validity Days"
- },
- {
"fieldname": "column_break_5",
"fieldtype": "Column Break"
},
@@ -170,15 +146,6 @@
"fieldtype": "Column Break"
},
{
- "fieldname": "crm_settings_section",
- "fieldtype": "Section Break",
- "label": "CRM Settings"
- },
- {
- "fieldname": "column_break_9",
- "fieldtype": "Column Break"
- },
- {
"fieldname": "item_price_settings_section",
"fieldtype": "Section Break",
"label": "Item Price Settings"
@@ -204,7 +171,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2021-09-08 19:38:10.175989",
+ "modified": "2021-09-13 12:32:17.004404",
"modified_by": "Administrator",
"module": "Selling",
"name": "Selling Settings",
diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.py b/erpnext/selling/doctype/selling_settings/selling_settings.py
index e7c5e76..fb86e61 100644
--- a/erpnext/selling/doctype/selling_settings/selling_settings.py
+++ b/erpnext/selling/doctype/selling_settings/selling_settings.py
@@ -8,7 +8,6 @@
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
from frappe.model.document import Document
from frappe.utils import cint
-from frappe.utils.nestedset import get_root_of
class SellingSettings(Document):
@@ -37,9 +36,3 @@
editable_bundle_item_rates = cint(self.editable_bundle_item_rates)
make_property_setter("Packed Item", "rate", "read_only", not(editable_bundle_item_rates), "Check", validate_fields_for_doctype=False)
-
- def set_default_customer_group_and_territory(self):
- if not self.customer_group:
- self.customer_group = get_root_of('Customer Group')
- if not self.territory:
- self.territory = get_root_of('Territory')
diff --git a/erpnext/selling/form_tour/customer/customer.json b/erpnext/selling/form_tour/customer/customer.json
new file mode 100644
index 0000000..1de45b7
--- /dev/null
+++ b/erpnext/selling/form_tour/customer/customer.json
@@ -0,0 +1,29 @@
+{
+ "creation": "2021-11-23 10:44:13.185982",
+ "docstatus": 0,
+ "doctype": "Form Tour",
+ "idx": 0,
+ "is_standard": 1,
+ "modified": "2021-11-23 10:54:09.602358",
+ "modified_by": "Administrator",
+ "module": "Selling",
+ "name": "Customer",
+ "owner": "Administrator",
+ "reference_doctype": "Customer",
+ "save_on_complete": 1,
+ "steps": [
+ {
+ "description": "Enter the Full Name of the Customer",
+ "field": "",
+ "fieldname": "customer_name",
+ "fieldtype": "Data",
+ "has_next_condition": 0,
+ "is_table_field": 0,
+ "label": "Full Name",
+ "parent_field": "",
+ "position": "Left",
+ "title": "Full Name"
+ }
+ ],
+ "title": "Customer"
+}
\ No newline at end of file
diff --git a/erpnext/selling/form_tour/quotation/quotation.json b/erpnext/selling/form_tour/quotation/quotation.json
new file mode 100644
index 0000000..2a2aa5e
--- /dev/null
+++ b/erpnext/selling/form_tour/quotation/quotation.json
@@ -0,0 +1,67 @@
+{
+ "creation": "2021-11-23 12:00:36.138824",
+ "docstatus": 0,
+ "doctype": "Form Tour",
+ "idx": 0,
+ "is_standard": 1,
+ "modified": "2021-11-23 12:02:48.010298",
+ "modified_by": "Administrator",
+ "module": "Selling",
+ "name": "Quotation",
+ "owner": "Administrator",
+ "reference_doctype": "Quotation",
+ "save_on_complete": 1,
+ "steps": [
+ {
+ "description": "Select a customer or lead for whom this quotation is being prepared. Let's select a Customer.",
+ "field": "",
+ "fieldname": "quotation_to",
+ "fieldtype": "Link",
+ "has_next_condition": 0,
+ "is_table_field": 0,
+ "label": "Quotation To",
+ "parent_field": "",
+ "position": "Right",
+ "title": "Quotation To"
+ },
+ {
+ "description": "Select a specific Customer to whom this quotation will be sent.",
+ "field": "",
+ "fieldname": "party_name",
+ "fieldtype": "Dynamic Link",
+ "has_next_condition": 0,
+ "is_table_field": 0,
+ "label": "Party",
+ "parent_field": "",
+ "position": "Right",
+ "title": "Party"
+ },
+ {
+ "child_doctype": "Quotation Item",
+ "description": "Select an item for which you will be quoting a price.",
+ "field": "",
+ "fieldname": "items",
+ "fieldtype": "Table",
+ "has_next_condition": 0,
+ "is_table_field": 0,
+ "label": "Items",
+ "parent_field": "",
+ "parent_fieldname": "items",
+ "position": "Bottom",
+ "title": "Items"
+ },
+ {
+ "description": "You can select pre-populated Sales Taxes and Charges from here.",
+ "field": "",
+ "fieldname": "taxes",
+ "fieldtype": "Table",
+ "has_next_condition": 0,
+ "is_table_field": 0,
+ "label": "Sales Taxes and Charges",
+ "parent_field": "",
+ "position": "Bottom",
+ "title": "Sales Taxes and Charges"
+ }
+ ],
+ "title": "Quotation"
+}
\ No newline at end of file
diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js
index e61a634..ce74f6d 100644
--- a/erpnext/selling/page/point_of_sale/pos_controller.js
+++ b/erpnext/selling/page/point_of_sale/pos_controller.js
@@ -643,7 +643,7 @@
message: __('Item Code: {0} is not available under warehouse {1}.', [bold_item_code, bold_warehouse])
})
} else if (available_qty < qty_needed) {
- frappe.show_alert({
+ frappe.throw({
message: __('Stock quantity not enough for Item Code: {0} under warehouse {1}. Available quantity {2}.', [bold_item_code, bold_warehouse, bold_available_qty]),
indicator: 'orange'
});
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 9d8338e..4920584 100644
--- a/erpnext/selling/page/point_of_sale/pos_item_cart.js
+++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js
@@ -49,11 +49,11 @@
this.$component.append(
`<div class="cart-container">
<div class="abs-cart-container">
- <div class="cart-label">Item Cart</div>
+ <div class="cart-label">${__('Item Cart')}</div>
<div class="cart-header">
- <div class="name-header">Item</div>
- <div class="qty-header">Qty</div>
- <div class="rate-amount-header">Amount</div>
+ <div class="name-header">${__('Item')}</div>
+ <div class="qty-header">${__('Quantity')}</div>
+ <div class="rate-amount-header">${__('Amount')}</div>
</div>
<div class="cart-items-section"></div>
<div class="cart-totals-section"></div>
@@ -78,7 +78,7 @@
make_no_items_placeholder() {
this.$cart_header.css('display', 'none');
this.$cart_items_wrapper.html(
- `<div class="no-item-wrapper">No items in cart</div>`
+ `<div class="no-item-wrapper">${__('No items in cart')}</div>`
);
}
@@ -98,19 +98,23 @@
this.$totals_section.append(
`<div class="add-discount-wrapper">
- ${this.get_discount_icon()} Add Discount
+ ${this.get_discount_icon()} ${__('Add Discount')}
+ </div>
+ <div class="item-qty-total-container">
+ <div class="item-qty-total-label">${__('Total Items')}</div>
+ <div class="item-qty-total-value">0.00</div>
</div>
<div class="net-total-container">
- <div class="net-total-label">Net Total</div>
+ <div class="net-total-label">${__("Net Total")}</div>
<div class="net-total-value">0.00</div>
</div>
<div class="taxes-container"></div>
<div class="grand-total-container">
- <div>Grand Total</div>
+ <div>${__('Grand Total')}</div>
<div>0.00</div>
</div>
- <div class="checkout-btn">Checkout</div>
- <div class="edit-cart-btn">Edit Cart</div>`
+ <div class="checkout-btn">${__('Checkout')}</div>
+ <div class="edit-cart-btn">${__('Edit Cart')}</div>`
)
this.$add_discount_elem = this.$component.find(".add-discount-wrapper");
@@ -126,10 +130,10 @@
},
cols: 5,
keys: [
- [ 1, 2, 3, 'Quantity' ],
- [ 4, 5, 6, 'Discount' ],
- [ 7, 8, 9, 'Rate' ],
- [ '.', 0, 'Delete', 'Remove' ]
+ [ 1, 2, 3, __('Quantity') ],
+ [ 4, 5, 6, __('Discount') ],
+ [ 7, 8, 9, __('Rate') ],
+ [ '.', 0, __('Delete'), __('Remove') ]
],
css_classes: [
[ '', '', '', 'col-span-2' ],
@@ -142,13 +146,14 @@
this.$numpad_section.prepend(
`<div class="numpad-totals">
+ <span class="numpad-item-qty-total"></span>
<span class="numpad-net-total"></span>
<span class="numpad-grand-total"></span>
</div>`
)
this.$numpad_section.append(
- `<div class="numpad-btn checkout-btn" data-button-value="checkout">Checkout</div>`
+ `<div class="numpad-btn checkout-btn" data-button-value="checkout">${__('Checkout')}</div>`
)
}
@@ -386,7 +391,7 @@
'border': '1px dashed var(--gray-500)',
'padding': 'var(--padding-sm) var(--padding-md)'
});
- me.$add_discount_elem.html(`${me.get_discount_icon()} Add Discount`);
+ me.$add_discount_elem.html(`${me.get_discount_icon()} ${__('Add Discount')}`);
me.discount_field = undefined;
}
},
@@ -411,7 +416,7 @@
});
this.$add_discount_elem.html(
`<div class="edit-discount-btn">
- ${this.get_discount_icon()} Additional ${String(discount).bold()}% discount applied
+ ${this.get_discount_icon()} ${__("Additional")} ${String(discount).bold()}% ${__("discount applied")}
</div>`
);
}
@@ -445,7 +450,7 @@
function get_customer_description() {
if (!email_id && !mobile_no) {
- return `<div class="customer-desc">Click to add email / phone</div>`;
+ return `<div class="customer-desc">${__('Click to add email / phone')}</div>`;
} else if (email_id && !mobile_no) {
return `<div class="customer-desc">${email_id}</div>`;
} else if (mobile_no && !email_id) {
@@ -470,6 +475,7 @@
if (!frm) frm = this.events.get_frm();
this.render_net_total(frm.doc.net_total);
+ this.render_total_item_qty(frm.doc.items);
const grand_total = cint(frappe.sys_defaults.disable_rounded_total) ? frm.doc.grand_total : frm.doc.rounded_total;
this.render_grand_total(grand_total);
@@ -479,22 +485,37 @@
render_net_total(value) {
const currency = this.events.get_frm().doc.currency;
this.$totals_section.find('.net-total-container').html(
- `<div>Net Total</div><div>${format_currency(value, currency)}</div>`
+ `<div>${__('Net Total')}</div><div>${format_currency(value, currency)}</div>`
)
this.$numpad_section.find('.numpad-net-total').html(
- `<div>Net Total: <span>${format_currency(value, currency)}</span></div>`
+ `<div>${__('Net Total')}: <span>${format_currency(value, currency)}</span></div>`
+ );
+ }
+
+ render_total_item_qty(items) {
+ var total_item_qty = 0;
+ items.map((item) => {
+ total_item_qty = total_item_qty + item.qty;
+ });
+
+ this.$totals_section.find('.item-qty-total-container').html(
+ `<div>${__('Total Quantity')}</div><div>${total_item_qty}</div>`
+ );
+
+ this.$numpad_section.find('.numpad-item-qty-total').html(
+ `<div>${__('Total Quantity')}: <span>${total_item_qty}</span></div>`
);
}
render_grand_total(value) {
const currency = this.events.get_frm().doc.currency;
this.$totals_section.find('.grand-total-container').html(
- `<div>Grand Total</div><div>${format_currency(value, currency)}</div>`
+ `<div>${__('Grand Total')}</div><div>${format_currency(value, currency)}</div>`
)
this.$numpad_section.find('.numpad-grand-total').html(
- `<div>Grand Total: <span>${format_currency(value, currency)}</span></div>`
+ `<div>${__('Grand Total')}: <span>${format_currency(value, currency)}</span></div>`
);
}
@@ -502,6 +523,7 @@
if (taxes.length) {
const currency = this.events.get_frm().doc.currency;
const taxes_html = taxes.map(t => {
+ if (t.tax_amount_after_discount_amount == 0.0) return;
const description = /[0-9]+/.test(t.description) ? t.description : `${t.description} @ ${t.rate}%`;
return `<div class="tax-row">
<div class="tax-label">${description}</div>
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 ec861d7..fb69b63 100644
--- a/erpnext/selling/page/point_of_sale/pos_item_details.js
+++ b/erpnext/selling/page/point_of_sale/pos_item_details.js
@@ -28,7 +28,7 @@
init_child_components() {
this.$component.html(
`<div class="item-details-header">
- <div class="label">Item Details</div>
+ <div class="label">${__('Item Details')}</div>
<div class="close-btn">
<svg width="32" height="32" viewBox="0 0 14 14" fill="none">
<path d="M4.93764 4.93759L7.00003 6.99998M9.06243 9.06238L7.00003 6.99998M7.00003 6.99998L4.93764 9.06238L9.06243 4.93759" stroke="#8D99A6"/>
@@ -201,8 +201,9 @@
`<div class="grid-filler no-select"></div>`
);
}
+ const label = __('Auto Fetch Serial Numbers');
this.$form_container.append(
- `<div class="btn btn-sm btn-secondary auto-fetch-btn">Auto Fetch Serial Numbers</div>`
+ `<div class="btn btn-sm btn-secondary auto-fetch-btn">${label}</div>`
);
this.$form_container.find('.serial_no-control').find('textarea').css('height', '6rem');
}
diff --git a/erpnext/selling/page/point_of_sale/pos_item_selector.js b/erpnext/selling/page/point_of_sale/pos_item_selector.js
index 8352b14..a30bcd7 100644
--- a/erpnext/selling/page/point_of_sale/pos_item_selector.js
+++ b/erpnext/selling/page/point_of_sale/pos_item_selector.js
@@ -24,7 +24,7 @@
this.wrapper.append(
`<section class="items-selector">
<div class="filter-section">
- <div class="label">All Items</div>
+ <div class="label">${__('All Items')}</div>
<div class="search-field"></div>
<div class="item-group-field"></div>
</div>
@@ -113,7 +113,7 @@
`<div class="item-wrapper"
data-item-code="${escape(item.item_code)}" data-serial-no="${escape(serial_no)}"
data-batch-no="${escape(batch_no)}" data-uom="${escape(stock_uom)}"
- data-rate="${escape(price_list_rate)}"
+ data-rate="${escape(price_list_rate || 0)}"
title="${item.item_name}">
${get_item_image_html()}
diff --git a/erpnext/selling/page/point_of_sale/pos_past_order_list.js b/erpnext/selling/page/point_of_sale/pos_past_order_list.js
index e0993e2..a0475c7 100644
--- a/erpnext/selling/page/point_of_sale/pos_past_order_list.js
+++ b/erpnext/selling/page/point_of_sale/pos_past_order_list.js
@@ -16,7 +16,7 @@
this.wrapper.append(
`<section class="past-order-list">
<div class="filter-section">
- <div class="label">Recent Orders</div>
+ <div class="label">${__('Recent Orders')}</div>
<div class="search-field"></div>
<div class="status-field"></div>
</div>
diff --git a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js
index dd9e05a..eeb8523 100644
--- a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js
+++ b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js
@@ -17,16 +17,16 @@
this.wrapper.append(
`<section class="past-order-summary">
<div class="no-summary-placeholder">
- Select an invoice to load summary data
+ ${__('Select an invoice to load summary data')}
</div>
<div class="invoice-summary-wrapper">
<div class="abs-container">
<div class="upper-section"></div>
- <div class="label">Items</div>
+ <div class="label">${__('Items')}</div>
<div class="items-container summary-container"></div>
- <div class="label">Totals</div>
+ <div class="label">${__('Totals')}</div>
<div class="totals-container summary-container"></div>
- <div class="label">Payments</div>
+ <div class="label">${__('Payments')}</div>
<div class="payments-container summary-container"></div>
<div class="summary-btns"></div>
</div>
@@ -82,7 +82,7 @@
return `<div class="left-section">
<div class="customer-name">${doc.customer}</div>
<div class="customer-email">${this.customer_email}</div>
- <div class="cashier">Sold by: ${doc.owner}</div>
+ <div class="cashier">${__('Sold by')}: ${doc.owner}</div>
</div>
<div class="right-section">
<div class="paid-amount">${format_currency(doc.paid_amount, doc.currency)}</div>
@@ -121,7 +121,7 @@
get_net_total_html(doc) {
return `<div class="summary-row-wrapper">
- <div>Net Total</div>
+ <div>${__('Net Total')}</div>
<div>${format_currency(doc.net_total, doc.currency)}</div>
</div>`;
}
@@ -144,14 +144,14 @@
get_grand_total_html(doc) {
return `<div class="summary-row-wrapper grand-total">
- <div>Grand Total</div>
+ <div>${__('Grand Total')}</div>
<div>${format_currency(doc.grand_total, doc.currency)}</div>
</div>`;
}
get_payment_html(doc, payment) {
return `<div class="summary-row-wrapper payments">
- <div>${payment.mode_of_payment}</div>
+ <div>${__(payment.mode_of_payment)}</div>
<div>${format_currency(payment.amount, doc.currency)}</div>
</div>`;
}
@@ -285,8 +285,9 @@
if (m.condition) {
m.visible_btns.forEach(b => {
const class_name = b.split(' ')[0].toLowerCase();
+ const btn = __(b);
this.$summary_btns.append(
- `<div class="summary-btn btn btn-default ${class_name}-btn">${b}</div>`
+ `<div class="summary-btn btn btn-default ${class_name}-btn">${btn}</div>`
);
});
}
diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js
index 7ddbf45..b9b6559 100644
--- a/erpnext/selling/page/point_of_sale/pos_payment.js
+++ b/erpnext/selling/page/point_of_sale/pos_payment.js
@@ -18,11 +18,11 @@
prepare_dom() {
this.wrapper.append(
`<section class="payment-container">
- <div class="section-label payment-section">Payment Method</div>
+ <div class="section-label payment-section">${__('Payment Method')}</div>
<div class="payment-modes"></div>
<div class="fields-numpad-container">
<div class="fields-section">
- <div class="section-label">Additional Information</div>
+ <div class="section-label">${__('Additional Information')}</div>
<div class="invoice-fields"></div>
</div>
<div class="number-pad"></div>
@@ -30,7 +30,7 @@
<div class="totals-section">
<div class="totals"></div>
</div>
- <div class="submit-order-btn">Complete Order</div>
+ <div class="submit-order-btn">${__("Complete Order")}</div>
</section>`
);
this.$component = this.wrapper.find('.payment-container');
@@ -518,12 +518,12 @@
this.$totals.html(
`<div class="col">
- <div class="total-label">Grand Total</div>
+ <div class="total-label">${__('Grand Total')}</div>
<div class="value">${format_currency(grand_total, currency)}</div>
</div>
<div class="seperator-y"></div>
<div class="col">
- <div class="total-label">Paid Amount</div>
+ <div class="total-label">${__('Paid Amount')}</div>
<div class="value">${format_currency(paid_amount, currency)}</div>
</div>
<div class="seperator-y"></div>
diff --git a/erpnext/selling/report/pending_so_items_for_purchase_request/test_pending_so_items_for_purchase_request.py b/erpnext/selling/report/pending_so_items_for_purchase_request/test_pending_so_items_for_purchase_request.py
index 9c30afc..d62915f 100644
--- a/erpnext/selling/report/pending_so_items_for_purchase_request/test_pending_so_items_for_purchase_request.py
+++ b/erpnext/selling/report/pending_so_items_for_purchase_request/test_pending_so_items_for_purchase_request.py
@@ -2,8 +2,6 @@
# For license information, please see license.txt
-import unittest
-
from frappe.utils import add_months, nowdate
from erpnext.selling.doctype.sales_order.sales_order import make_material_request
@@ -11,9 +9,10 @@
from erpnext.selling.report.pending_so_items_for_purchase_request.pending_so_items_for_purchase_request import (
execute,
)
+from erpnext.tests.utils import ERPNextTestCase
-class TestPendingSOItemsForPurchaseRequest(unittest.TestCase):
+class TestPendingSOItemsForPurchaseRequest(ERPNextTestCase):
def test_result_for_partial_material_request(self):
so = make_sales_order()
mr=make_material_request(so.name)
diff --git a/erpnext/selling/report/sales_analytics/test_analytics.py b/erpnext/selling/report/sales_analytics/test_analytics.py
index 8ffc5d6..f56cce2 100644
--- a/erpnext/selling/report/sales_analytics/test_analytics.py
+++ b/erpnext/selling/report/sales_analytics/test_analytics.py
@@ -2,15 +2,14 @@
# For license information, please see license.txt
-import unittest
-
import frappe
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
from erpnext.selling.report.sales_analytics.sales_analytics import execute
+from erpnext.tests.utils import ERPNextTestCase
-class TestAnalytics(unittest.TestCase):
+class TestAnalytics(ERPNextTestCase):
def test_sales_analytics(self):
frappe.db.sql("delete from `tabSales Order` where company='_Test Company 2'")
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 82e5d0c..0c0acc7 100644
--- a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py
+++ b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py
@@ -61,6 +61,7 @@
IF(so.status in ('Completed','To Bill'), 0, (SELECT delay_days)) as delay,
soi.qty, soi.delivered_qty,
(soi.qty - soi.delivered_qty) AS pending_qty,
+ IF((SELECT pending_qty) = 0, (TO_SECONDS(Max(dn.posting_date))-TO_SECONDS(so.transaction_date)), 0) as time_taken_to_deliver,
IFNULL(SUM(sii.qty), 0) as billed_qty,
soi.base_amount as amount,
(soi.delivered_qty * soi.base_rate) as delivered_qty_amount,
@@ -70,9 +71,13 @@
so.company, soi.name
FROM
`tabSales Order` so,
- `tabSales Order Item` soi
+ (`tabSales Order Item` soi
LEFT JOIN `tabSales Invoice Item` sii
- ON sii.so_detail = soi.name and sii.docstatus = 1
+ ON sii.so_detail = soi.name and sii.docstatus = 1)
+ LEFT JOIN `tabDelivery Note Item` dni
+ on dni.so_detail = soi.name
+ RIGHT JOIN `tabDelivery Note` dn
+ on dni.parent = dn.name and dn.docstatus = 1
WHERE
soi.parent = so.name
and so.status not in ('Stopped', 'Closed', 'On Hold')
@@ -259,6 +264,12 @@
"fieldname": "delay",
"fieldtype": "Data",
"width": 100
+ },
+ {
+ "label": _("Time Taken to Deliver"),
+ "fieldname": "time_taken_to_deliver",
+ "fieldtype": "Duration",
+ "width": 100
}
])
if not filters.get("group_by_so"):
diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js
index 2050478..540aca2 100644
--- a/erpnext/selling/sales_common.js
+++ b/erpnext/selling/sales_common.js
@@ -41,6 +41,7 @@
me.frm.set_query('contact_person', erpnext.queries.contact_query);
me.frm.set_query('customer_address', erpnext.queries.address_query);
me.frm.set_query('shipping_address_name', erpnext.queries.address_query);
+ me.frm.set_query('dispatch_address_name', erpnext.queries.dispatch_address_query);
if(this.frm.fields_dict.selling_price_list) {
@@ -157,25 +158,19 @@
commission_rate() {
this.calculate_commission();
- refresh_field("total_commission");
}
total_commission() {
- if(this.frm.doc.base_net_total) {
- frappe.model.round_floats_in(this.frm.doc, ["base_net_total", "total_commission"]);
+ frappe.model.round_floats_in(this.frm.doc, ["amount_eligible_for_commission", "total_commission"]);
- if(this.frm.doc.base_net_total < this.frm.doc.total_commission) {
- var msg = (__("[Error]") + " " +
- __(frappe.meta.get_label(this.frm.doc.doctype, "total_commission",
- this.frm.doc.name)) + " > " +
- __(frappe.meta.get_label(this.frm.doc.doctype, "base_net_total", this.frm.doc.name)));
- frappe.msgprint(msg);
- throw msg;
- }
+ const { amount_eligible_for_commission } = this.frm.doc;
+ if(!amount_eligible_for_commission) return;
- this.frm.set_value("commission_rate",
- flt(this.frm.doc.total_commission * 100.0 / this.frm.doc.base_net_total));
- }
+ this.frm.set_value(
+ "commission_rate", flt(
+ this.frm.doc.total_commission * 100.0 / amount_eligible_for_commission
+ )
+ );
}
allocated_percentage(doc, cdt, cdn) {
@@ -185,7 +180,7 @@
sales_person.allocated_percentage = flt(sales_person.allocated_percentage,
precision("allocated_percentage", sales_person));
- sales_person.allocated_amount = flt(this.frm.doc.base_net_total *
+ sales_person.allocated_amount = flt(this.frm.doc.amount_eligible_for_commission *
sales_person.allocated_percentage / 100.0,
precision("allocated_amount", sales_person));
refresh_field(["allocated_amount"], sales_person);
@@ -259,28 +254,39 @@
}
calculate_commission() {
- if(this.frm.fields_dict.commission_rate) {
- if(this.frm.doc.commission_rate > 100) {
- var msg = __(frappe.meta.get_label(this.frm.doc.doctype, "commission_rate", this.frm.doc.name)) +
- " " + __("cannot be greater than 100");
- frappe.msgprint(msg);
- throw msg;
- }
+ if(!this.frm.fields_dict.commission_rate) return;
- this.frm.doc.total_commission = flt(this.frm.doc.base_net_total * this.frm.doc.commission_rate / 100.0,
- precision("total_commission"));
+ if(this.frm.doc.commission_rate > 100) {
+ this.frm.set_value("commission_rate", 100);
+ frappe.throw(`${__(frappe.meta.get_label(
+ this.frm.doc.doctype, "commission_rate", this.frm.doc.name
+ ))} ${__("cannot be greater than 100")}`);
}
+
+ this.frm.doc.amount_eligible_for_commission = this.frm.doc.items.reduce(
+ (sum, item) => item.grant_commission ? sum + item.base_net_amount : sum, 0
+ )
+
+ this.frm.doc.total_commission = flt(
+ this.frm.doc.amount_eligible_for_commission * this.frm.doc.commission_rate / 100.0,
+ precision("total_commission")
+ );
+
+ refresh_field(["amount_eligible_for_commission", "total_commission"]);
}
calculate_contribution() {
var me = this;
$.each(this.frm.doc.doctype.sales_team || [], function(i, sales_person) {
frappe.model.round_floats_in(sales_person);
- if(sales_person.allocated_percentage) {
- sales_person.allocated_amount = flt(
- me.frm.doc.base_net_total * sales_person.allocated_percentage / 100.0,
- precision("allocated_amount", sales_person));
- }
+ if (!sales_person.allocated_percentage) return;
+
+ sales_person.allocated_amount = flt(
+ me.frm.doc.amount_eligible_for_commission
+ * sales_person.allocated_percentage
+ / 100.0,
+ precision("allocated_amount", sales_person)
+ );
});
}
diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js
index 95ca386..45e8dcc 100644
--- a/erpnext/setup/doctype/company/company.js
+++ b/erpnext/setup/doctype/company/company.js
@@ -12,6 +12,10 @@
}
});
}
+
+ frm.call('check_if_transactions_exist').then(r => {
+ frm.toggle_enable("default_currency", (!r.message));
+ });
},
setup: function(frm) {
erpnext.company.setup_queries(frm);
@@ -75,21 +79,15 @@
},
refresh: function(frm) {
- if(!frm.doc.__islocal) {
- frm.doc.abbr && frm.set_df_property("abbr", "read_only", 1);
- frm.set_df_property("parent_company", "read_only", 1);
- disbale_coa_fields(frm);
- }
+ frm.toggle_display('address_html', !frm.is_new());
- frm.toggle_display('address_html', !frm.doc.__islocal);
- if(!frm.doc.__islocal) {
+ if (!frm.is_new()) {
+ frm.doc.abbr && frm.set_df_property("abbr", "read_only", 1);
+ disbale_coa_fields(frm);
frappe.contacts.render_address_and_contact(frm);
frappe.dynamic_link = {doc: frm.doc, fieldname: 'name', doctype: 'Company'}
- frm.toggle_enable("default_currency", (frm.doc.__onload &&
- !frm.doc.__onload.transactions_exist));
-
if (frappe.perm.has_perm("Cost Center", 0, 'read')) {
frm.add_custom_button(__('Cost Centers'), function() {
frappe.set_route('Tree', 'Cost Center', {'company': frm.doc.name});
diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py
index 5ebfa04..0a02bcd 100644
--- a/erpnext/setup/doctype/company/company.py
+++ b/erpnext/setup/doctype/company/company.py
@@ -22,8 +22,8 @@
def onload(self):
load_address_and_contact(self, "company")
- self.get("__onload")["transactions_exist"] = self.check_if_transactions_exist()
+ @frappe.whitelist()
def check_if_transactions_exist(self):
exists = False
for doctype in ["Sales Invoice", "Delivery Note", "Sales Order", "Quotation",
@@ -47,6 +47,7 @@
self.validate_perpetual_inventory()
self.validate_perpetual_inventory_for_non_stock_items()
self.check_country_change()
+ self.check_parent_changed()
self.set_chart_of_accounts()
self.validate_parent_company()
@@ -130,6 +131,10 @@
self.name in frappe.local.enable_perpetual_inventory:
frappe.local.enable_perpetual_inventory[self.name] = self.enable_perpetual_inventory
+ if frappe.flags.parent_company_changed:
+ from frappe.utils.nestedset import rebuild_tree
+ rebuild_tree("Company", "parent_company")
+
frappe.clear_cache()
def create_default_warehouses(self):
@@ -191,7 +196,7 @@
def check_country_change(self):
frappe.flags.country_change = False
- if not self.get('__islocal') and \
+ if not self.is_new() and \
self.country != frappe.get_cached_value('Company', self.name, 'country'):
frappe.flags.country_change = True
@@ -396,6 +401,13 @@
if not frappe.db.get_value('GL Entry', {'company': self.name}):
frappe.db.sql("delete from `tabProcess Deferred Accounting` where company=%s", self.name)
+ def check_parent_changed(self):
+ frappe.flags.parent_company_changed = False
+
+ if not self.is_new() and \
+ self.parent_company != frappe.db.get_value("Company", self.name, "parent_company"):
+ frappe.flags.parent_company_changed = True
+
def get_name_with_abbr(name, company):
company_abbr = frappe.get_cached_value('Company', company, "abbr")
parts = name.split(" - ")
@@ -413,7 +425,7 @@
frappe.get_attr(module_name)(company, False)
except Exception as e:
frappe.log_error()
- frappe.throw(_("Failed to setup defaults for country {0}. Please contact support@erpnext.com").format(frappe.bold(country)))
+ frappe.throw(_("Failed to setup defaults for country {0}. Please contact support.").format(frappe.bold(country)))
def update_company_current_month_sales(company):
diff --git a/erpnext/setup/doctype/company/test_company.py b/erpnext/setup/doctype/company/test_company.py
index 4ee9492..e175c54 100644
--- a/erpnext/setup/doctype/company/test_company.py
+++ b/erpnext/setup/doctype/company/test_company.py
@@ -93,6 +93,61 @@
frappe.db.sql(""" delete from `tabMode of Payment Account`
where company =%s """, (company))
+ def test_basic_tree(self, records=None):
+ min_lft = 1
+ max_rgt = frappe.db.sql("select max(rgt) from `tabCompany`")[0][0]
+
+ if not records:
+ records = test_records[2:]
+
+ for company in records:
+ lft, rgt, parent_company = frappe.db.get_value("Company", company["company_name"],
+ ["lft", "rgt", "parent_company"])
+
+ if parent_company:
+ parent_lft, parent_rgt = frappe.db.get_value("Company", parent_company,
+ ["lft", "rgt"])
+ else:
+ # root
+ parent_lft = min_lft - 1
+ parent_rgt = max_rgt + 1
+
+ self.assertTrue(lft)
+ self.assertTrue(rgt)
+ self.assertTrue(lft < rgt)
+ self.assertTrue(parent_lft < parent_rgt)
+ self.assertTrue(lft > parent_lft)
+ self.assertTrue(rgt < parent_rgt)
+ self.assertTrue(lft >= min_lft)
+ self.assertTrue(rgt <= max_rgt)
+
+ def get_no_of_children(self, company):
+ def get_no_of_children(companies, no_of_children):
+ children = []
+ for company in companies:
+ children += frappe.db.sql_list("""select name from `tabCompany`
+ where ifnull(parent_company, '')=%s""", company or '')
+
+ if len(children):
+ return get_no_of_children(children, no_of_children + len(children))
+ else:
+ return no_of_children
+
+ return get_no_of_children([company], 0)
+
+ def test_change_parent_company(self):
+ child_company = frappe.get_doc("Company", "_Test Company 5")
+
+ # changing parent of company
+ child_company.parent_company = "_Test Company 3"
+ child_company.save()
+ self.test_basic_tree()
+
+ # move it back
+ child_company.parent_company = "_Test Company 4"
+ child_company.save()
+ self.test_basic_tree()
+
def create_company_communication(doctype, docname):
comm = frappe.get_doc({
"doctype": "Communication",
diff --git a/erpnext/setup/doctype/company/test_records.json b/erpnext/setup/doctype/company/test_records.json
index 9e55702..89be607 100644
--- a/erpnext/setup/doctype/company/test_records.json
+++ b/erpnext/setup/doctype/company/test_records.json
@@ -36,7 +36,7 @@
"abbr": "_TC3",
"company_name": "_Test Company 3",
"is_group": 1,
- "country": "India",
+ "country": "Pakistan",
"default_currency": "INR",
"doctype": "Company",
"domain": "Manufacturing",
@@ -49,7 +49,7 @@
"company_name": "_Test Company 4",
"parent_company": "_Test Company 3",
"is_group": 1,
- "country": "India",
+ "country": "Pakistan",
"default_currency": "INR",
"doctype": "Company",
"domain": "Manufacturing",
@@ -61,7 +61,7 @@
"abbr": "_TC5",
"company_name": "_Test Company 5",
"parent_company": "_Test Company 4",
- "country": "India",
+ "country": "Pakistan",
"default_currency": "INR",
"doctype": "Company",
"domain": "Manufacturing",
diff --git a/erpnext/setup/doctype/company/tests/test_company.js b/erpnext/setup/doctype/company/tests/test_company.js
deleted file mode 100644
index b568494..0000000
--- a/erpnext/setup/doctype/company/tests/test_company.js
+++ /dev/null
@@ -1,25 +0,0 @@
-QUnit.module('setup');
-
-QUnit.test("Test: Company [SetUp]", function (assert) {
- assert.expect(2);
- let done = assert.async();
-
- frappe.run_serially([
- // test company creation
- () => frappe.set_route("List", "Company", "List"),
- () => frappe.new_doc("Company"),
- () => frappe.timeout(1),
- () => cur_frm.set_value("company_name", "Test Company"),
- () => cur_frm.set_value("abbr", "TC"),
- () => cur_frm.set_value("domain", "Services"),
- () => cur_frm.set_value("default_currency", "INR"),
- // save form
- () => cur_frm.save(),
- () => frappe.timeout(1),
- () => assert.equal("Debtors - TC", cur_frm.doc.default_receivable_account,
- 'chart of acounts created'),
- () => assert.equal("Main - TC", cur_frm.doc.cost_center,
- 'chart of cost centers created'),
- () => done()
- ]);
-});
diff --git a/erpnext/setup/doctype/company/tests/test_company_production.js b/erpnext/setup/doctype/company/tests/test_company_production.js
deleted file mode 100644
index a4c1e2e..0000000
--- a/erpnext/setup/doctype/company/tests/test_company_production.js
+++ /dev/null
@@ -1,19 +0,0 @@
-QUnit.test("Test: Company", function (assert) {
- assert.expect(0);
-
- let done = assert.async();
-
- frappe.run_serially([
- // Added company for Work Order testing
- () => frappe.set_route("List", "Company"),
- () => frappe.new_doc("Company"),
- () => frappe.timeout(1),
- () => cur_frm.set_value("company_name", "For Testing"),
- () => cur_frm.set_value("abbr", "RB"),
- () => cur_frm.set_value("default_currency", "INR"),
- () => cur_frm.save(),
- () => frappe.timeout(1),
-
- () => done()
- ]);
-});
diff --git a/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py b/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py
index 2b007e9..06a79b4 100644
--- a/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py
+++ b/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py
@@ -62,8 +62,13 @@
if kwargs['params'].get('date') and kwargs['params'].get('from') and kwargs['params'].get('to'):
if test_exchange_values.get(kwargs['params']['date']):
return PatchResponse({'result': test_exchange_values[kwargs['params']['date']]}, 200)
+ elif args[0].startswith("https://frankfurter.app") and kwargs.get('params'):
+ if kwargs['params'].get('base') and kwargs['params'].get('symbols'):
+ date = args[0].replace("https://frankfurter.app/", "")
+ if test_exchange_values.get(date):
+ return PatchResponse({'rates': {kwargs['params'].get('symbols'): test_exchange_values.get(date)}}, 200)
- return PatchResponse({'result': None}, 404)
+ return PatchResponse({'rates': None}, 404)
@mock.patch('requests.get', side_effect=patched_requests_get)
class TestCurrencyExchange(unittest.TestCase):
@@ -102,6 +107,41 @@
self.assertFalse(exchange_rate == 60)
self.assertEqual(flt(exchange_rate, 3), 65.1)
+ def test_exchange_rate_via_exchangerate_host(self, mock_get):
+ save_new_records(test_records)
+
+ # Update Currency Exchange Rate
+ settings = frappe.get_single("Currency Exchange Settings")
+ settings.service_provider = 'exchangerate.host'
+ settings.save()
+
+ # Update exchange
+ frappe.db.set_value("Accounts Settings", None, "allow_stale", 1)
+
+ # Start with allow_stale is True
+ exchange_rate = get_exchange_rate("USD", "INR", "2016-01-01", "for_buying")
+ self.assertEqual(flt(exchange_rate, 3), 60.0)
+
+ exchange_rate = get_exchange_rate("USD", "INR", "2016-01-15", "for_buying")
+ self.assertEqual(exchange_rate, 65.1)
+
+ 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
+ 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.999)
+
+ exchange_rate = get_exchange_rate("USD", "INR", "2016-01-20", "for_buying")
+ self.assertFalse(exchange_rate == 60)
+ self.assertEqual(flt(exchange_rate, 3), 65.1)
+
+ settings = frappe.get_single("Currency Exchange Settings")
+ settings.service_provider = 'frankfurter.app'
+ settings.save()
+
def test_exchange_rate_strict(self, mock_get):
# strict currency settings
frappe.db.set_value("Accounts Settings", None, "allow_stale", 0)
diff --git a/erpnext/setup/form_tour/company/company.json b/erpnext/setup/form_tour/company/company.json
new file mode 100644
index 0000000..c66abc0
--- /dev/null
+++ b/erpnext/setup/form_tour/company/company.json
@@ -0,0 +1,67 @@
+{
+ "creation": "2021-11-24 10:17:18.534917",
+ "docstatus": 0,
+ "doctype": "Form Tour",
+ "first_document": 1,
+ "idx": 0,
+ "include_name_field": 0,
+ "is_standard": 1,
+ "modified": "2021-11-24 15:38:21.026582",
+ "modified_by": "Administrator",
+ "module": "Setup",
+ "name": "Company",
+ "owner": "Administrator",
+ "reference_doctype": "Company",
+ "save_on_complete": 0,
+ "steps": [
+ {
+ "description": "This is the default currency for this company.",
+ "field": "",
+ "fieldname": "default_currency",
+ "fieldtype": "Link",
+ "has_next_condition": 0,
+ "is_table_field": 0,
+ "label": "Default Currency",
+ "parent_field": "",
+ "position": "Right",
+ "title": "Default Currency"
+ },
+ {
+ "description": "Here, you can add multiple addresses of the company",
+ "field": "",
+ "fieldname": "company_info",
+ "fieldtype": "Section Break",
+ "has_next_condition": 0,
+ "is_table_field": 0,
+ "label": "Address & Contact",
+ "parent_field": "",
+ "position": "Top",
+ "title": "Address & Contact"
+ },
+ {
+ "description": "Here, you can set default Accounts, which will ease the creation of accounting entries.",
+ "field": "",
+ "fieldname": "default_settings",
+ "fieldtype": "Section Break",
+ "has_next_condition": 0,
+ "is_table_field": 0,
+ "label": "Accounts Settings",
+ "parent_field": "",
+ "position": "Top",
+ "title": "Accounts Settings"
+ },
+ {
+ "description": "This setting is recommended if you wish to track the real-time stock balance in your books of account. This will allow the creation of a General Ledger entry for every stock transaction.",
+ "field": "",
+ "fieldname": "enable_perpetual_inventory",
+ "fieldtype": "Check",
+ "has_next_condition": 0,
+ "is_table_field": 0,
+ "label": "Enable Perpetual Inventory",
+ "parent_field": "",
+ "position": "Right",
+ "title": "Enable Perpetual Inventory"
+ }
+ ],
+ "title": "Company"
+}
\ No newline at end of file
diff --git a/erpnext/setup/install.py b/erpnext/setup/install.py
index 86c9b3f..bafaab8 100644
--- a/erpnext/setup/install.py
+++ b/erpnext/setup/install.py
@@ -60,6 +60,22 @@
frappe.db.set_default("date_format", "dd-mm-yyyy")
+ setup_currency_exchange()
+
+def setup_currency_exchange():
+ ces = frappe.get_single('Currency Exchange Settings')
+ try:
+ ces.set('result_key', [])
+ ces.set('req_params', [])
+
+ ces.api_endpoint = "https://frankfurter.app/{transaction_date}"
+ ces.append('result_key', {'key': 'rates'})
+ ces.append('result_key', {'key': '{to_currency}'})
+ ces.append('req_params', {'key': 'base', 'value': '{from_currency}'})
+ ces.append('req_params', {'key': 'symbols', 'value': '{to_currency}'})
+ ces.save()
+ except frappe.ValidationError:
+ pass
def create_compact_item_print_custom_field():
create_custom_field('Print Settings', {
diff --git a/erpnext/setup/module_onboarding/home/home.json b/erpnext/setup/module_onboarding/home/home.json
new file mode 100644
index 0000000..1b2dbc6
--- /dev/null
+++ b/erpnext/setup/module_onboarding/home/home.json
@@ -0,0 +1,62 @@
+{
+ "allow_roles": [
+ {
+ "role": "Accounts Manager"
+ },
+ {
+ "role": "Stock Manager"
+ },
+ {
+ "role": "Sales Manager"
+ },
+ {
+ "role": "Purchase Manager"
+ },
+ {
+ "role": "Manufacturing Manager"
+ },
+ {
+ "role": "Item Manager"
+ }
+ ],
+ "creation": "2021-11-22 12:19:15.888642",
+ "docstatus": 0,
+ "doctype": "Module Onboarding",
+ "documentation_url": "https://docs.erpnext.com/docs/v13/user/manual/en/setting-up/company-setup",
+ "idx": 0,
+ "is_complete": 0,
+ "modified": "2021-12-15 14:23:52.460913",
+ "modified_by": "Administrator",
+ "module": "Setup",
+ "name": "Home",
+ "owner": "Administrator",
+ "steps": [
+ {
+ "step": "Company Set Up"
+ },
+ {
+ "step": "Navigation Help"
+ },
+ {
+ "step": "Data import"
+ },
+ {
+ "step": "Create an Item"
+ },
+ {
+ "step": "Create a Customer"
+ },
+ {
+ "step": "Create a Supplier"
+ },
+ {
+ "step": "Create a Quotation"
+ },
+ {
+ "step": "Letterhead"
+ }
+ ],
+ "subtitle": "Company, Item, Customer, Supplier, Navigation Help, Data Import, Letter Head, Quotation",
+ "success_message": "Masters are all set up!",
+ "title": "Let's Set Up Some Masters"
+}
\ No newline at end of file
diff --git a/erpnext/setup/onboarding_step/company_set_up/company_set_up.json b/erpnext/setup/onboarding_step/company_set_up/company_set_up.json
new file mode 100644
index 0000000..6f65832
--- /dev/null
+++ b/erpnext/setup/onboarding_step/company_set_up/company_set_up.json
@@ -0,0 +1,21 @@
+{
+ "action": "Create Entry",
+ "action_label": "Let's review your Company",
+ "creation": "2021-11-22 11:55:48.931427",
+ "description": "# Set Up a Company\n\nA company is a legal entity for which you will set up your books of account and create accounting transactions. In ERPNext, you can create multiple companies, and establish relationships (group/subsidiary) among them.\n\nWithin the company master, you can capture various default accounts for that Company and set crucial settings related to the accounting methodology followed for a company.\n",
+ "docstatus": 0,
+ "doctype": "Onboarding Step",
+ "idx": 0,
+ "is_complete": 0,
+ "is_single": 0,
+ "is_skipped": 0,
+ "modified": "2021-12-15 14:22:18.317423",
+ "modified_by": "Administrator",
+ "name": "Company Set Up",
+ "owner": "Administrator",
+ "reference_document": "Company",
+ "show_form_tour": 1,
+ "show_full_form": 1,
+ "title": "Set Up a Company",
+ "validate_action": 1
+}
\ No newline at end of file
diff --git a/erpnext/setup/onboarding_step/create_a_customer/create_a_customer.json b/erpnext/setup/onboarding_step/create_a_customer/create_a_customer.json
new file mode 100644
index 0000000..f74d745
--- /dev/null
+++ b/erpnext/setup/onboarding_step/create_a_customer/create_a_customer.json
@@ -0,0 +1,21 @@
+{
+ "action": "Create Entry",
+ "action_label": "Let\u2019s create your first Customer",
+ "creation": "2020-05-14 17:46:41.831517",
+ "description": "# Create a Customer\n\nThe Customer master is at the heart of your sales transactions. Customers are linked in Quotations, Sales Orders, Invoices, and Payments. Customers can be either numbered or identified by name (you would typically do this based on the number of customers you have).\n\nThrough Customer\u2019s master, you can effectively track essentials like:\n - Customer\u2019s multiple address and contacts\n - Account Receivables\n - Credit Limit and Credit Period\n",
+ "docstatus": 0,
+ "doctype": "Onboarding Step",
+ "idx": 0,
+ "is_complete": 0,
+ "is_single": 0,
+ "is_skipped": 0,
+ "modified": "2021-12-15 14:20:31.197564",
+ "modified_by": "Administrator",
+ "name": "Create a Customer",
+ "owner": "Administrator",
+ "reference_document": "Customer",
+ "show_form_tour": 0,
+ "show_full_form": 0,
+ "title": "Manage Customers",
+ "validate_action": 1
+}
\ No newline at end of file
diff --git a/erpnext/setup/onboarding_step/create_a_quotation/create_a_quotation.json b/erpnext/setup/onboarding_step/create_a_quotation/create_a_quotation.json
new file mode 100644
index 0000000..8bdb621
--- /dev/null
+++ b/erpnext/setup/onboarding_step/create_a_quotation/create_a_quotation.json
@@ -0,0 +1,21 @@
+{
+ "action": "Create Entry",
+ "action_label": "Let\u2019s create your first Quotation",
+ "creation": "2020-06-01 13:34:58.958641",
+ "description": "# Create a Quotation\n\nLet\u2019s get started with business transactions by creating your first Quotation. You can create a Quotation for an existing customer or a prospect. It will be an approved document, with items you sell and the proposed price + taxes applied. After completing the instructions, you will get a Quotation in a ready to share print format.",
+ "docstatus": 0,
+ "doctype": "Onboarding Step",
+ "idx": 0,
+ "is_complete": 0,
+ "is_single": 0,
+ "is_skipped": 0,
+ "modified": "2021-12-15 14:21:31.675330",
+ "modified_by": "Administrator",
+ "name": "Create a Quotation",
+ "owner": "Administrator",
+ "reference_document": "Quotation",
+ "show_form_tour": 1,
+ "show_full_form": 1,
+ "title": "Create your first Quotation",
+ "validate_action": 1
+}
\ No newline at end of file
diff --git a/erpnext/setup/onboarding_step/create_a_supplier/create_a_supplier.json b/erpnext/setup/onboarding_step/create_a_supplier/create_a_supplier.json
new file mode 100644
index 0000000..9574141
--- /dev/null
+++ b/erpnext/setup/onboarding_step/create_a_supplier/create_a_supplier.json
@@ -0,0 +1,21 @@
+{
+ "action": "Create Entry",
+ "action_label": "Let\u2019s create your first Supplier",
+ "creation": "2020-05-14 22:09:10.043554",
+ "description": "# Create a Supplier\n\nAlso known as Vendor, is a master at the center of your purchase transactions. Suppliers are linked in Request for Quotation, Purchase Orders, Receipts, and Payments. Suppliers can be either numbered or identified by name.\n\nThrough Supplier\u2019s master, you can effectively track essentials like:\n - Supplier\u2019s multiple address and contacts\n - Account Receivables\n - Credit Limit and Credit Period\n",
+ "docstatus": 0,
+ "doctype": "Onboarding Step",
+ "idx": 0,
+ "is_complete": 0,
+ "is_single": 0,
+ "is_skipped": 0,
+ "modified": "2021-12-15 14:21:23.518301",
+ "modified_by": "Administrator",
+ "name": "Create a Supplier",
+ "owner": "Administrator",
+ "reference_document": "Supplier",
+ "show_form_tour": 0,
+ "show_full_form": 0,
+ "title": "Manage Suppliers",
+ "validate_action": 1
+}
\ No newline at end of file
diff --git a/erpnext/setup/onboarding_step/create_an_item/create_an_item.json b/erpnext/setup/onboarding_step/create_an_item/create_an_item.json
new file mode 100644
index 0000000..cd29683
--- /dev/null
+++ b/erpnext/setup/onboarding_step/create_an_item/create_an_item.json
@@ -0,0 +1,23 @@
+{
+ "action": "Create Entry",
+ "action_label": "Create a new Item",
+ "creation": "2021-05-17 13:47:18.515052",
+ "description": "# Create an Item\n\nItem is a product, of a or service offered by your company, or something you buy as a part of your supplies or raw materials.\n\nItems are integral to everything you do in ERPNext - from billing, purchasing to managing inventory. Everything you buy or sell, whether it is a physical product or a service is an Item. Items can be stock, non-stock, variants, serialized, batched, assets etc.\n",
+ "docstatus": 0,
+ "doctype": "Onboarding Step",
+ "form_tour": "Item General",
+ "idx": 0,
+ "intro_video_url": "",
+ "is_complete": 0,
+ "is_single": 0,
+ "is_skipped": 0,
+ "modified": "2021-12-15 14:19:56.297772",
+ "modified_by": "Administrator",
+ "name": "Create an Item",
+ "owner": "Administrator",
+ "reference_document": "Item",
+ "show_form_tour": 1,
+ "show_full_form": 1,
+ "title": "Manage Items",
+ "validate_action": 1
+}
\ No newline at end of file
diff --git a/erpnext/setup/onboarding_step/data_import/data_import.json b/erpnext/setup/onboarding_step/data_import/data_import.json
new file mode 100644
index 0000000..48741dc
--- /dev/null
+++ b/erpnext/setup/onboarding_step/data_import/data_import.json
@@ -0,0 +1,21 @@
+{
+ "action": "Watch Video",
+ "action_label": "Learn more about data migration",
+ "creation": "2021-05-19 05:29:16.809610",
+ "description": "# Import Data from Spreadsheet\n\nIn ERPNext, you can easily migrate your historical data using spreadsheets. You can use it for migrating not just masters (like Customer, Supplier, Items), but also for transactions like (outstanding invoices, opening stock and accounting entries, etc). If you are migrating from [Tally](https://tallysolutions.com/) or [Quickbooks](https://quickbooks.intuit.com/in/), we got special migration tools for you.",
+ "docstatus": 0,
+ "doctype": "Onboarding Step",
+ "idx": 0,
+ "is_complete": 0,
+ "is_single": 0,
+ "is_skipped": 0,
+ "modified": "2021-12-15 13:10:57.346422",
+ "modified_by": "Administrator",
+ "name": "Data import",
+ "owner": "Administrator",
+ "show_form_tour": 0,
+ "show_full_form": 0,
+ "title": "Import Data from Spreadsheet",
+ "validate_action": 1,
+ "video_url": "https://youtu.be/DQyqeurPI64"
+}
\ No newline at end of file
diff --git a/erpnext/setup/onboarding_step/letterhead/letterhead.json b/erpnext/setup/onboarding_step/letterhead/letterhead.json
new file mode 100644
index 0000000..8e1bb8c
--- /dev/null
+++ b/erpnext/setup/onboarding_step/letterhead/letterhead.json
@@ -0,0 +1,21 @@
+{
+ "action": "Create Entry",
+ "action_label": "Let\u2019s setup your first Letter Head",
+ "creation": "2021-11-22 12:36:34.583783",
+ "description": "# Create a Letter Head\n\nA Letter Head contains your organization's name, logo, address, etc which appears at the header and footer portion in documents. You can learn more about Setting up Letter Head in ERPNext here.\n",
+ "docstatus": 0,
+ "doctype": "Onboarding Step",
+ "idx": 0,
+ "is_complete": 0,
+ "is_single": 0,
+ "is_skipped": 0,
+ "modified": "2021-12-15 14:21:39.037742",
+ "modified_by": "Administrator",
+ "name": "Letterhead",
+ "owner": "Administrator",
+ "reference_document": "Letter Head",
+ "show_form_tour": 1,
+ "show_full_form": 1,
+ "title": "Setup Your Letterhead",
+ "validate_action": 1
+}
\ No newline at end of file
diff --git a/erpnext/setup/onboarding_step/navigation_help/navigation_help.json b/erpnext/setup/onboarding_step/navigation_help/navigation_help.json
new file mode 100644
index 0000000..388853d
--- /dev/null
+++ b/erpnext/setup/onboarding_step/navigation_help/navigation_help.json
@@ -0,0 +1,21 @@
+{
+ "action": "Watch Video",
+ "action_label": "Learn about Navigation options",
+ "creation": "2021-11-22 12:09:52.233872",
+ "description": "# Navigation in ERPNext\n\nEase of navigating and browsing around the ERPNext is one of our core strengths. In the following video, you will learn how to reach a specific feature in ERPNext via module page or awesome bar\u2019s shortcut.\n",
+ "docstatus": 0,
+ "doctype": "Onboarding Step",
+ "idx": 0,
+ "is_complete": 0,
+ "is_single": 0,
+ "is_skipped": 0,
+ "modified": "2021-12-15 14:20:55.441678",
+ "modified_by": "Administrator",
+ "name": "Navigation Help",
+ "owner": "Administrator",
+ "show_form_tour": 0,
+ "show_full_form": 0,
+ "title": "How to Navigate in ERPNext",
+ "validate_action": 1,
+ "video_url": "https://youtu.be/j60xyNFqX_A"
+}
\ No newline at end of file
diff --git a/erpnext/setup/setup_wizard/data/country_wise_tax.json b/erpnext/setup/setup_wizard/data/country_wise_tax.json
index 14b7951..91e8eff 100644
--- a/erpnext/setup/setup_wizard/data/country_wise_tax.json
+++ b/erpnext/setup/setup_wizard/data/country_wise_tax.json
@@ -1178,11 +1178,13 @@
{
"title": "Reverse Charge In-State",
"is_inter_state": 0,
+ "is_reverse_charge": 1,
"gst_state": ""
},
{
"title": "Reverse Charge Out-State",
"is_inter_state": 1,
+ "is_reverse_charge": 1,
"gst_state": ""
},
{
diff --git a/erpnext/setup/setup_wizard/operations/defaults_setup.py b/erpnext/setup/setup_wizard/operations/defaults_setup.py
index e4b1fa2..ca1f57e 100644
--- a/erpnext/setup/setup_wizard/operations/defaults_setup.py
+++ b/erpnext/setup/setup_wizard/operations/defaults_setup.py
@@ -68,6 +68,8 @@
hr_settings.send_interview_feedback_reminder = 1
hr_settings.feedback_reminder_notification_template = _("Interview Feedback Reminder")
+
+ hr_settings.exit_questionnaire_notification_template = _("Exit Questionnaire Notification")
hr_settings.save()
def set_no_copy_fields_in_variant_settings():
diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py
index 7c884f7..d7c6913 100644
--- a/erpnext/setup/setup_wizard/operations/install_fixtures.py
+++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py
@@ -33,7 +33,6 @@
{ 'doctype': 'Domain', 'domain': 'Services'},
{ 'doctype': 'Domain', 'domain': 'Education'},
{ 'doctype': 'Domain', 'domain': 'Healthcare'},
- { 'doctype': 'Domain', 'domain': 'Agriculture'},
{ 'doctype': 'Domain', 'domain': 'Non Profit'},
# ensure at least an empty Address Template exists for this Country
@@ -278,6 +277,11 @@
records += [{'doctype': 'Email Template', 'name': _('Interview Feedback Reminder'), 'response': response,
'subject': _('Interview Feedback Reminder'), 'owner': frappe.session.user}]
+ response = frappe.read_file(os.path.join(base_path, 'exit_interview/exit_questionnaire_notification_template.html'))
+
+ records += [{'doctype': 'Email Template', 'name': _('Exit Questionnaire Notification'), 'response': response,
+ 'subject': _('Exit Questionnaire Notification'), 'owner': frappe.session.user}]
+
base_path = frappe.get_app_path("erpnext", "stock", "doctype")
response = frappe.read_file(os.path.join(base_path, "delivery_trip/dispatch_notification_template.html"))
@@ -303,7 +307,6 @@
def update_selling_defaults():
selling_settings = frappe.get_doc("Selling Settings")
- selling_settings.set_default_customer_group_and_territory()
selling_settings.cust_master_name = "Customer Name"
selling_settings.so_required = "No"
selling_settings.dn_required = "No"
@@ -350,7 +353,8 @@
"doctype": "UOM",
"uom_name": _(d.get("uom_name")),
"name": _(d.get("uom_name")),
- "must_be_whole_number": d.get("must_be_whole_number")
+ "must_be_whole_number": d.get("must_be_whole_number"),
+ "enabled": 1,
}).db_insert()
# bootstrap uom conversion factors
diff --git a/erpnext/setup/utils.py b/erpnext/setup/utils.py
index 1478007..4441bb9 100644
--- a/erpnext/setup/utils.py
+++ b/erpnext/setup/utils.py
@@ -53,6 +53,7 @@
frappe.db.set_value("Stock Settings", None, "auto_insert_price_list_rate_if_missing", 0)
enable_all_roles_and_domains()
+ set_defaults_for_tests()
frappe.db.commit()
@@ -99,15 +100,21 @@
if not value:
import requests
- api_url = "https://api.exchangerate.host/convert"
- response = requests.get(api_url, params={
- "date": transaction_date,
- "from": from_currency,
- "to": to_currency
- })
+ settings = frappe.get_cached_doc('Currency Exchange Settings')
+ req_params = {
+ "transaction_date": transaction_date,
+ "from_currency": from_currency,
+ "to_currency": to_currency
+ }
+ params = {}
+ for row in settings.req_params:
+ params[row.key] = format_ces_api(row.value, req_params)
+ response = requests.get(format_ces_api(settings.api_endpoint, req_params), params=params)
# expire in 6 hours
response.raise_for_status()
- value = response.json()["result"]
+ value = response.json()
+ for res_key in settings.result_key:
+ value = value[format_ces_api(str(res_key.key), req_params)]
cache.setex(name=key, time=21600, value=flt(value))
return flt(value)
except Exception:
@@ -115,6 +122,13 @@
frappe.msgprint(_("Unable to find exchange rate for {0} to {1} for key date {2}. Please create a Currency Exchange record manually").format(from_currency, to_currency, transaction_date))
return 0.0
+def format_ces_api(data, param):
+ return data.format(
+ transaction_date=param.get("transaction_date"),
+ to_currency=param.get("to_currency"),
+ from_currency=param.get("from_currency")
+ )
+
def enable_all_roles_and_domains():
""" enable all roles and domain for testing """
# add all roles to users
@@ -127,6 +141,14 @@
[d.name for d in domains])
add_all_roles_to('Administrator')
+def set_defaults_for_tests():
+ from frappe.utils.nestedset import get_root_of
+
+ selling_settings = frappe.get_single("Selling Settings")
+ selling_settings.customer_group = get_root_of("Customer Group")
+ selling_settings.territory = get_root_of("Territory")
+ selling_settings.save()
+
def insert_record(records):
for r in records:
diff --git a/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json b/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json
index 1412acf..e47837f 100644
--- a/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json
+++ b/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json
@@ -10,7 +10,7 @@
"idx": 0,
"label": "ERPNext Settings",
"links": [],
- "modified": "2021-10-26 21:32:55.323591",
+ "modified": "2021-11-05 21:32:55.323591",
"modified_by": "Administrator",
"module": "Setup",
"name": "ERPNext Settings",
@@ -123,6 +123,13 @@
"label": "Products Settings",
"link_to": "Products Settings",
"type": "DocType"
+ },
+ {
+ "doc_view": "",
+ "icon": "crm",
+ "label": "CRM Settings",
+ "link_to": "CRM Settings",
+ "type": "DocType"
}
],
"title": "ERPNext Settings"
diff --git a/erpnext/setup/workspace/home/home.json b/erpnext/setup/workspace/home/home.json
index 4e1ccf9..f9c585c0 100644
--- a/erpnext/setup/workspace/home/home.json
+++ b/erpnext/setup/workspace/home/home.json
@@ -1,13 +1,18 @@
{
"charts": [],
- "content": "[{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"level\":4,\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Item\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Customer\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Supplier\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Invoice\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Leaderboard\",\"col\":4}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"level\":4,\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Accounting\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Stock\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Human Resources\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"CRM\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Data Import and Settings\",\"col\":4}}]",
+ "content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Home\",\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\",\"level\":4,\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Item\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Customer\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Supplier\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Invoice\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Leaderboard\",\"col\":4}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\",\"level\":4,\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Accounting\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Stock\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Human Resources\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"CRM\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Data Import and Settings\",\"col\":4}}]",
"creation": "2020-01-23 13:46:38.833076",
+ "developer_mode_only": 0,
+ "disable_user_customization": 0,
"docstatus": 0,
"doctype": "Workspace",
+ "extends_another_page": 0,
"for_user": "",
"hide_custom": 0,
"icon": "getting-started",
"idx": 0,
+ "is_default": 0,
+ "is_standard": 0,
"label": "Home",
"links": [
{
@@ -271,12 +276,14 @@
"type": "Link"
}
],
- "modified": "2021-08-10 15:33:20.704741",
+ "modified": "2021-11-22 12:50:15.771366",
"modified_by": "Administrator",
"module": "Setup",
"name": "Home",
"owner": "Administrator",
"parent_page": "",
+ "pin_to_bottom": 0,
+ "pin_to_top": 0,
"public": 1,
"restrict_to_domain": "",
"roles": [],
@@ -309,4 +316,4 @@
}
],
"title": "Home"
-}
\ No newline at end of file
+}
diff --git a/erpnext/startup/boot.py b/erpnext/startup/boot.py
index ed8c878..0da45a5 100644
--- a/erpnext/startup/boot.py
+++ b/erpnext/startup/boot.py
@@ -22,7 +22,7 @@
'customer_group')
bootinfo.sysdefaults.allow_stale = cint(frappe.db.get_single_value('Accounts Settings',
'allow_stale'))
- bootinfo.sysdefaults.quotation_valid_till = cint(frappe.db.get_single_value('Selling Settings',
+ bootinfo.sysdefaults.quotation_valid_till = cint(frappe.db.get_single_value('CRM Settings',
'default_valid_till'))
# if no company, show a dialog box to create a new company
diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py
index fdefd24..5593101 100644
--- a/erpnext/stock/doctype/batch/batch.py
+++ b/erpnext/stock/doctype/batch/batch.py
@@ -312,3 +312,28 @@
if frappe.db.get_value("Item", args.item, "has_batch_no"):
args.doctype = "Batch"
frappe.get_doc(args).insert().name
+
+@frappe.whitelist()
+def get_pos_reserved_batch_qty(filters):
+ import json
+
+ if isinstance(filters, str):
+ filters = json.loads(filters)
+
+ p = frappe.qb.DocType("POS Invoice").as_("p")
+ item = frappe.qb.DocType("POS Invoice Item").as_("item")
+ sum_qty = frappe.query_builder.functions.Sum(item.qty).as_("qty")
+
+ reserved_batch_qty = frappe.qb.from_(p).from_(item).select(sum_qty).where(
+ (p.name == item.parent) &
+ (p.consolidated_invoice.isnull()) &
+ (p.status != "Consolidated") &
+ (p.docstatus == 1) &
+ (item.docstatus == 1) &
+ (item.item_code == filters.get('item_code')) &
+ (item.warehouse == filters.get('warehouse')) &
+ (item.batch_no == filters.get('batch_no'))
+ ).run()
+
+ flt_reserved_batch_qty = flt(reserved_batch_qty[0][0])
+ return flt_reserved_batch_qty
\ No newline at end of file
diff --git a/erpnext/stock/doctype/batch/test_batch.js b/erpnext/stock/doctype/batch/test_batch.js
deleted file mode 100644
index 2d2150b..0000000
--- a/erpnext/stock/doctype/batch/test_batch.js
+++ /dev/null
@@ -1,22 +0,0 @@
-QUnit.module('Stock');
-
-QUnit.test("test Batch", function(assert) {
- assert.expect(1);
- let done = assert.async();
- frappe.run_serially([
- () => {
- return frappe.tests.make('Batch', [
- {batch_id:'TEST-BATCH-001'},
- {item:'Test Product 4'},
- {expiry_date:frappe.datetime.add_days(frappe.datetime.now_date(), 2)},
- ]);
- },
- () => cur_frm.save(),
- () => {
- // get_item_details
- assert.ok(cur_frm.doc.batch_id=='TEST-BATCH-001', "Batch Id correct");
- },
- () => frappe.timeout(0.3),
- () => done()
- ]);
-});
diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py
index 48b1cc5..0ef7ce2 100644
--- a/erpnext/stock/doctype/bin/bin.py
+++ b/erpnext/stock/doctype/bin/bin.py
@@ -4,7 +4,9 @@
import frappe
from frappe.model.document import Document
-from frappe.utils import flt, nowdate
+from frappe.query_builder import Case
+from frappe.query_builder.functions import Coalesce, Sum
+from frappe.utils import flt
class Bin(Document):
@@ -19,34 +21,42 @@
- flt(self.reserved_qty_for_production) - flt(self.reserved_qty_for_sub_contract))
def get_first_sle(self):
- sle = frappe.db.sql("""
- select * from `tabStock Ledger Entry`
- where item_code = %s
- and warehouse = %s
- order by timestamp(posting_date, posting_time) asc, creation asc
- limit 1
- """, (self.item_code, self.warehouse), as_dict=1)
- return sle and sle[0] or None
+ sle = frappe.qb.DocType("Stock Ledger Entry")
+ first_sle = (
+ frappe.qb.from_(sle)
+ .select("*")
+ .where((sle.item_code == self.item_code) & (sle.warehouse == self.warehouse))
+ .orderby(sle.posting_date, sle.posting_time, sle.creation)
+ .limit(1)
+ ).run(as_dict=True)
+
+ return first_sle and first_sle[0] or None
def update_reserved_qty_for_production(self):
'''Update qty reserved for production from Production Item tables
in open work orders'''
- self.reserved_qty_for_production = frappe.db.sql('''
- SELECT
- CASE WHEN ifnull(skip_transfer, 0) = 0 THEN
- SUM(item.required_qty - item.transferred_qty)
- ELSE
- SUM(item.required_qty - item.consumed_qty)
- END
- FROM `tabWork Order` pro, `tabWork Order Item` item
- WHERE
- item.item_code = %s
- and item.parent = pro.name
- and pro.docstatus = 1
- and item.source_warehouse = %s
- and pro.status not in ("Stopped", "Completed")
- and (item.required_qty > item.transferred_qty or item.required_qty > item.consumed_qty)
- ''', (self.item_code, self.warehouse))[0][0]
+
+ wo = frappe.qb.DocType("Work Order")
+ wo_item = frappe.qb.DocType("Work Order Item")
+
+ self.reserved_qty_for_production = (
+ frappe.qb
+ .from_(wo)
+ .from_(wo_item)
+ .select(Sum(Case()
+ .when(wo.skip_transfer == 0, wo_item.required_qty - wo_item.transferred_qty)
+ .else_(wo_item.required_qty - wo_item.consumed_qty))
+ )
+ .where(
+ (wo_item.item_code == self.item_code)
+ & (wo_item.parent == wo.name)
+ & (wo.docstatus == 1)
+ & (wo_item.source_warehouse == self.warehouse)
+ & (wo.status.notin(["Stopped", "Completed"]))
+ & ((wo_item.required_qty > wo_item.transferred_qty)
+ | (wo_item.required_qty > wo_item.consumed_qty))
+ )
+ ).run()[0][0] or 0.0
self.set_projected_qty()
@@ -55,36 +65,53 @@
def update_reserved_qty_for_sub_contracting(self):
#reserved qty
- reserved_qty_for_sub_contract = frappe.db.sql('''
- select ifnull(sum(itemsup.required_qty),0)
- from `tabPurchase Order` po, `tabPurchase Order Item Supplied` itemsup
- where
- itemsup.rm_item_code = %s
- and itemsup.parent = po.name
- and po.docstatus = 1
- and po.is_subcontracted = 'Yes'
- and po.status != 'Closed'
- and po.per_received < 100
- and itemsup.reserve_warehouse = %s''', (self.item_code, self.warehouse))[0][0]
- #Get Transferred Entries
- materials_transferred = frappe.db.sql("""
- select
- ifnull(sum(CASE WHEN se.is_return = 1 THEN (transfer_qty * -1) ELSE transfer_qty END),0)
- from
- `tabStock Entry` se, `tabStock Entry Detail` sed, `tabPurchase Order` po
- where
- se.docstatus=1
- and se.purpose='Send to Subcontractor'
- and ifnull(se.purchase_order, '') !=''
- and (sed.item_code = %(item)s or sed.original_item = %(item)s)
- and se.name = sed.parent
- and se.purchase_order = po.name
- and po.docstatus = 1
- and po.is_subcontracted = 'Yes'
- and po.status != 'Closed'
- and po.per_received < 100
- """, {'item': self.item_code})[0][0]
+ po = frappe.qb.DocType("Purchase Order")
+ supplied_item = frappe.qb.DocType("Purchase Order Item Supplied")
+
+ reserved_qty_for_sub_contract = (
+ frappe.qb
+ .from_(po)
+ .from_(supplied_item)
+ .select(Sum(Coalesce(supplied_item.required_qty, 0)))
+ .where(
+ (supplied_item.rm_item_code == self.item_code)
+ & (po.name == supplied_item.parent)
+ & (po.docstatus == 1)
+ & (po.is_subcontracted == "Yes")
+ & (po.status != "Closed")
+ & (po.per_received < 100)
+ & (supplied_item.reserve_warehouse == self.warehouse)
+ )
+ ).run()[0][0] or 0.0
+
+ se = frappe.qb.DocType("Stock Entry")
+ se_item = frappe.qb.DocType("Stock Entry Detail")
+
+ materials_transferred = (
+ frappe.qb
+ .from_(se)
+ .from_(se_item)
+ .from_(po)
+ .select(Sum(
+ Case()
+ .when(se.is_return == 1, se_item.transfer_qty * -1)
+ .else_(se_item.transfer_qty)
+ ))
+ .where(
+ (se.docstatus == 1)
+ & (se.purpose == "Send to Subcontractor")
+ & (Coalesce(se.purchase_order, "") != "")
+ & ((se_item.item_code == self.item_code)
+ | (se_item.original_item == self.item_code))
+ & (se.name == se_item.parent)
+ & (po.name == se.purchase_order)
+ & (po.docstatus == 1)
+ & (po.is_subcontracted == "Yes")
+ & (po.status != "Closed")
+ & (po.per_received < 100)
+ )
+ ).run()[0][0] or 0.0
if reserved_qty_for_sub_contract > materials_transferred:
reserved_qty_for_sub_contract = reserved_qty_for_sub_contract - materials_transferred
@@ -100,47 +127,35 @@
def update_stock(bin_name, args, allow_negative_stock=False, via_landed_cost_voucher=False):
- '''Called from erpnext.stock.utils.update_bin'''
+ """WARNING: This function is deprecated. Inline this function instead of using it."""
+ from erpnext.stock.stock_ledger import repost_current_voucher
+
+ repost_current_voucher(args, allow_negative_stock, via_landed_cost_voucher)
update_qty(bin_name, args)
- if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation":
- from erpnext.stock.stock_ledger import update_entries_after, update_qty_in_future_sle
-
- if not args.get("posting_date"):
- args["posting_date"] = nowdate()
-
- if args.get("is_cancelled") and via_landed_cost_voucher:
- return
-
- # Reposts only current voucher SL Entries
- # Updates valuation rate, stock value, stock queue for current transaction
- update_entries_after({
- "item_code": args.get('item_code'),
- "warehouse": args.get('warehouse'),
- "posting_date": args.get("posting_date"),
- "posting_time": args.get("posting_time"),
- "voucher_type": args.get("voucher_type"),
- "voucher_no": args.get("voucher_no"),
- "sle_id": args.get('name'),
- "creation": args.get('creation')
- }, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher)
-
- # update qty in future sle and Validate negative qty
- update_qty_in_future_sle(args, allow_negative_stock)
-
def get_bin_details(bin_name):
return frappe.db.get_value('Bin', bin_name, ['actual_qty', 'ordered_qty',
'reserved_qty', 'indented_qty', 'planned_qty', 'reserved_qty_for_production',
'reserved_qty_for_sub_contract'], as_dict=1)
def update_qty(bin_name, args):
- bin_details = get_bin_details(bin_name)
+ from erpnext.controllers.stock_controller import future_sle_exists
- # update the stock values (for current quantities)
- if args.get("voucher_type")=="Stock Reconciliation":
- actual_qty = args.get('qty_after_transaction')
- else:
- actual_qty = bin_details.actual_qty + flt(args.get("actual_qty"))
+ bin_details = get_bin_details(bin_name)
+ # actual qty is already updated by processing current voucher
+ actual_qty = bin_details.actual_qty
+
+ # actual qty is not up to date in case of backdated transaction
+ if future_sle_exists(args):
+ actual_qty = frappe.db.get_value("Stock Ledger Entry",
+ filters={
+ "item_code": args.get("item_code"),
+ "warehouse": args.get("warehouse"),
+ "is_cancelled": 0
+ },
+ fieldname="qty_after_transaction",
+ order_by="posting_date desc, posting_time desc, creation desc",
+ ) or 0.0
ordered_qty = flt(bin_details.ordered_qty) + flt(args.get("ordered_qty"))
reserved_qty = flt(bin_details.reserved_qty) + flt(args.get("reserved_qty"))
@@ -160,4 +175,4 @@
'indented_qty': indented_qty,
'planned_qty': planned_qty,
'projected_qty': projected_qty
- })
\ No newline at end of file
+ })
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json
index ad1b3b4..55a4c95 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.json
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.json
@@ -145,6 +145,7 @@
"sales_team_section_break",
"sales_partner",
"column_break7",
+ "amount_eligible_for_commission",
"commission_rate",
"total_commission",
"section_break1",
@@ -1302,16 +1303,23 @@
"label": "Dispatch Address",
"print_hide": 1,
"read_only": 1
+ },
+ {
+ "fieldname": "amount_eligible_for_commission",
+ "fieldtype": "Currency",
+ "label": "Amount Eligible for Commission",
+ "read_only": 1
}
],
"icon": "fa fa-truck",
"idx": 146,
"is_submittable": 1,
"links": [],
- "modified": "2021-10-08 14:29:13.428984",
+ "modified": "2021-10-09 14:29:13.428984",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note",
+ "naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py
index 5268460..70d48a4 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.py
@@ -138,6 +138,7 @@
self.update_current_stock()
if not self.installation_status: self.installation_status = 'Not Installed'
+ self.reset_default_field_value("set_warehouse", "items", "warehouse")
def validate_with_previous_doc(self):
super(DeliveryNote, self).validate_with_previous_doc({
diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.js b/erpnext/stock/doctype/delivery_note/test_delivery_note.js
deleted file mode 100644
index 76f7989..0000000
--- a/erpnext/stock/doctype/delivery_note/test_delivery_note.js
+++ /dev/null
@@ -1,35 +0,0 @@
-QUnit.module('Stock');
-
-QUnit.test("test delivery note", function(assert) {
- assert.expect(2);
- let done = assert.async();
- frappe.run_serially([
- () => {
- return frappe.tests.make('Delivery Note', [
- {customer:'Test Customer 1'},
- {items: [
- [
- {'item_code': 'Test Product 1'},
- {'qty': 5},
- ]
- ]},
- {shipping_address_name: 'Test1-Shipping'},
- {contact_person: 'Contact 1-Test Customer 1'},
- {taxes_and_charges: 'TEST In State GST - FT'},
- {tc_name: 'Test Term 1'},
- {transporter_name:'TEST TRANSPORT'},
- {lr_no:'MH-04-FG 1111'}
- ]);
- },
- () => cur_frm.save(),
- () => {
- // get_item_details
- assert.ok(cur_frm.doc.items[0].item_name=='Test Product 1', "Item name correct");
- assert.ok(cur_frm.doc.grand_total==590, " Grand Total correct");
- },
- () => frappe.tests.click_button('Submit'),
- () => frappe.tests.click_button('Yes'),
- () => frappe.timeout(0.3),
- () => done()
- ]);
-});
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
deleted file mode 100644
index 9f1375f..0000000
--- a/erpnext/stock/doctype/delivery_note/test_delivery_note_with_margin.js
+++ /dev/null
@@ -1,36 +0,0 @@
-QUnit.module('Stock');
-
-QUnit.test("test delivery note with margin", function(assert) {
- assert.expect(3);
- let done = assert.async();
- frappe.run_serially([
- () => {
- return frappe.tests.make('Delivery Note', [
- {customer:'Test Customer 1'},
- {selling_price_list: 'Test-Selling-USD'},
- {currency: 'USD'},
- {items: [
- [
- {'item_code': 'Test Product 4'},
- {'qty': 1},
- {'margin_type': 'Amount'},
- {'margin_rate_or_amount': 10}
- ]
- ]},
- ]);
- },
-
- () => cur_frm.save(),
- () => {
- // get_rate_details
- assert.ok(cur_frm.doc.items[0].rate_with_margin == 210, "Margin rate correct");
- assert.ok(cur_frm.doc.items[0].base_rate_with_margin == cur_frm.doc.conversion_rate * 210, "Base margin rate correct");
- assert.ok(cur_frm.doc.total == 210, "Amount correct");
- },
-
- () => frappe.tests.click_button('Submit'),
- () => frappe.tests.click_button('Yes'),
- () => frappe.timeout(0.3),
- () => done()
- ]);
-});
diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
index a96c299..51c88be 100644
--- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
+++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
@@ -49,6 +49,7 @@
"pricing_rules",
"stock_uom_rate",
"is_free_item",
+ "grant_commission",
"section_break_25",
"net_rate",
"net_amount",
@@ -753,13 +754,20 @@
"no_copy": 1,
"options": "currency",
"read_only": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "grant_commission",
+ "fieldtype": "Check",
+ "label": "Grant Commission",
+ "read_only": 1
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-10-05 12:12:44.018872",
+ "modified": "2021-10-06 12:12:44.018872",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note Item",
diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json
index 50e9f0a..bbbebc4 100644
--- a/erpnext/stock/doctype/item/item.json
+++ b/erpnext/stock/doctype/item/item.json
@@ -88,6 +88,7 @@
"sales_details",
"sales_uom",
"is_sales_item",
+ "grant_commission",
"column_break3",
"max_discount",
"deferred_revenue",
@@ -344,8 +345,7 @@
"fieldname": "valuation_method",
"fieldtype": "Select",
"label": "Valuation Method",
- "options": "\nFIFO\nMoving Average",
- "set_only_once": 1
+ "options": "\nFIFO\nMoving Average"
},
{
"depends_on": "is_stock_item",
@@ -893,6 +893,12 @@
"fieldtype": "Check",
"label": "Published in Website",
"read_only": 1
+ },
+ {
+ "default": "1",
+ "fieldname": "grant_commission",
+ "fieldtype": "Check",
+ "label": "Grant Commission"
}
],
"icon": "fa fa-tag",
@@ -900,8 +906,7 @@
"image_field": "image",
"index_web_pages_for_search": 1,
"links": [],
- "max_attachments": 1,
- "modified": "2021-10-27 21:04:00.324786",
+ "modified": "2021-12-14 04:13:16.857534",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item",
@@ -969,7 +974,7 @@
"search_fields": "item_name,description,item_group,customer_code",
"show_name_in_global_search": 1,
"show_preview_popup": 1,
- "sort_field": "idx desc,modified desc",
+ "sort_field": "modified",
"sort_order": "DESC",
"title_field": "item_name",
"track_changes": 1
diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py
index 5b8b96e..dc54c3a 100644
--- a/erpnext/stock/doctype/item/item.py
+++ b/erpnext/stock/doctype/item/item.py
@@ -501,7 +501,6 @@
def recalculate_bin_qty(self, new_name):
from erpnext.stock.stock_balance import repost_stock
- frappe.db.auto_commit_on_many_writes = 1
existing_allow_negative_stock = frappe.db.get_value("Stock Settings", None, "allow_negative_stock")
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1)
@@ -515,7 +514,6 @@
repost_stock(new_name, warehouse)
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", existing_allow_negative_stock)
- frappe.db.auto_commit_on_many_writes = 0
def update_bom_item_desc(self):
if self.is_new():
diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py
index c0efa1e..4028d93 100644
--- a/erpnext/stock/doctype/item/test_item.py
+++ b/erpnext/stock/doctype/item/test_item.py
@@ -488,7 +488,7 @@
item_doc.save()
# Check values saved correctly
- barcodes = frappe.get_list(
+ barcodes = frappe.get_all(
'Item Barcode',
fields=['barcode', 'barcode_type'],
filters={'parent': item_code})
@@ -532,6 +532,17 @@
self.assertIsInstance(count, int)
self.assertTrue(count >= 0)
+ def test_index_creation(self):
+ "check if index is getting created in db"
+
+ indices = frappe.db.sql("show index from tabItem", as_dict=1)
+ expected_columns = {"item_code", "item_name", "item_group", "route"}
+ for index in indices:
+ expected_columns.discard(index.get("Column_name"))
+
+ if expected_columns:
+ self.fail(f"Expected db index on these columns: {', '.join(expected_columns)}")
+
def test_attribute_completions(self):
expected_attrs = {"Small", "Extra Small", "Extra Large", "Large", "2XL", "Medium"}
diff --git a/erpnext/stock/doctype/item/tests/test_item.js b/erpnext/stock/doctype/item/tests/test_item.js
deleted file mode 100644
index 7f7e72d..0000000
--- a/erpnext/stock/doctype/item/tests/test_item.js
+++ /dev/null
@@ -1,121 +0,0 @@
-QUnit.module('stock');
-QUnit.test("test: item", function (assert) {
- assert.expect(6);
- let done = assert.async();
- let keyboard_cost = 800;
- let screen_cost = 4000;
- let CPU_cost = 15000;
- let scrap_cost = 100;
- let no_of_items_to_stock = 100;
- let is_stock_item = 1;
- frappe.run_serially([
- // test item creation
- () => frappe.set_route("List", "Item"),
-
- // Create a keyboard item
- () => frappe.tests.make(
- "Item", [
- {item_code: "Keyboard"},
- {item_group: "Products"},
- {is_stock_item: is_stock_item},
- {standard_rate: keyboard_cost},
- {opening_stock: no_of_items_to_stock},
- {default_warehouse: "Stores - FT"}
- ]
- ),
- () => {
- assert.ok(cur_frm.doc.item_name.includes('Keyboard'),
- 'Item Keyboard created correctly');
- assert.ok(cur_frm.doc.item_code.includes('Keyboard'),
- 'item_code for Keyboard set correctly');
- assert.ok(cur_frm.doc.item_group.includes('Products'),
- 'item_group for Keyboard set correctly');
- assert.equal(cur_frm.doc.is_stock_item, is_stock_item,
- 'is_stock_item for Keyboard set correctly');
- assert.equal(cur_frm.doc.standard_rate, keyboard_cost,
- 'standard_rate for Keyboard set correctly');
- assert.equal(cur_frm.doc.opening_stock, no_of_items_to_stock,
- 'opening_stock for Keyboard set correctly');
- },
-
- // Create a Screen item
- () => frappe.tests.make(
- "Item", [
- {item_code: "Screen"},
- {item_group: "Products"},
- {is_stock_item: is_stock_item},
- {standard_rate: screen_cost},
- {opening_stock: no_of_items_to_stock},
- {default_warehouse: "Stores - FT"}
- ]
- ),
-
- // Create a CPU item
- () => frappe.tests.make(
- "Item", [
- {item_code: "CPU"},
- {item_group: "Products"},
- {is_stock_item: is_stock_item},
- {standard_rate: CPU_cost},
- {opening_stock: no_of_items_to_stock},
- {default_warehouse: "Stores - FT"}
- ]
- ),
-
- // Create a laptop item
- () => frappe.tests.make(
- "Item", [
- {item_code: "Laptop"},
- {item_group: "Products"},
- {default_warehouse: "Stores - FT"}
- ]
- ),
- () => frappe.tests.make(
- "Item", [
- {item_code: "Computer"},
- {item_group: "Products"},
- {is_stock_item: 0},
- ]
- ),
-
- // Create a scrap item
- () => frappe.tests.make(
- "Item", [
- {item_code: "Scrap item"},
- {item_group: "Products"},
- {is_stock_item: is_stock_item},
- {standard_rate: scrap_cost},
- {opening_stock: no_of_items_to_stock},
- {default_warehouse: "Stores - FT"}
- ]
- ),
- () => frappe.tests.make(
- "Item", [
- {item_code: "Test Product 4"},
- {item_group: "Products"},
- {is_stock_item: 1},
- {has_batch_no: 1},
- {create_new_batch: 1},
- {uoms:
- [
- [
- {uom:"Unit"},
- {conversion_factor: 10},
- ]
- ]
- },
- {taxes:
- [
- [
- {tax_type:"SGST - "+frappe.get_abbr(frappe.defaults.get_default("Company"))},
- {tax_rate: 0},
- ]
- ]},
- {has_serial_no: 1},
- {standard_rate: 100},
- {opening_stock: 100},
- ]
- ),
- () => done()
- ]);
-});
diff --git a/erpnext/stock/doctype/item_price/test_item_price.js b/erpnext/stock/doctype/item_price/test_item_price.js
deleted file mode 100644
index 49dbaa2..0000000
--- a/erpnext/stock/doctype/item_price/test_item_price.js
+++ /dev/null
@@ -1,22 +0,0 @@
-QUnit.module('Stock');
-
-QUnit.test("test item price", function(assert) {
- assert.expect(2);
- let done = assert.async();
- frappe.run_serially([
- () => {
- return frappe.tests.make('Item Price', [
- {price_list:'Test-Selling-USD'},
- {item_code: 'Test Product 4'},
- {price_list_rate: 200}
- ]);
- },
- () => cur_frm.save(),
- () => {
- assert.ok(cur_frm.doc.item_name == 'Test Product 4', "Item name correct");
- assert.ok(cur_frm.doc.price_list_rate == 200, "Price list rate correct");
- },
- () => frappe.timeout(0.3),
- () => done()
- ]);
-});
diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py
index d717c50..103e8d6 100644
--- a/erpnext/stock/doctype/material_request/material_request.py
+++ b/erpnext/stock/doctype/material_request/material_request.py
@@ -80,6 +80,9 @@
# NOTE: Since Item BOM and FG quantities are combined, using current data, it cannot be validated
# Though the creation of Material Request from a Production Plan can be rethought to fix this
+ self.reset_default_field_value("set_warehouse", "items", "warehouse")
+ self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse")
+
def set_title(self):
'''Set title as comma separated list of items'''
if not self.title:
diff --git a/erpnext/stock/doctype/material_request/tests/test_material_request.js b/erpnext/stock/doctype/material_request/tests/test_material_request.js
deleted file mode 100644
index a2cd03b..0000000
--- a/erpnext/stock/doctype/material_request/tests/test_material_request.js
+++ /dev/null
@@ -1,39 +0,0 @@
-QUnit.module('Stock');
-
-QUnit.test("test material request", function(assert) {
- assert.expect(5);
- let done = assert.async();
- frappe.run_serially([
- () => {
- return frappe.tests.make('Material Request', [
- {items: [
- [
- {'schedule_date': frappe.datetime.add_days(frappe.datetime.nowdate(), 5)},
- {'qty': 5},
- {'item_code': 'Test Product 1'},
- ],
- [
- {'schedule_date': frappe.datetime.add_days(frappe.datetime.nowdate(), 6)},
- {'qty': 2},
- {'item_code': 'Test Product 2'},
- ]
- ]},
- ]);
- },
- () => cur_frm.save(),
- () => {
- assert.ok(cur_frm.doc.schedule_date == frappe.datetime.add_days(frappe.datetime.now_date(), 5), "Schedule Date correct");
-
- // get_item_details
- assert.ok(cur_frm.doc.items[0].item_name=='Test Product 1', "Item name correct");
- assert.ok(cur_frm.doc.items[0].schedule_date == frappe.datetime.add_days(frappe.datetime.now_date(), 5), "Schedule Date correct");
-
- assert.ok(cur_frm.doc.items[1].item_name=='Test Product 2', "Item name correct");
- assert.ok(cur_frm.doc.items[1].schedule_date == frappe.datetime.add_days(frappe.datetime.now_date(), 6), "Schedule Date correct");
- },
- () => frappe.tests.click_button('Submit'),
- () => frappe.tests.click_button('Yes'),
- () => frappe.timeout(0.3),
- () => 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
deleted file mode 100644
index 6fb55ae..0000000
--- a/erpnext/stock/doctype/material_request/tests/test_material_request_from_bom.js
+++ /dev/null
@@ -1,27 +0,0 @@
-QUnit.module('manufacturing');
-
-QUnit.test("test material request get items from BOM", function(assert) {
- assert.expect(4);
- let done = assert.async();
- frappe.run_serially([
- () => frappe.set_route('Form', 'BOM'),
- () => frappe.timeout(3),
- () => frappe.click_button('Get Items from BOM'),
- () => frappe.timeout(3),
- () => {
- assert.ok(cur_dialog, 'dialog appeared');
- },
- () => cur_dialog.set_value('bom', 'Laptop'),
- () => cur_dialog.set_value('warehouse', 'Laptop Scrap Warehouse'),
- () => frappe.click_button('Get Items from BOM'),
- () => frappe.timeout(3),
- () => {
- assert.ok(cur_frm.doc.items[0].item_code, "First row is not empty");
- assert.ok(cur_frm.doc.items[0].item_name, "Item name is not empty");
- assert.equal(cur_frm.doc.items[0].item_name, "Laptop", cur_frm.doc.items[0].item_name);
- },
- () => cur_frm.doc.items[0].schedule_date = '2017-12-12',
- () => cur_frm.save(),
- () => 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
deleted file mode 100644
index 137079b..0000000
--- a/erpnext/stock/doctype/material_request/tests/test_material_request_type_manufacture.js
+++ /dev/null
@@ -1,29 +0,0 @@
-QUnit.module('Stock');
-
-QUnit.test("test material request", function(assert) {
- assert.expect(1);
- let done = assert.async();
- frappe.run_serially([
- () => {
- return frappe.tests.make('Material Request', [
- {material_request_type:'Manufacture'},
- {items: [
- [
- {'schedule_date': frappe.datetime.add_days(frappe.datetime.nowdate(), 5)},
- {'qty': 5},
- {'item_code': 'Test Product 1'},
- ]
- ]},
- ]);
- },
- () => cur_frm.save(),
- () => {
- // get_item_details
- assert.ok(cur_frm.doc.items[0].item_name=='Test Product 1', "Item name correct");
- },
- () => frappe.tests.click_button('Submit'),
- () => frappe.tests.click_button('Yes'),
- () => frappe.timeout(0.3),
- () => 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
deleted file mode 100644
index b03a854..0000000
--- a/erpnext/stock/doctype/material_request/tests/test_material_request_type_material_issue.js
+++ /dev/null
@@ -1,29 +0,0 @@
-QUnit.module('Stock');
-
-QUnit.test("test material request for issue", function(assert) {
- assert.expect(1);
- let done = assert.async();
- frappe.run_serially([
- () => {
- return frappe.tests.make('Material Request', [
- {material_request_type:'Material Issue'},
- {items: [
- [
- {'schedule_date': frappe.datetime.add_days(frappe.datetime.nowdate(), 5)},
- {'qty': 5},
- {'item_code': 'Test Product 1'},
- ]
- ]},
- ]);
- },
- () => cur_frm.save(),
- () => {
- // get_item_details
- assert.ok(cur_frm.doc.items[0].item_name=='Test Product 1', "Item name correct");
- },
- () => frappe.tests.click_button('Submit'),
- () => frappe.tests.click_button('Yes'),
- () => frappe.timeout(0.3),
- () => 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
deleted file mode 100644
index 7c62c2e..0000000
--- a/erpnext/stock/doctype/material_request/tests/test_material_request_type_material_transfer.js
+++ /dev/null
@@ -1,29 +0,0 @@
-QUnit.module('Stock');
-
-QUnit.test("test material request for transfer", function(assert) {
- assert.expect(1);
- let done = assert.async();
- frappe.run_serially([
- () => {
- return frappe.tests.make('Material Request', [
- {material_request_type:'Manufacture'},
- {items: [
- [
- {'schedule_date': frappe.datetime.add_days(frappe.datetime.nowdate(), 5)},
- {'qty': 5},
- {'item_code': 'Test Product 1'},
- ]
- ]},
- ]);
- },
- () => cur_frm.save(),
- () => {
- // get_item_details
- assert.ok(cur_frm.doc.items[0].item_name=='Test Product 1', "Item name correct");
- },
- () => frappe.tests.click_button('Submit'),
- () => frappe.tests.click_button('Yes'),
- () => frappe.timeout(0.3),
- () => done()
- ]);
-});
diff --git a/erpnext/stock/doctype/packed_item/packed_item.py b/erpnext/stock/doctype/packed_item/packed_item.py
index 3f73093..e4091c4 100644
--- a/erpnext/stock/doctype/packed_item/packed_item.py
+++ b/erpnext/stock/doctype/packed_item/packed_item.py
@@ -108,9 +108,32 @@
packed_items = doc.get("packed_items")
doc.set("packed_items", [])
+
for d in packed_items:
if d not in delete_list:
- doc.append("packed_items", d)
+ add_item_to_packing_list(doc, d)
+
+def add_item_to_packing_list(doc, packed_item):
+ doc.append("packed_items", {
+ 'parent_item': packed_item.parent_item,
+ 'item_code': packed_item.item_code,
+ 'item_name': packed_item.item_name,
+ 'uom': packed_item.uom,
+ 'qty': packed_item.qty,
+ 'rate': packed_item.rate,
+ 'conversion_factor': packed_item.conversion_factor,
+ 'description': packed_item.description,
+ 'warehouse': packed_item.warehouse,
+ 'batch_no': packed_item.batch_no,
+ 'actual_batch_qty': packed_item.actual_batch_qty,
+ 'serial_no': packed_item.serial_no,
+ 'target_warehouse': packed_item.target_warehouse,
+ 'actual_qty': packed_item.actual_qty,
+ 'projected_qty': packed_item.projected_qty,
+ 'incoming_rate': packed_item.incoming_rate,
+ 'prevdoc_doctype': packed_item.prevdoc_doctype,
+ 'parent_detail_docname': packed_item.parent_detail_docname
+ })
def update_product_bundle_price(doc, parent_items):
"""Updates the prices of Product Bundles based on the rates of the Items in the bundle."""
@@ -128,7 +151,8 @@
else:
update_parent_item_price(doc, parent_items[parent_items_index][0], bundle_price)
- bundle_price = 0
+ bundle_item_rate = bundle_item.rate if bundle_item.rate else 0
+ bundle_price = bundle_item.qty * bundle_item_rate
parent_items_index += 1
# for the last product bundle
diff --git a/erpnext/stock/doctype/packing_slip_item/packing_slip_item.json b/erpnext/stock/doctype/packing_slip_item/packing_slip_item.json
index 29c4193..4270839 100644
--- a/erpnext/stock/doctype/packing_slip_item/packing_slip_item.json
+++ b/erpnext/stock/doctype/packing_slip_item/packing_slip_item.json
@@ -1,451 +1,140 @@
{
- "allow_copy": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "autoname": "hash",
- "beta": 0,
- "creation": "2013-04-08 13:10:16",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "Document",
- "editable_grid": 1,
- "engine": "InnoDB",
+ "actions": [],
+ "autoname": "hash",
+ "creation": "2013-04-08 13:10:16",
+ "doctype": "DocType",
+ "document_type": "Document",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "item_code",
+ "column_break_2",
+ "item_name",
+ "batch_no",
+ "desc_section",
+ "description",
+ "quantity_section",
+ "qty",
+ "net_weight",
+ "column_break_10",
+ "stock_uom",
+ "weight_uom",
+ "page_break",
+ "dn_detail"
+ ],
"fields": [
{
- "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": 1,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Item Code",
- "length": 0,
- "no_copy": 0,
- "options": "Item",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "print_width": "100px",
- "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_global_search": 1,
+ "in_list_view": 1,
+ "label": "Item Code",
+ "options": "Item",
+ "print_width": "100px",
+ "reqd": 1,
"width": "100px"
- },
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_2",
- "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_2",
+ "fieldtype": "Column Break"
+ },
{
- "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,
- "options": "item_code.item_name",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "print_width": "200px",
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0,
+ "fetch_from": "item_code.item_name",
+ "fieldname": "item_name",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Item Name",
+ "print_width": "200px",
+ "read_only": 1,
"width": "200px"
- },
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "batch_no",
- "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": "Batch No",
- "length": 0,
- "no_copy": 0,
- "options": "Batch",
- "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": "batch_no",
+ "fieldtype": "Link",
+ "label": "Batch No",
+ "options": "Batch"
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 1,
- "columns": 0,
- "fieldname": "desc_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": "Description",
- "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
- },
+ "collapsible": 1,
+ "fieldname": "desc_section",
+ "fieldtype": "Section Break",
+ "label": "Description"
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "description",
- "fieldtype": "Text Editor",
- "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": "Description",
- "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": "description",
+ "fieldtype": "Text Editor",
+ "label": "Description"
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "quantity_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": "Quantity",
- "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_section",
+ "fieldtype": "Section Break",
+ "label": "Quantity"
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "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": "Quantity",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "print_width": "100px",
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0,
+ "fieldname": "qty",
+ "fieldtype": "Float",
+ "in_list_view": 1,
+ "label": "Quantity",
+ "print_width": "100px",
+ "reqd": 1,
"width": "100px"
- },
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "net_weight",
- "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": "Net Weight",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "print_width": "100px",
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0,
+ "fieldname": "net_weight",
+ "fieldtype": "Float",
+ "in_list_view": 1,
+ "label": "Net Weight",
+ "print_width": "100px",
"width": "100px"
- },
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_10",
- "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_10",
+ "fieldtype": "Column Break"
+ },
{
- "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": "UOM",
- "length": 0,
- "no_copy": 0,
- "options": "UOM",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "print_width": "100px",
- "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": "UOM",
+ "options": "UOM",
+ "print_width": "100px",
+ "read_only": 1,
"width": "100px"
- },
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "weight_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": "Weight UOM",
- "length": 0,
- "no_copy": 0,
- "options": "UOM",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "print_width": "100px",
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0,
+ "fieldname": "weight_uom",
+ "fieldtype": "Link",
+ "label": "Weight UOM",
+ "options": "UOM",
+ "print_width": "100px",
"width": "100px"
- },
+ },
{
- "allow_on_submit": 1,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "page_break",
- "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": "Page Break",
- "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,
- "unique": 0
- },
+ "allow_on_submit": 1,
+ "default": "0",
+ "fieldname": "page_break",
+ "fieldtype": "Check",
+ "in_list_view": 1,
+ "label": "Page Break"
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "dn_detail",
- "fieldtype": "Data",
- "hidden": 1,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "DN Detail",
- "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,
- "unique": 0
+ "fieldname": "dn_detail",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "in_list_view": 1,
+ "label": "DN Detail"
}
- ],
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 1,
- "image_view": 0,
- "in_create": 0,
-
- "is_submittable": 0,
- "issingle": 0,
- "istable": 1,
- "max_attachments": 0,
- "modified": "2018-06-01 07:21:58.220980",
- "modified_by": "Administrator",
- "module": "Stock",
- "name": "Packing Slip Item",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "track_changes": 1,
- "track_seen": 0
+ ],
+ "idx": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-12-14 01:22:00.715935",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Packing Slip Item",
+ "naming_rule": "Random",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/price_list/test_price_list_uom.js b/erpnext/stock/doctype/price_list/test_price_list_uom.js
deleted file mode 100644
index 3896c0e..0000000
--- a/erpnext/stock/doctype/price_list/test_price_list_uom.js
+++ /dev/null
@@ -1,58 +0,0 @@
-QUnit.module('Price List');
-
-QUnit.test("test price list with uom dependancy", function(assert) {
- assert.expect(2);
- let done = assert.async();
- frappe.run_serially([
-
- () => frappe.set_route('Form', 'Price List', 'Standard Buying'),
- () => {
- cur_frm.set_value('price_not_uom_dependent','1');
- frappe.timeout(1);
- },
- () => cur_frm.save(),
-
- () => frappe.timeout(1),
-
- () => {
- return frappe.tests.make('Item Price', [
- {price_list:'Standard Buying'},
- {item_code: 'Test Product 3'},
- {price_list_rate: 200}
- ]);
- },
-
- () => cur_frm.save(),
-
- () => {
- return frappe.tests.make('Purchase Order', [
- {supplier: 'Test Supplier'},
- {currency: 'INR'},
- {buying_price_list: 'Standard Buying'},
- {items: [
- [
- {"item_code": 'Test Product 3'},
- {"schedule_date": frappe.datetime.add_days(frappe.datetime.now_date(), 2)},
- {"uom": 'Nos'},
- {"conversion_factor": 3}
- ]
- ]},
-
- ]);
- },
-
- () => cur_frm.save(),
- () => frappe.timeout(0.3),
-
- () => {
- assert.ok(cur_frm.doc.items[0].item_name == 'Test Product 3', "Item code correct");
- assert.ok(cur_frm.doc.items[0].price_list_rate == 200, "Price list rate correct");
- },
-
- () => frappe.tests.click_button('Submit'),
- () => frappe.tests.click_button('Yes'),
- () => frappe.timeout(1),
-
- () => done()
- ]);
-});
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index 762f45f..c97b306 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -118,6 +118,10 @@
if getdate(self.posting_date) > getdate(nowdate()):
throw(_("Posting Date cannot be future date"))
+ self.reset_default_field_value("set_warehouse", "items", "warehouse")
+ self.reset_default_field_value("rejected_warehouse", "items", "rejected_warehouse")
+ self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse")
+
def validate_cwip_accounts(self):
for item in self.get('items'):
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.js
deleted file mode 100644
index d1f4485..0000000
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.js
+++ /dev/null
@@ -1,42 +0,0 @@
-QUnit.module('Stock');
-
-QUnit.test("test Purchase Receipt", function(assert) {
- assert.expect(4);
- let done = assert.async();
- frappe.run_serially([
- () => {
- return frappe.tests.make('Purchase Receipt', [
- {supplier: 'Test Supplier'},
- {items: [
- [
- {'received_qty': 5},
- {'qty': 4},
- {'item_code': 'Test Product 1'},
- {'uom': 'Nos'},
- {'warehouse':'Stores - '+frappe.get_abbr(frappe.defaults.get_default('Company'))},
- {'rejected_warehouse':'Work In Progress - '+frappe.get_abbr(frappe.defaults.get_default('Company'))},
- ]
- ]},
- {taxes_and_charges: 'TEST In State GST - FT'},
- {tc_name: 'Test Term 1'},
- {terms: 'This is Test'}
- ]);
- },
- () => cur_frm.save(),
- () => {
- // get_item_details
- assert.ok(cur_frm.doc.items[0].item_name=='Test Product 1', "Item name correct");
- // get tax details
- assert.ok(cur_frm.doc.taxes_and_charges=='TEST In State GST - FT', "Tax details correct");
- // get tax account head details
- assert.ok(cur_frm.doc.taxes[0].account_head=='CGST - '+frappe.get_abbr(frappe.defaults.get_default('Company')), " Account Head abbr correct");
- // grand_total Calculated
- assert.ok(cur_frm.doc.grand_total==472, "Grad Total correct");
-
- },
- () => frappe.tests.click_button('Submit'),
- () => frappe.tests.click_button('Yes'),
- () => frappe.timeout(0.3),
- () => done()
- ]);
-});
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index 102730b..2909a2d 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -10,6 +10,7 @@
import erpnext
from erpnext.accounts.doctype.account.test_account import get_inventory_account
+from erpnext.controllers.buying_controller import QtyMismatchError
from erpnext.stock.doctype.item.test_item import create_item, make_item
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice
from erpnext.stock.doctype.serial_no.serial_no import SerialNoDuplicateError, get_serial_nos
@@ -22,20 +23,54 @@
def setUp(self):
frappe.db.set_value("Buying Settings", None, "allow_multiple_items", 1)
+ def test_purchase_receipt_received_qty(self):
+ """
+ 1. Test if received qty is validated against accepted + rejected
+ 2. Test if received qty is auto set on save
+ """
+ pr = make_purchase_receipt(
+ qty=1,
+ rejected_qty=1,
+ received_qty=3,
+ item_code="_Test Item Home Desktop 200",
+ do_not_save=True
+ )
+ self.assertRaises(QtyMismatchError, pr.save)
+
+ pr.items[0].received_qty = 0
+ pr.save()
+ self.assertEqual(pr.items[0].received_qty, 2)
+
+ # teardown
+ pr.delete()
+
def test_reverse_purchase_receipt_sle(self):
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'])
+ sl_entry = frappe.db.get_all(
+ "Stock Ledger Entry",
+ {
+ "voucher_type": "Purchase Receipt",
+ "voucher_no": pr.name
+ },
+ ['actual_qty']
+ )
self.assertEqual(len(sl_entry), 1)
self.assertEqual(sl_entry[0].actual_qty, 0.5)
pr.cancel()
- sl_entry_cancelled = frappe.db.get_all("Stock Ledger Entry", {"voucher_type": "Purchase Receipt",
- "voucher_no": pr.name}, ['actual_qty'], order_by='creation')
+ sl_entry_cancelled = frappe.db.get_all(
+ "Stock Ledger Entry",
+ {
+ "voucher_type": "Purchase Receipt",
+ "voucher_no": pr.name
+ },
+ ['actual_qty'],
+ order_by='creation'
+ )
self.assertEqual(len(sl_entry_cancelled), 2)
self.assertEqual(sl_entry_cancelled[1].actual_qty, -0.5)
@@ -61,8 +96,15 @@
}]
}).insert()
- template = frappe.db.get_value('Payment Terms Template', '_Test Payment Terms Template For Purchase Invoice')
- old_template_in_supplier = frappe.db.get_value("Supplier", "_Test Supplier", "payment_terms")
+ template = frappe.db.get_value(
+ "Payment Terms Template",
+ "_Test Payment Terms Template For Purchase Invoice"
+ )
+ old_template_in_supplier = frappe.db.get_value(
+ "Supplier",
+ "_Test Supplier",
+ "payment_terms"
+ )
frappe.db.set_value("Supplier", "_Test Supplier", "payment_terms", template)
pr = make_purchase_receipt(do_not_save=True)
@@ -88,30 +130,59 @@
# teardown
pi.delete() # draft PI
pr.cancel()
- frappe.db.set_value("Supplier", "_Test Supplier", "payment_terms", old_template_in_supplier)
- frappe.get_doc('Payment Terms Template', '_Test Payment Terms Template For Purchase Invoice').delete()
+ frappe.db.set_value(
+ "Supplier",
+ "_Test Supplier",
+ "payment_terms",
+ old_template_in_supplier
+ )
+ frappe.get_doc(
+ "Payment Terms Template",
+ "_Test Payment Terms Template For Purchase Invoice"
+ ).delete()
def test_purchase_receipt_no_gl_entry(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
- company = frappe.db.get_value('Warehouse', '_Test Warehouse - _TC', 'company')
-
- existing_bin_qty, existing_bin_stock_value = frappe.db.get_value("Bin", {"item_code": "_Test Item",
- "warehouse": "_Test Warehouse - _TC"}, ["actual_qty", "stock_value"])
+ existing_bin_qty, existing_bin_stock_value = frappe.db.get_value(
+ "Bin",
+ {
+ "item_code": "_Test Item",
+ "warehouse": "_Test Warehouse - _TC"
+ },
+ ["actual_qty", "stock_value"]
+ )
if existing_bin_qty < 0:
- make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=abs(existing_bin_qty))
+ make_stock_entry(
+ item_code="_Test Item",
+ target="_Test Warehouse - _TC",
+ qty=abs(existing_bin_qty)
+ )
pr = make_purchase_receipt()
- stock_value_difference = frappe.db.get_value("Stock Ledger Entry",
- {"voucher_type": "Purchase Receipt", "voucher_no": pr.name,
- "item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC"}, "stock_value_difference")
+ stock_value_difference = frappe.db.get_value(
+ "Stock Ledger Entry",
+ {
+ "voucher_type": "Purchase Receipt",
+ "voucher_no": pr.name,
+ "item_code": "_Test Item",
+ "warehouse": "_Test Warehouse - _TC"
+ },
+ "stock_value_difference"
+ )
self.assertEqual(stock_value_difference, 250)
- current_bin_stock_value = frappe.db.get_value("Bin", {"item_code": "_Test Item",
- "warehouse": "_Test Warehouse - _TC"}, "stock_value")
+ current_bin_stock_value = frappe.db.get_value(
+ "Bin",
+ {
+ "item_code": "_Test Item",
+ "warehouse": "_Test Warehouse - _TC"
+ },
+ "stock_value"
+ )
self.assertEqual(current_bin_stock_value, existing_bin_stock_value + 250)
self.assertFalse(get_gl_entries("Purchase Receipt", pr.name))
@@ -133,13 +204,17 @@
pr = make_purchase_receipt(item_code=item.name, qty=5, rate=500)
- self.assertTrue(frappe.db.get_value('Batch', {'item': item.name, 'reference_name': pr.name}))
+ self.assertTrue(
+ frappe.db.get_value('Batch', {'item': item.name, 'reference_name': pr.name})
+ )
pr.load_from_db()
batch_no = pr.items[0].batch_no
pr.cancel()
- self.assertFalse(frappe.db.get_value('Batch', {'item': item.name, 'reference_name': pr.name}))
+ self.assertFalse(
+ frappe.db.get_value('Batch', {'item': item.name, 'reference_name': pr.name})
+ )
self.assertFalse(frappe.db.get_all('Serial No', {'batch_no': batch_no}))
def test_duplicate_serial_nos(self):
@@ -158,42 +233,78 @@
pr = make_purchase_receipt(item_code=item.name, qty=2, rate=500)
pr.load_from_db()
- serial_nos = frappe.db.get_value('Stock Ledger Entry',
- {'voucher_type': 'Purchase Receipt', 'voucher_no': pr.name, 'item_code': item.name}, 'serial_no')
+ serial_nos = frappe.db.get_value(
+ "Stock Ledger Entry",
+ {
+ "voucher_type": "Purchase Receipt",
+ "voucher_no": pr.name,
+ "item_code": item.name
+ },
+ "serial_no"
+ )
serial_nos = get_serial_nos(serial_nos)
self.assertEquals(get_serial_nos(pr.items[0].serial_no), serial_nos)
# Then tried to receive same serial nos in difference company
- pr_different_company = make_purchase_receipt(item_code=item.name, qty=2, rate=500,
- serial_no='\n'.join(serial_nos), company='_Test Company 1', do_not_submit=True,
- warehouse = 'Stores - _TC1')
+ pr_different_company = make_purchase_receipt(
+ item_code=item.name,
+ qty=2,
+ rate=500,
+ serial_no='\n'.join(serial_nos),
+ company='_Test Company 1',
+ do_not_submit=True,
+ warehouse = 'Stores - _TC1'
+ )
self.assertRaises(SerialNoDuplicateError, pr_different_company.submit)
# Then made delivery note to remove the serial nos from stock
- dn = create_delivery_note(item_code=item.name, qty=2, rate = 1500, serial_no='\n'.join(serial_nos))
+ dn = create_delivery_note(
+ item_code=item.name,
+ qty=2,
+ rate=1500,
+ serial_no='\n'.join(serial_nos)
+ )
dn.load_from_db()
self.assertEquals(get_serial_nos(dn.items[0].serial_no), serial_nos)
posting_date = add_days(today(), -3)
# Try to receive same serial nos again in the same company with backdated.
- pr1 = make_purchase_receipt(item_code=item.name, qty=2, rate=500,
- posting_date=posting_date, serial_no='\n'.join(serial_nos), do_not_submit=True)
+ pr1 = make_purchase_receipt(
+ item_code=item.name,
+ qty=2,
+ rate=500,
+ posting_date=posting_date,
+ serial_no='\n'.join(serial_nos),
+ do_not_submit=True
+ )
self.assertRaises(SerialNoExistsInFutureTransaction, pr1.submit)
# Try to receive same serial nos with different company with backdated.
- pr2 = make_purchase_receipt(item_code=item.name, qty=2, rate=500,
- posting_date=posting_date, serial_no='\n'.join(serial_nos), company='_Test Company 1', do_not_submit=True,
- warehouse = 'Stores - _TC1')
+ pr2 = make_purchase_receipt(
+ item_code=item.name,
+ qty=2,
+ rate=500,
+ posting_date=posting_date,
+ serial_no='\n'.join(serial_nos),
+ company="_Test Company 1",
+ do_not_submit=True,
+ warehouse="Stores - _TC1"
+ )
self.assertRaises(SerialNoExistsInFutureTransaction, pr2.submit)
# Receive the same serial nos after the delivery note posting date and time
- make_purchase_receipt(item_code=item.name, qty=2, rate=500, serial_no='\n'.join(serial_nos))
+ make_purchase_receipt(
+ item_code=item.name,
+ qty=2,
+ rate=500,
+ serial_no='\n'.join(serial_nos)
+ )
# Raise the error for backdated deliver note entry cancel
self.assertRaises(SerialNoExistsInFutureTransaction, dn.cancel)
@@ -236,11 +347,23 @@
def test_subcontracting(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
- frappe.db.set_value("Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", "BOM")
- make_stock_entry(item_code="_Test Item", target="_Test Warehouse 1 - _TC", qty=100, basic_rate=100)
- make_stock_entry(item_code="_Test Item Home Desktop 100", target="_Test Warehouse 1 - _TC",
- qty=100, basic_rate=100)
- pr = make_purchase_receipt(item_code="_Test FG Item", qty=10, rate=500, is_subcontracted="Yes")
+ frappe.db.set_value(
+ "Buying Settings", None,
+ "backflush_raw_materials_of_subcontract_based_on", "BOM"
+ )
+
+ make_stock_entry(
+ item_code="_Test Item", qty=100,
+ target="_Test Warehouse 1 - _TC", basic_rate=100
+ )
+ make_stock_entry(
+ item_code="_Test Item Home Desktop 100", qty=100,
+ target="_Test Warehouse 1 - _TC", basic_rate=100
+ )
+ pr = make_purchase_receipt(
+ item_code="_Test FG Item", qty=10,
+ rate=500, is_subcontracted="Yes"
+ )
self.assertEqual(len(pr.get("supplied_items")), 2)
rm_supp_cost = sum(d.amount for d in pr.get("supplied_items"))
@@ -250,17 +373,33 @@
def test_subcontracting_gle_fg_item_rate_zero(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
- frappe.db.set_value("Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", "BOM")
+ frappe.db.set_value(
+ "Buying Settings", None,
+ "backflush_raw_materials_of_subcontract_based_on", "BOM"
+ )
- se1 = make_stock_entry(item_code="_Test Item", target="Work In Progress - TCP1",
- qty=100, basic_rate=100, company="_Test Company with perpetual inventory")
+ se1 = make_stock_entry(
+ item_code="_Test Item",
+ target="Work In Progress - TCP1",
+ qty=100, basic_rate=100,
+ company="_Test Company with perpetual inventory"
+ )
- se2 = make_stock_entry(item_code="_Test Item Home Desktop 100", target="Work In Progress - TCP1",
- qty=100, basic_rate=100, company="_Test Company with perpetual inventory")
+ se2 = make_stock_entry(
+ item_code="_Test Item Home Desktop 100",
+ target="Work In Progress - TCP1",
+ qty=100, basic_rate=100,
+ company="_Test Company with perpetual inventory"
+ )
- pr = make_purchase_receipt(item_code="_Test FG Item", qty=10, rate=0, is_subcontracted="Yes",
- company="_Test Company with perpetual inventory", warehouse='Stores - TCP1',
- supplier_warehouse='Work In Progress - TCP1')
+ pr = make_purchase_receipt(
+ item_code="_Test FG Item",
+ qty=10, rate=0,
+ is_subcontracted="Yes",
+ company="_Test Company with perpetual inventory",
+ warehouse="Stores - TCP1",
+ supplier_warehouse="Work In Progress - TCP1"
+ )
gl_entries = get_gl_entries("Purchase Receipt", pr.name)
@@ -294,13 +433,23 @@
po = create_purchase_order(item_code=item_code, qty=1, include_exploded_items=0,
is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC")
- #stock raw materials in a warehouse before transfer
- se1 = make_stock_entry(target="_Test Warehouse - _TC",
- item_code = "Test Extra Item 1", qty=10, basic_rate=100)
- se2 = make_stock_entry(target="_Test Warehouse - _TC",
- item_code = "_Test FG Item", qty=1, basic_rate=100)
- se3 = make_stock_entry(target="_Test Warehouse - _TC",
- item_code = "Test Extra Item 2", qty=1, basic_rate=100)
+ # stock raw materials in a warehouse before transfer
+ make_stock_entry(
+ target="_Test Warehouse - _TC",
+ item_code = "Test Extra Item 1",
+ qty=10, basic_rate=100
+ )
+ make_stock_entry(
+ target="_Test Warehouse - _TC",
+ item_code = "_Test FG Item",
+ qty=1, basic_rate=100
+ )
+ make_stock_entry(
+ target="_Test Warehouse - _TC",
+ item_code = "Test Extra Item 2",
+ qty=1, basic_rate=100
+ )
+
rm_items = [
{
"item_code": item_code,
@@ -334,11 +483,17 @@
def test_serial_no_supplier(self):
pr = make_purchase_receipt(item_code="_Test Serialized Item With Series", qty=1)
- self.assertEqual(frappe.db.get_value("Serial No", pr.get("items")[0].serial_no, "supplier"),
- pr.supplier)
+ pr_row_1_serial_no = pr.get("items")[0].serial_no
+
+ self.assertEqual(
+ frappe.db.get_value("Serial No", pr_row_1_serial_no, "supplier"),
+ pr.supplier
+ )
pr.cancel()
- self.assertFalse(frappe.db.get_value("Serial No", pr.get("items")[0].serial_no, "warehouse"))
+ self.assertFalse(
+ frappe.db.get_value("Serial No", pr_row_1_serial_no, "warehouse")
+ )
def test_rejected_serial_no(self):
pr = frappe.copy_doc(test_records[0])
@@ -365,18 +520,33 @@
pr.cancel()
def test_purchase_return_partial(self):
- pr = make_purchase_receipt(company="_Test Company with perpetual inventory",
- warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1")
+ pr = make_purchase_receipt(
+ company="_Test Company with perpetual inventory",
+ warehouse = "Stores - TCP1",
+ supplier_warehouse = "Work in Progress - TCP1"
+ )
- return_pr = make_purchase_receipt(company="_Test Company with perpetual inventory",
- warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1",
- is_return=1, return_against=pr.name, qty=-2, do_not_submit=1)
+ return_pr = make_purchase_receipt(
+ company="_Test Company with perpetual inventory",
+ warehouse = "Stores - TCP1",
+ supplier_warehouse = "Work in Progress - TCP1",
+ is_return=1,
+ return_against=pr.name,
+ qty=-2,
+ do_not_submit=1
+ )
return_pr.items[0].purchase_receipt_item = pr.items[0].name
return_pr.submit()
# check sle
- outgoing_rate = frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Purchase Receipt",
- "voucher_no": return_pr.name}, "outgoing_rate")
+ outgoing_rate = frappe.db.get_value(
+ "Stock Ledger Entry",
+ {
+ "voucher_type": "Purchase Receipt",
+ "voucher_no": return_pr.name
+ },
+ "outgoing_rate"
+ )
self.assertEqual(outgoing_rate, 50)
@@ -440,11 +610,21 @@
pr.cancel()
def test_purchase_return_full(self):
- pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1",
- supplier_warehouse = "Work in Progress - TCP1")
+ pr = make_purchase_receipt(
+ company="_Test Company with perpetual inventory",
+ warehouse = "Stores - TCP1",
+ supplier_warehouse = "Work in Progress - TCP1"
+ )
- return_pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1",
- supplier_warehouse = "Work in Progress - TCP1", is_return=1, return_against=pr.name, qty=-5, do_not_submit=1)
+ return_pr = make_purchase_receipt(
+ company="_Test Company with perpetual inventory",
+ warehouse = "Stores - TCP1",
+ supplier_warehouse = "Work in Progress - TCP1",
+ is_return=1,
+ return_against=pr.name,
+ qty=-5,
+ do_not_submit=1
+ )
return_pr.items[0].purchase_receipt_item = pr.items[0].name
return_pr.submit()
@@ -466,15 +646,41 @@
rejected_warehouse="_Test Rejected Warehouse - TCP1"
if not frappe.db.exists("Warehouse", rejected_warehouse):
- get_warehouse(company = "_Test Company with perpetual inventory",
- abbr = " - TCP1", warehouse_name = "_Test Rejected Warehouse").name
+ get_warehouse(
+ company = "_Test Company with perpetual inventory",
+ abbr = " - TCP1",
+ warehouse_name = "_Test Rejected Warehouse"
+ ).name
- pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1", received_qty=4, qty=2, rejected_warehouse=rejected_warehouse)
+ pr = make_purchase_receipt(
+ company="_Test Company with perpetual inventory",
+ warehouse = "Stores - TCP1",
+ supplier_warehouse = "Work in Progress - TCP1",
+ qty=2,
+ rejected_qty=2,
+ rejected_warehouse=rejected_warehouse
+ )
- return_pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1", is_return=1, return_against=pr.name, received_qty = -4, qty=-2, rejected_warehouse=rejected_warehouse)
+ return_pr = make_purchase_receipt(
+ company="_Test Company with perpetual inventory",
+ warehouse = "Stores - TCP1",
+ supplier_warehouse = "Work in Progress - TCP1",
+ is_return=1,
+ return_against=pr.name,
+ qty=-2,
+ rejected_qty = -2,
+ rejected_warehouse=rejected_warehouse
+ )
- actual_qty = frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Purchase Receipt",
- "voucher_no": return_pr.name, 'warehouse': return_pr.items[0].rejected_warehouse}, "actual_qty")
+ actual_qty = frappe.db.get_value(
+ "Stock Ledger Entry",
+ {
+ "voucher_type": "Purchase Receipt",
+ "voucher_no": return_pr.name,
+ "warehouse": return_pr.items[0].rejected_warehouse
+ },
+ "actual_qty"
+ )
self.assertEqual(actual_qty, -2)
@@ -499,8 +705,13 @@
"purchase_document_no": pr.name
})
- return_pr = make_purchase_receipt(item_code="_Test Serialized Item With Series", qty=-1,
- is_return=1, return_against=pr.name, serial_no=serial_no)
+ return_pr = make_purchase_receipt(
+ item_code="_Test Serialized Item With Series",
+ qty=-1,
+ is_return=1,
+ return_against=pr.name,
+ serial_no=serial_no
+ )
_check_serial_no_values(serial_no, {
"warehouse": "",
@@ -522,9 +733,21 @@
})
row.db_update()
- pr = make_purchase_receipt(item_code=item_code, qty=1, uom="Box", conversion_factor=1.0)
- return_pr = make_purchase_receipt(item_code=item_code, qty=-10, uom="Unit",
- stock_uom="Box", conversion_factor=0.1, is_return=1, return_against=pr.name)
+ pr = make_purchase_receipt(
+ item_code=item_code,
+ qty=1,
+ uom="Box",
+ conversion_factor=1.0
+ )
+ return_pr = make_purchase_receipt(
+ item_code=item_code,
+ qty=-10,
+ uom="Unit",
+ stock_uom="Box",
+ conversion_factor=0.1,
+ is_return=1,
+ return_against=pr.name
+ )
self.assertEqual(abs(return_pr.items[0].stock_qty), 1.0)
@@ -540,13 +763,19 @@
pr.submit()
update_purchase_receipt_status(pr.name, "Closed")
- self.assertEqual(frappe.db.get_value("Purchase Receipt", pr.name, "status"), "Closed")
+ self.assertEqual(
+ frappe.db.get_value("Purchase Receipt", pr.name, "status"), "Closed"
+ )
pr.reload()
pr.cancel()
def test_pr_billing_status(self):
- # PO -> PR1 -> PI and PO -> PI and PO -> PR2
+ """Flow:
+ 1. PO -> PR1 -> PI
+ 2. PO -> PI
+ 3. PO -> PR2.
+ """
from erpnext.buying.doctype.purchase_order.purchase_order import (
make_purchase_invoice as make_purchase_invoice_from_po,
)
@@ -610,21 +839,39 @@
pr_doc = make_purchase_receipt(item_code=item_code,
qty=1, serial_no = serial_no)
- self.assertEqual(serial_no, frappe.db.get_value("Serial No",
- {"purchase_document_type": "Purchase Receipt", "purchase_document_no": pr_doc.name}, "name"))
+ self.assertEqual(
+ serial_no,
+ frappe.db.get_value(
+ "Serial No",
+ {
+ "purchase_document_type": "Purchase Receipt",
+ "purchase_document_no": pr_doc.name
+ },
+ "name"
+ )
+ )
pr_doc.cancel()
- #check for the auto created serial nos
+ # check for the auto created serial nos
item_code = "Test Auto Created Serial No"
if not frappe.db.exists("Item", item_code):
- item = make_item(item_code, dict(has_serial_no=1, serial_no_series="KLJL.###"))
+ make_item(item_code, dict(has_serial_no=1, serial_no_series="KLJL.###"))
new_pr_doc = make_purchase_receipt(item_code=item_code, qty=1)
serial_no = get_serial_nos(new_pr_doc.items[0].serial_no)[0]
- self.assertEqual(serial_no, frappe.db.get_value("Serial No",
- {"purchase_document_type": "Purchase Receipt", "purchase_document_no": new_pr_doc.name}, "name"))
+ self.assertEqual(
+ serial_no,
+ frappe.db.get_value(
+ "Serial No",
+ {
+ "purchase_document_type": "Purchase Receipt",
+ "purchase_document_no": new_pr_doc.name
+ },
+ "name"
+ )
+ )
new_pr_doc.cancel()
@@ -700,8 +947,12 @@
def test_purchase_receipt_cost_center(self):
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
+
cost_center = "_Test Cost Center for BS Account - TCP1"
- create_cost_center(cost_center_name="_Test Cost Center for BS Account", company="_Test Company with perpetual inventory")
+ create_cost_center(
+ cost_center_name="_Test Cost Center for BS Account",
+ company="_Test Company with perpetual inventory"
+ )
if not frappe.db.exists('Location', 'Test Location'):
frappe.get_doc({
@@ -709,10 +960,16 @@
'location_name': 'Test Location'
}).insert()
- pr = make_purchase_receipt(cost_center=cost_center, company="_Test Company with perpetual inventory",
- warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1")
+ pr = make_purchase_receipt(
+ cost_center=cost_center,
+ company="_Test Company with perpetual inventory",
+ warehouse = "Stores - TCP1",
+ supplier_warehouse = "Work in Progress - TCP1"
+ )
- stock_in_hand_account = get_inventory_account(pr.company, pr.get("items")[0].warehouse)
+ stock_in_hand_account = get_inventory_account(
+ pr.company, pr.get("items")[0].warehouse
+ )
gl_entries = get_gl_entries("Purchase Receipt", pr.name)
self.assertTrue(gl_entries)
@@ -736,9 +993,16 @@
'doctype': 'Location',
'location_name': 'Test Location'
}).insert()
- pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1")
- stock_in_hand_account = get_inventory_account(pr.company, pr.get("items")[0].warehouse)
+ pr = make_purchase_receipt(
+ company="_Test Company with perpetual inventory",
+ warehouse = "Stores - TCP1",
+ supplier_warehouse = "Work in Progress - TCP1"
+ )
+
+ stock_in_hand_account = get_inventory_account(
+ pr.company, pr.get("items")[0].warehouse
+ )
gl_entries = get_gl_entries("Purchase Receipt", pr.name)
self.assertTrue(gl_entries)
@@ -766,7 +1030,11 @@
po = create_purchase_order()
pr = create_pr_against_po(po.name)
- pr1 = make_purchase_receipt(is_return=1, return_against=pr.name, qty=-1, do_not_submit=True)
+ pr1 = make_purchase_receipt(
+ qty=-1,
+ is_return=1, return_against=pr.name,
+ do_not_submit=True
+ )
pr1.items[0].purchase_order = po.name
pr1.items[0].purchase_order_item = po.items[0].name
pr1.items[0].purchase_receipt_item = pr.items[0].name
@@ -799,7 +1067,11 @@
pi1.save()
pi1.submit()
- pr2 = make_purchase_receipt(is_return=1, return_against=pr1.name, qty=-2, do_not_submit=True)
+ pr2 = make_purchase_receipt(
+ qty=-2,
+ is_return=1, return_against=pr1.name,
+ do_not_submit=True
+ )
pr2.items[0].purchase_receipt_item = pr1.items[0].name
pr2.submit()
@@ -841,14 +1113,22 @@
pr1.cancel()
def test_stock_transfer_from_purchase_receipt_with_valuation(self):
- create_warehouse("_Test Warehouse for Valuation", company="_Test Company with perpetual inventory",
- properties={"account": '_Test Account Stock In Hand - TCP1'})
+ create_warehouse(
+ "_Test Warehouse for Valuation",
+ company="_Test Company with perpetual inventory",
+ properties={"account": '_Test Account Stock In Hand - TCP1'}
+ )
- pr1 = make_purchase_receipt(warehouse = '_Test Warehouse for Valuation - TCP1',
- company="_Test Company with perpetual inventory")
+ pr1 = make_purchase_receipt(
+ warehouse = '_Test Warehouse for Valuation - TCP1',
+ company="_Test Company with perpetual inventory"
+ )
- pr = make_purchase_receipt(company="_Test Company with perpetual inventory",
- warehouse = "Stores - TCP1", do_not_save=1)
+ pr = make_purchase_receipt(
+ company="_Test Company with perpetual inventory",
+ warehouse = "Stores - TCP1",
+ do_not_save=1
+ )
pr.items[0].from_warehouse = '_Test Warehouse for Valuation - TCP1'
pr.supplier_warehouse = ''
@@ -930,10 +1210,24 @@
}
rm_items = [
- {"item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 3","item_name":"_Test Item",
- "qty":300,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos", "name": po.supplied_items[0].name},
- {"item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 3","item_name":"_Test Item",
- "qty":200,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos", "name": po.supplied_items[0].name}
+ {
+ "item_code":item_code,
+ "rm_item_code":"Sub Contracted Raw Material 3",
+ "item_name":"_Test Item",
+ "qty":300,
+ "warehouse":"_Test Warehouse - _TC",
+ "stock_uom":"Nos",
+ "name": po.supplied_items[0].name
+ },
+ {
+ "item_code":item_code,
+ "rm_item_code":"Sub Contracted Raw Material 3",
+ "item_name":"_Test Item",
+ "qty":200,
+ "warehouse":"_Test Warehouse - _TC",
+ "stock_uom":"Nos",
+ "name": po.supplied_items[0].name
+ }
]
rm_item_string = json.dumps(rm_items)
@@ -943,8 +1237,14 @@
se.items[1].batch_no = ste2.items[0].batch_no
se.submit()
- supplied_qty = frappe.db.get_value("Purchase Order Item Supplied",
- {"parent": po.name, "rm_item_code": "Sub Contracted Raw Material 3"}, "supplied_qty")
+ supplied_qty = frappe.db.get_value(
+ "Purchase Order Item Supplied",
+ {
+ "parent": po.name,
+ "rm_item_code": "Sub Contracted Raw Material 3"
+ },
+ "supplied_qty"
+ )
self.assertEqual(supplied_qty, 500.00)
@@ -1016,10 +1316,18 @@
company = '_Test Company with perpetual inventory'
service_item = '_Test Non Stock Item'
- before_test_value = frappe.db.get_value('Company', company, 'enable_perpetual_inventory_for_non_stock_items')
- frappe.db.set_value('Company', company, 'enable_perpetual_inventory_for_non_stock_items', 1)
+ before_test_value = frappe.db.get_value(
+ 'Company', company, 'enable_perpetual_inventory_for_non_stock_items'
+ )
+ frappe.db.set_value(
+ 'Company', company,
+ 'enable_perpetual_inventory_for_non_stock_items', 1
+ )
srbnb_account = 'Stock Received But Not Billed - TCP1'
- frappe.db.set_value('Company', company, 'service_received_but_not_billed', srbnb_account)
+ frappe.db.set_value(
+ 'Company', company,
+ 'service_received_but_not_billed', srbnb_account
+ )
pr = make_purchase_receipt(
company=company, item=service_item,
@@ -1051,7 +1359,10 @@
self.assertEqual(len(item_one_gl_entry), 1)
self.assertEqual(len(item_two_gl_entry), 1)
- frappe.db.set_value('Company', company, 'enable_perpetual_inventory_for_non_stock_items', before_test_value)
+ frappe.db.set_value(
+ 'Company', company,
+ 'enable_perpetual_inventory_for_non_stock_items', before_test_value
+ )
def test_purchase_receipt_with_exchange_rate_difference(self):
from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import (
@@ -1076,10 +1387,19 @@
pr.submit()
# Get exchnage gain and loss account
- exchange_gain_loss_account = frappe.db.get_value('Company', pr.company, 'exchange_gain_loss_account')
+ exchange_gain_loss_account = frappe.db.get_value(
+ 'Company', pr.company, 'exchange_gain_loss_account'
+ )
# fetching the latest GL Entry with exchange gain and loss account account
- amount = frappe.db.get_value('GL Entry', {'account': exchange_gain_loss_account, 'voucher_no': pr.name}, 'credit')
+ amount = frappe.db.get_value(
+ 'GL Entry',
+ {
+ 'account': exchange_gain_loss_account,
+ 'voucher_no': pr.name
+ },
+ 'credit'
+ )
discrepancy_caused_by_exchange_rate_diff = abs(pi.items[0].base_net_amount - pr.items[0].base_net_amount)
self.assertEqual(discrepancy_caused_by_exchange_rate_diff, amount)
@@ -1225,8 +1545,8 @@
pr.return_against = args.return_against
pr.apply_putaway_rule = args.apply_putaway_rule
qty = args.qty or 5
- received_qty = args.received_qty or qty
- rejected_qty = args.rejected_qty or flt(received_qty) - flt(qty)
+ rejected_qty = args.rejected_qty or 0
+ received_qty = args.received_qty or flt(rejected_qty) + flt(qty)
item_code = args.item or args.item_code or "_Test Item"
uom = args.uom or frappe.db.get_value("Item", item_code, "stock_uom") or "_Test UOM"
@@ -1249,9 +1569,12 @@
if args.get_multiple_items:
pr.items = []
- for item in get_items(warehouse= args.warehouse, cost_center = args.cost_center or frappe.get_cached_value('Company', pr.company, 'cost_center')):
- pr.append("items", item)
+ company_cost_center = frappe.get_cached_value('Company', pr.company, 'cost_center')
+ cost_center = args.cost_center or company_cost_center
+
+ for item in get_items(warehouse=args.warehouse, cost_center=cost_center):
+ pr.append("items", item)
if args.get_taxes_and_charges:
for tax in get_taxes():
diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
index 3efa66e..30ea1c3 100644
--- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
+++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
@@ -197,6 +197,7 @@
},
{
"bold": 1,
+ "default": "0",
"fieldname": "received_qty",
"fieldtype": "Float",
"label": "Received Quantity",
@@ -204,6 +205,7 @@
"oldfieldtype": "Currency",
"print_hide": 1,
"print_width": "100px",
+ "read_only": 1,
"reqd": 1,
"width": "100px"
},
@@ -219,8 +221,10 @@
"width": "100px"
},
{
+ "columns": 1,
"fieldname": "rejected_qty",
"fieldtype": "Float",
+ "in_list_view": 1,
"label": "Rejected Quantity",
"oldfieldname": "rejected_qty",
"oldfieldtype": "Currency",
@@ -327,7 +331,7 @@
},
{
"bold": 1,
- "columns": 3,
+ "columns": 2,
"fieldname": "rate",
"fieldtype": "Currency",
"in_list_view": 1,
@@ -543,6 +547,7 @@
"fieldname": "stock_qty",
"fieldtype": "Float",
"label": "Accepted Qty in Stock UOM",
+ "no_copy": 1,
"oldfieldname": "stock_qty",
"oldfieldtype": "Currency",
"print_hide": 1,
@@ -882,7 +887,9 @@
"fieldname": "received_stock_qty",
"fieldtype": "Float",
"label": "Received Qty in Stock UOM",
- "print_hide": 1
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
},
{
"depends_on": "eval: doc.uom != doc.stock_uom",
@@ -969,10 +976,11 @@
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2021-09-01 16:02:40.338597",
+ "modified": "2021-11-15 15:46:10.591600",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt Item",
+ "naming_rule": "Random",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.js b/erpnext/stock/doctype/quality_inspection/quality_inspection.js
index d08dc3e..eea2879 100644
--- a/erpnext/stock/doctype/quality_inspection/quality_inspection.js
+++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.js
@@ -59,7 +59,7 @@
},
item_code: function(frm) {
- if (frm.doc.item_code) {
+ if (frm.doc.item_code && !frm.doc.quality_inspection_template) {
return frm.call({
method: "get_quality_inspection_template",
doc: frm.doc,
diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.py b/erpnext/stock/doctype/quality_inspection/quality_inspection.py
index 913ee15..4e3b80a 100644
--- a/erpnext/stock/doctype/quality_inspection/quality_inspection.py
+++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.py
@@ -18,6 +18,15 @@
if not self.readings and self.item_code:
self.get_item_specification_details()
+ if self.inspection_type=="In Process" and self.reference_type=="Job Card":
+ item_qi_template = frappe.db.get_value("Item", self.item_code, 'quality_inspection_template')
+ parameters = get_template_details(item_qi_template)
+ for reading in self.readings:
+ for d in parameters:
+ if reading.specification == d.specification:
+ reading.update(d)
+ reading.status = "Accepted"
+
if self.readings:
self.inspect_and_set_status()
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 a800bf8..cd7e63b 100644
--- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json
+++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json
@@ -64,7 +64,7 @@
"in_standard_filter": 1,
"label": "Status",
"no_copy": 1,
- "options": "Queued\nIn Progress\nCompleted\nFailed",
+ "options": "Queued\nIn Progress\nCompleted\nSkipped\nFailed",
"read_only": 1
},
{
@@ -177,10 +177,11 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2021-07-22 18:59:43.057878",
+ "modified": "2021-11-24 02:18:10.524560",
"modified_by": "Administrator",
"module": "Stock",
"name": "Repost Item Valuation",
+ "naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
@@ -206,27 +207,12 @@
"print": 1,
"read": 1,
"report": 1,
- "role": "Stock User",
- "share": 1,
- "submit": 1,
- "write": 1
- },
- {
- "cancel": 1,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "print": 1,
- "read": 1,
- "report": 1,
"role": "Stock Manager",
"share": 1,
"submit": 1,
"write": 1
},
{
- "cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
@@ -234,7 +220,7 @@
"print": 1,
"read": 1,
"report": 1,
- "role": "Accounts User",
+ "role": "Accounts Manager",
"share": 1,
"submit": 1,
"write": 1
@@ -242,4 +228,4 @@
],
"sort_field": "modified",
"sort_order": "DESC"
-}
\ No newline at end of file
+}
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 170aa7f..fb3b355 100644
--- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
+++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
@@ -1,7 +1,6 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
-
import frappe
from frappe import _
from frappe.model.document import Document
@@ -19,7 +18,7 @@
class RepostItemValuation(Document):
def validate(self):
- self.set_status()
+ self.set_status(write=False)
self.reset_field_values()
self.set_company()
@@ -27,26 +26,27 @@
if self.based_on == 'Transaction':
self.item_code = None
self.warehouse = None
- else:
- self.voucher_type = None
- self.voucher_no = None
self.allow_negative_stock = self.allow_negative_stock or \
cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock"))
def set_company(self):
- if self.voucher_type and self.voucher_no:
+ if self.based_on == "Transaction":
self.company = frappe.get_cached_value(self.voucher_type, self.voucher_no, "company")
elif self.warehouse:
self.company = frappe.get_cached_value("Warehouse", self.warehouse, "company")
- def set_status(self, status=None):
+ def set_status(self, status=None, write=True):
+ status = status or self.status
if not status:
- status = 'Queued'
- self.db_set('status', status)
+ self.status = 'Queued'
+ else:
+ self.status = status
+ if write:
+ self.db_set('status', self.status)
def on_submit(self):
- if not frappe.flags.in_test:
+ if not frappe.flags.in_test or self.flags.dont_run_in_test or frappe.flags.dont_execute_stock_reposts:
return
frappe.enqueue(repost, timeout=1800, queue='long',
@@ -54,9 +54,42 @@
@frappe.whitelist()
def restart_reposting(self):
- self.set_status('Queued')
- frappe.enqueue(repost, timeout=1800, queue='long',
- job_name='repost_sle', now=True, doc=self)
+ self.set_status('Queued', write=False)
+ self.current_index = 0
+ self.distinct_item_and_warehouse = None
+ self.items_to_be_repost = None
+ self.db_update()
+
+ def deduplicate_similar_repost(self):
+ """ Deduplicate similar reposts based on item-warehouse-posting combination."""
+ if self.based_on != "Item and Warehouse":
+ return
+
+ filters = {
+ "item_code": self.item_code,
+ "warehouse": self.warehouse,
+ "name": self.name,
+ "posting_date": self.posting_date,
+ "posting_time": self.posting_time,
+ }
+
+ frappe.db.sql("""
+ update `tabRepost Item Valuation`
+ set status = 'Skipped'
+ WHERE item_code = %(item_code)s
+ and warehouse = %(warehouse)s
+ and name != %(name)s
+ and TIMESTAMP(posting_date, posting_time) > TIMESTAMP(%(posting_date)s, %(posting_time)s)
+ and docstatus = 1
+ and status = 'Queued'
+ and based_on = 'Item and Warehouse'
+ """,
+ filters
+ )
+
+def on_doctype_update():
+ frappe.db.add_index("Repost Item Valuation", ["warehouse", "item_code"], "item_warehouse")
+
def repost(doc):
try:
@@ -64,7 +97,8 @@
return
doc.set_status('In Progress')
- frappe.db.commit()
+ if not frappe.flags.in_test:
+ frappe.db.commit()
repost_sl_entries(doc)
repost_gl_entries(doc)
@@ -133,8 +167,10 @@
riv_entries = get_repost_item_valuation_entries()
for row in riv_entries:
- doc = frappe.get_cached_doc('Repost Item Valuation', row.name)
- repost(doc)
+ doc = frappe.get_doc('Repost Item Valuation', row.name)
+ if doc.status in ('Queued', 'In Progress'):
+ repost(doc)
+ doc.deduplicate_similar_repost()
riv_entries = get_repost_item_valuation_entries()
if riv_entries:
diff --git a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py
index c086f93..78b432d 100644
--- a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py
+++ b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py
@@ -4,10 +4,14 @@
import unittest
import frappe
+from frappe.utils import nowdate
+from erpnext.controllers.stock_controller import create_item_wise_repost_entries
+from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
from erpnext.stock.doctype.repost_item_valuation.repost_item_valuation import (
in_configured_timeslot,
)
+from erpnext.stock.utils import PendingRepostingError
class TestRepostItemValuation(unittest.TestCase):
@@ -70,3 +74,91 @@
in_configured_timeslot(repost_settings, case.get("current_time")),
msg=f"Exepcted false from : {case}",
)
+
+ def test_create_item_wise_repost_item_valuation_entries(self):
+ pr = make_purchase_receipt(
+ company="_Test Company with perpetual inventory",
+ warehouse="Stores - TCP1",
+ get_multiple_items=True,
+ )
+
+ rivs = create_item_wise_repost_entries(pr.doctype, pr.name)
+ self.assertGreaterEqual(len(rivs), 2)
+ self.assertIn("_Test Item", [d.item_code for d in rivs])
+
+ for riv in rivs:
+ self.assertEqual(riv.company, "_Test Company with perpetual inventory")
+ self.assertEqual(riv.warehouse, "Stores - TCP1")
+
+ def test_deduplication(self):
+ def _assert_status(doc, status):
+ doc.load_from_db()
+ self.assertEqual(doc.status, status)
+
+ riv_args = frappe._dict(
+ doctype="Repost Item Valuation",
+ item_code="_Test Item",
+ warehouse="_Test Warehouse - _TC",
+ based_on="Item and Warehouse",
+ voucher_type="Sales Invoice",
+ voucher_no="SI-1",
+ posting_date="2021-01-02",
+ posting_time="00:01:00",
+ )
+
+ # new repost without any duplicates
+ riv1 = frappe.get_doc(riv_args)
+ riv1.flags.dont_run_in_test = True
+ riv1.submit()
+ _assert_status(riv1, "Queued")
+ self.assertEqual(riv1.voucher_type, "Sales Invoice") # traceability
+ self.assertEqual(riv1.voucher_no, "SI-1")
+
+ # newer than existing duplicate - riv1
+ riv2 = frappe.get_doc(riv_args.update({"posting_date": "2021-01-03"}))
+ riv2.flags.dont_run_in_test = True
+ riv2.submit()
+ riv1.deduplicate_similar_repost()
+ _assert_status(riv2, "Skipped")
+
+ # older than exisitng duplicate - riv1
+ riv3 = frappe.get_doc(riv_args.update({"posting_date": "2021-01-01"}))
+ riv3.flags.dont_run_in_test = True
+ riv3.submit()
+ riv3.deduplicate_similar_repost()
+ _assert_status(riv3, "Queued")
+ _assert_status(riv1, "Skipped")
+
+ # unrelated reposts, shouldn't do anything to others.
+ riv4 = frappe.get_doc(riv_args.update({"warehouse": "Stores - _TC"}))
+ riv4.flags.dont_run_in_test = True
+ riv4.submit()
+ riv4.deduplicate_similar_repost()
+ _assert_status(riv4, "Queued")
+ _assert_status(riv3, "Queued")
+
+ # to avoid breaking other tests accidentaly
+ riv4.set_status("Skipped")
+ riv3.set_status("Skipped")
+
+ def test_stock_freeze_validation(self):
+
+ today = nowdate()
+
+ riv = frappe.get_doc(
+ doctype="Repost Item Valuation",
+ item_code="_Test Item",
+ warehouse="_Test Warehouse - _TC",
+ based_on="Item and Warehouse",
+ posting_date=today,
+ posting_time="00:01:00",
+ )
+ riv.flags.dont_run_in_test = True # keep it queued
+ riv.submit()
+
+ stock_settings = frappe.get_doc("Stock Settings")
+ stock_settings.stock_frozen_upto = today
+
+ self.assertRaises(PendingRepostingError, stock_settings.save)
+
+ riv.set_status("Skipped")
diff --git a/erpnext/stock/doctype/serial_no/serial_no.json b/erpnext/stock/doctype/serial_no/serial_no.json
index a3d44af..6e1e0d4 100644
--- a/erpnext/stock/doctype/serial_no/serial_no.json
+++ b/erpnext/stock/doctype/serial_no/serial_no.json
@@ -1,7 +1,6 @@
{
"actions": [],
"allow_import": 1,
- "allow_rename": 1,
"autoname": "field:serial_no",
"creation": "2013-05-16 10:59:15",
"description": "Distinct unit of an Item",
@@ -434,10 +433,11 @@
"icon": "fa fa-barcode",
"idx": 1,
"links": [],
- "modified": "2021-01-08 14:31:15.375996",
+ "modified": "2021-12-23 10:44:30.299450",
"modified_by": "Administrator",
"module": "Stock",
"name": "Serial No",
+ "naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
@@ -476,5 +476,6 @@
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py
index 38291d1..2947faf 100644
--- a/erpnext/stock/doctype/serial_no/serial_no.py
+++ b/erpnext/stock/doctype/serial_no/serial_no.py
@@ -194,23 +194,6 @@
if sle_exists:
frappe.throw(_("Cannot delete Serial No {0}, as it is used in stock transactions").format(self.name))
- def before_rename(self, old, new, merge=False):
- if merge:
- frappe.throw(_("Sorry, Serial Nos cannot be merged"))
-
- def after_rename(self, old, new, merge=False):
- """rename serial_no text fields"""
- for dt in frappe.db.sql("""select parent from tabDocField
- where fieldname='serial_no' and fieldtype in ('Text', 'Small Text', 'Long Text')"""):
-
- for item in frappe.db.sql("""select name, serial_no from `tab%s`
- where serial_no like %s""" % (dt[0], frappe.db.escape('%' + old + '%'))):
-
- serial_nos = map(lambda i: new if i.upper()==old.upper() else i, item[1].split('\n'))
- frappe.db.sql("""update `tab%s` set serial_no = %s
- where name=%s""" % (dt[0], '%s', '%s'),
- ('\n'.join(list(serial_nos)), item[0]))
-
def update_serial_no_reference(self, serial_no=None):
last_sle = self.get_last_sle(serial_no)
self.set_purchase_details(last_sle.get("purchase_sle"))
diff --git a/erpnext/stock/doctype/shipment/test_shipment.py b/erpnext/stock/doctype/shipment/test_shipment.py
index 705b265..afe8218 100644
--- a/erpnext/stock/doctype/shipment/test_shipment.py
+++ b/erpnext/stock/doctype/shipment/test_shipment.py
@@ -39,9 +39,9 @@
"description": 'Test delivery note for shipment',
"qty": 5,
"uom": 'Nos',
- "warehouse": 'Stores - SC',
+ "warehouse": 'Stores - _TC',
"rate": item.standard_rate,
- "cost_center": 'Main - SC'
+ "cost_center": 'Main - _TC'
}
)
delivery_note.insert()
@@ -127,13 +127,7 @@
return create_shipment_address(address_title, company_name, 80331)
def get_shipment_company():
- company_name = 'Shipment Company'
- abbr = 'SC'
- companies = frappe.get_all("Company", fields=["name"], filters = {"company_name": company_name})
- if len(companies):
- return companies[0]
- else:
- return create_shipment_company(company_name, abbr)
+ return frappe.get_doc("Company", "_Test Company")
def get_shipment_item(company_name):
item_name = 'Testing Shipment item'
@@ -182,17 +176,6 @@
customer.insert()
return customer
-
-def create_shipment_company(company_name, abbr):
- company = frappe.new_doc("Company")
- company.company_name = company_name
- company.abbr = abbr
- company.default_currency = 'EUR'
- company.country = 'Germany'
- company.enable_perpetual_inventory = 0
- company.insert()
- return company
-
def create_shipment_customer(customer_name):
customer = frappe.new_doc("Customer")
customer.customer_name = customer_name
@@ -211,12 +194,12 @@
stock.posting_date = posting_date.strftime("%Y-%m-%d")
stock.append('items',
{
- "t_warehouse": 'Stores - SC',
+ "t_warehouse": 'Stores - _TC',
"item_code": item.name,
"qty": 5,
"uom": 'Nos',
"basic_rate": item.standard_rate,
- "cost_center": 'Main - SC'
+ "cost_center": 'Main - _TC'
}
)
stock.insert()
@@ -233,7 +216,7 @@
item.append('item_defaults',
{
"company": company_name,
- "default_warehouse": 'Stores - SC'
+ "default_warehouse": 'Stores - _TC'
}
)
item.insert()
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index 46c9576..93e303c 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -35,10 +35,16 @@
from erpnext.stock.utils import get_bin, get_incoming_rate
-class IncorrectValuationRateError(frappe.ValidationError): pass
-class DuplicateEntryForWorkOrderError(frappe.ValidationError): pass
-class OperationsNotCompleteError(frappe.ValidationError): pass
-class MaxSampleAlreadyRetainedError(frappe.ValidationError): pass
+class FinishedGoodError(frappe.ValidationError):
+ pass
+class IncorrectValuationRateError(frappe.ValidationError):
+ pass
+class DuplicateEntryForWorkOrderError(frappe.ValidationError):
+ pass
+class OperationsNotCompleteError(frappe.ValidationError):
+ pass
+class MaxSampleAlreadyRetainedError(frappe.ValidationError):
+ pass
from erpnext.controllers.stock_controller import StockController
@@ -103,6 +109,8 @@
self.set_actual_qty()
self.calculate_rate_and_amount()
self.validate_putaway_capacity()
+ self.reset_default_field_value("from_warehouse", "items", "s_warehouse")
+ self.reset_default_field_value("to_warehouse", "items", "t_warehouse")
def on_submit(self):
self.update_stock_ledger()
@@ -545,7 +553,7 @@
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", cache=True):
+ if not outgoing_items_cost and 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()])
@@ -699,6 +707,11 @@
finished_item = self.get_finished_item()
+ if not finished_item and self.purpose == "Manufacture":
+ # In case of independent Manufacture entry, don't auto set
+ # user must decide and set
+ return
+
for d in self.items:
if d.t_warehouse and not d.s_warehouse:
if self.purpose=="Repack" or d.item_code == finished_item:
@@ -719,38 +732,64 @@
return finished_item
def validate_finished_goods(self):
- """validation: finished good quantity should be same as manufacturing quantity"""
- if not self.work_order: return
+ """
+ 1. Check if FG exists
+ 2. Check if Multiple FG Items are present
+ 3. Check FG Item and Qty against WO if present
+ """
+ production_item, wo_qty, finished_items = None, 0, []
- production_item, wo_qty = frappe.db.get_value("Work Order",
- self.work_order, ["production_item", "qty"])
+ wo_details = frappe.db.get_value(
+ "Work Order", self.work_order, ["production_item", "qty"]
+ )
+ if wo_details:
+ production_item, wo_qty = wo_details
- finished_items = []
for d in self.get('items'):
if d.is_finished_item:
+ if not self.work_order:
+ finished_items.append(d.item_code)
+ continue # Independent Manufacture Entry, no WO to match against
+
if d.item_code != production_item:
frappe.throw(_("Finished Item {0} does not match with Work Order {1}")
- .format(d.item_code, self.work_order))
+ .format(d.item_code, self.work_order)
+ )
elif flt(d.transfer_qty) > flt(self.fg_completed_qty):
- frappe.throw(_("Quantity in row {0} ({1}) must be same as manufactured quantity {2}"). \
- format(d.idx, d.transfer_qty, self.fg_completed_qty))
+ frappe.throw(_("Quantity in row {0} ({1}) must be same as manufactured quantity {2}")
+ .format(d.idx, d.transfer_qty, self.fg_completed_qty)
+ )
+
finished_items.append(d.item_code)
if len(set(finished_items)) > 1:
- frappe.throw(_("Multiple items cannot be marked as finished item"))
+ frappe.throw(
+ msg=_("Multiple items cannot be marked as finished item"),
+ title=_("Note"),
+ exc=FinishedGoodError
+ )
if self.purpose == "Manufacture":
if not finished_items:
- frappe.throw(_('Finished Good has not set in the stock entry {0}')
- .format(self.name))
+ frappe.throw(
+ msg=_("There must be atleast 1 Finished Good in this Stock Entry").format(self.name),
+ title=_("Missing Finished Good"),
+ exc=FinishedGoodError
+ )
- allowance_percentage = flt(frappe.db.get_single_value("Manufacturing Settings",
- "overproduction_percentage_for_work_order"))
+ allowance_percentage = flt(
+ frappe.db.get_single_value(
+ "Manufacturing Settings","overproduction_percentage_for_work_order"
+ )
+ )
+ allowed_qty = wo_qty + ((allowance_percentage/100) * wo_qty)
- allowed_qty = wo_qty + (allowance_percentage/100 * wo_qty)
- if self.fg_completed_qty > allowed_qty:
- frappe.throw(_("For quantity {0} should not be greater than work order quantity {1}")
- .format(flt(self.fg_completed_qty), wo_qty))
+ # No work order could mean independent Manufacture entry, if so skip validation
+ if self.work_order and self.fg_completed_qty > allowed_qty:
+ frappe.throw(
+ _("For quantity {0} should not be greater than work order quantity {1}")
+ .format(flt(self.fg_completed_qty), wo_qty)
+ )
def update_stock_ledger(self):
sl_entries = []
@@ -1462,52 +1501,94 @@
return item_dict
def get_pro_order_required_items(self, backflush_based_on=None):
- item_dict = frappe._dict()
- pro_order = frappe.get_doc("Work Order", self.work_order)
- if not frappe.db.get_value("Warehouse", pro_order.wip_warehouse, "is_group"):
- wip_warehouse = pro_order.wip_warehouse
+ """
+ Gets Work Order Required Items only if Stock Entry purpose is **Material Transferred for Manufacture**.
+ """
+ item_dict, job_card_items = frappe._dict(), []
+ work_order = frappe.get_doc("Work Order", self.work_order)
+
+ consider_job_card = work_order.transfer_material_against == "Job Card" and self.get("job_card")
+ if consider_job_card:
+ job_card_items = self.get_job_card_item_codes(self.get("job_card"))
+
+ if not frappe.db.get_value("Warehouse", work_order.wip_warehouse, "is_group"):
+ wip_warehouse = work_order.wip_warehouse
else:
wip_warehouse = None
- for d in pro_order.get("required_items"):
- if ( ((flt(d.required_qty) > flt(d.transferred_qty)) or
- (backflush_based_on == "Material Transferred for Manufacture")) and
- (d.include_item_in_manufacturing or self.purpose != "Material Transfer for Manufacture")):
+ for d in work_order.get("required_items"):
+ if consider_job_card and (d.item_code not in job_card_items):
+ continue
+
+ transfer_pending = flt(d.required_qty) > flt(d.transferred_qty)
+ can_transfer = transfer_pending or (backflush_based_on == "Material Transferred for Manufacture")
+
+ if not can_transfer:
+ continue
+
+ if d.include_item_in_manufacturing:
item_row = d.as_dict()
+ item_row["idx"] = len(item_dict) + 1
+
+ if consider_job_card:
+ job_card_item = frappe.db.get_value(
+ "Job Card Item",
+ {
+ "item_code": d.item_code,
+ "parent": self.get("job_card")
+ }
+ )
+ item_row["job_card_item"] = job_card_item or None
+
if d.source_warehouse and not frappe.db.get_value("Warehouse", d.source_warehouse, "is_group"):
item_row["from_warehouse"] = d.source_warehouse
item_row["to_warehouse"] = wip_warehouse
if item_row["allow_alternative_item"]:
- item_row["allow_alternative_item"] = pro_order.allow_alternative_item
+ item_row["allow_alternative_item"] = work_order.allow_alternative_item
item_dict.setdefault(d.item_code, item_row)
return item_dict
+ def get_job_card_item_codes(self, job_card=None):
+ if not job_card:
+ return []
+
+ job_card_items = frappe.get_all(
+ "Job Card Item",
+ filters={
+ "parent": job_card
+ },
+ fields=["item_code"],
+ distinct=True
+ )
+ return [d.item_code for d in job_card_items]
+
def add_to_stock_entry_detail(self, item_dict, bom_no=None):
for d in item_dict:
- stock_uom = item_dict[d].get("stock_uom") or frappe.db.get_value("Item", d, "stock_uom")
+ item_row = item_dict[d]
+ stock_uom = item_row.get("stock_uom") or frappe.db.get_value("Item", d, "stock_uom")
se_child = self.append('items')
- se_child.s_warehouse = item_dict[d].get("from_warehouse")
- se_child.t_warehouse = item_dict[d].get("to_warehouse")
- se_child.item_code = item_dict[d].get('item_code') or cstr(d)
- se_child.uom = item_dict[d]["uom"] if item_dict[d].get("uom") else stock_uom
+ se_child.s_warehouse = item_row.get("from_warehouse")
+ se_child.t_warehouse = item_row.get("to_warehouse")
+ se_child.item_code = item_row.get('item_code') or cstr(d)
+ se_child.uom = item_row["uom"] if item_row.get("uom") else stock_uom
se_child.stock_uom = stock_uom
- se_child.qty = flt(item_dict[d]["qty"], se_child.precision("qty"))
- se_child.allow_alternative_item = item_dict[d].get("allow_alternative_item", 0)
- se_child.subcontracted_item = item_dict[d].get("main_item_code")
- se_child.cost_center = (item_dict[d].get("cost_center") or
- 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)
+ se_child.qty = flt(item_row["qty"], se_child.precision("qty"))
+ se_child.allow_alternative_item = item_row.get("allow_alternative_item", 0)
+ se_child.subcontracted_item = item_row.get("main_item_code")
+ se_child.cost_center = (item_row.get("cost_center") or
+ get_default_cost_center(item_row, company = self.company))
+ se_child.is_finished_item = item_row.get("is_finished_item", 0)
+ se_child.is_scrap_item = item_row.get("is_scrap_item", 0)
+ se_child.is_process_loss = item_row.get("is_process_loss", 0)
for field in ["idx", "po_detail", "original_item", "expense_account",
"description", "item_name", "serial_no", "batch_no", "allow_zero_valuation_rate"]:
- if item_dict[d].get(field):
- se_child.set(field, item_dict[d].get(field))
+ if item_row.get(field):
+ se_child.set(field, item_row.get(field))
if se_child.s_warehouse==None:
se_child.s_warehouse = self.from_warehouse
@@ -1515,12 +1596,11 @@
se_child.t_warehouse = self.to_warehouse
# in stock uom
- se_child.conversion_factor = flt(item_dict[d].get("conversion_factor")) or 1
- se_child.transfer_qty = flt(item_dict[d]["qty"]*se_child.conversion_factor, se_child.precision("qty"))
+ se_child.conversion_factor = flt(item_row.get("conversion_factor")) or 1
+ se_child.transfer_qty = flt(item_row["qty"]*se_child.conversion_factor, se_child.precision("qty"))
-
- # to be assigned for finished item
- se_child.bom_no = bom_no
+ se_child.bom_no = bom_no # to be assigned for finished item
+ se_child.job_card_item = item_row.get("job_card_item") if self.get("job_card") else None
def validate_with_material_request(self):
for item in self.get("items"):
diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
index 0679467..b874874 100644
--- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
@@ -15,7 +15,10 @@
set_item_variant_settings,
)
from erpnext.stock.doctype.serial_no.serial_no import * # noqa
-from erpnext.stock.doctype.stock_entry.stock_entry import move_sample_to_retention_warehouse
+from erpnext.stock.doctype.stock_entry.stock_entry import (
+ FinishedGoodError,
+ move_sample_to_retention_warehouse,
+)
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.stock.doctype.stock_ledger_entry.stock_ledger_entry import StockFreezeError
from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import (
@@ -24,7 +27,8 @@
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
create_stock_reconciliation,
)
-from erpnext.stock.stock_ledger import get_previous_sle
+from erpnext.stock.stock_ledger import NegativeStockError, get_previous_sle
+from erpnext.tests.utils import ERPNextTestCase, change_settings
def get_sle(**args):
@@ -38,9 +42,10 @@
order by timestamp(posting_date, posting_time) desc, creation desc limit 1"""% condition,
values, as_dict=1)
-class TestStockEntry(unittest.TestCase):
+class TestStockEntry(ERPNextTestCase):
def tearDown(self):
frappe.set_user("Administrator")
+ frappe.db.set_value("Manufacturing Settings", None, "material_consumption", "0")
def test_fifo(self):
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1)
@@ -582,6 +587,65 @@
self.assertEqual(fg_cost,
flt(rm_cost + bom_operation_cost + work_order.additional_operating_cost, 2))
+ def test_work_order_manufacture_with_material_consumption(self):
+ from erpnext.manufacturing.doctype.work_order.work_order import (
+ make_stock_entry as _make_stock_entry,
+ )
+ frappe.db.set_value("Manufacturing Settings", None, "material_consumption", "1")
+
+ bom_no = frappe.db.get_value("BOM", {"item": "_Test FG Item",
+ "is_default": 1, "docstatus": 1})
+
+ work_order = frappe.new_doc("Work Order")
+ work_order.update({
+ "company": "_Test Company",
+ "fg_warehouse": "_Test Warehouse 1 - _TC",
+ "production_item": "_Test FG Item",
+ "bom_no": bom_no,
+ "qty": 1.0,
+ "stock_uom": "_Test UOM",
+ "wip_warehouse": "_Test Warehouse - _TC"
+ })
+ work_order.insert()
+ work_order.submit()
+
+ make_stock_entry(item_code="_Test Item",
+ target="Stores - _TC", qty=10, basic_rate=5000.0)
+ make_stock_entry(item_code="_Test Item Home Desktop 100",
+ target="Stores - _TC", qty=10, basic_rate=1000.0)
+
+
+ s = frappe.get_doc(_make_stock_entry(work_order.name, "Material Transfer for Manufacture", 1))
+ for d in s.get("items"):
+ d.s_warehouse = "Stores - _TC"
+ s.insert()
+ s.submit()
+
+ # When Stock Entry has RM and FG
+ s = frappe.get_doc(_make_stock_entry(work_order.name, "Manufacture", 1))
+ s.save()
+ rm_cost = 0
+ for d in s.get('items'):
+ if d.s_warehouse:
+ rm_cost += d.amount
+ fg_cost = list(filter(lambda x: x.item_code=="_Test FG Item", s.get("items")))[0].amount
+ scrap_cost = list(filter(lambda x: x.is_scrap_item, s.get("items")))[0].amount
+ self.assertEqual(fg_cost,
+ flt(rm_cost - scrap_cost, 2))
+
+ # When Stock Entry has only FG + Scrap
+ s.items.pop(0)
+ s.items.pop(0)
+ s.submit()
+
+ rm_cost = 0
+ for d in s.get('items'):
+ if d.s_warehouse:
+ rm_cost += d.amount
+ self.assertEqual(rm_cost, 0)
+ expected_fg_cost = s.get_basic_rate_for_manufactured_item(1)
+ fg_cost = list(filter(lambda x: x.item_code=="_Test FG Item", s.get("items")))[0].amount
+ self.assertEqual(flt(fg_cost, 2), flt(expected_fg_cost, 2))
def test_variant_work_order(self):
bom_no = frappe.db.get_value("BOM", {"item": "_Test Variant Item",
@@ -868,6 +932,115 @@
distributed_costs = [d.additional_cost for d in se.items]
self.assertEqual([40.0, 60.0], distributed_costs)
+ def test_independent_manufacture_entry(self):
+ "Test FG items and incoming rate calculation in Maniufacture Entry without WO or BOM linked."
+ se = frappe.get_doc(
+ doctype="Stock Entry",
+ purpose="Manufacture",
+ stock_entry_type="Manufacture",
+ company="_Test Company",
+ items=[
+ frappe._dict(item_code="_Test Item", qty=1, basic_rate=200, s_warehouse="_Test Warehouse - _TC"),
+ frappe._dict(item_code="_Test FG Item", qty=4, t_warehouse="_Test Warehouse 1 - _TC")
+ ]
+ )
+ # SE must have atleast one FG
+ self.assertRaises(FinishedGoodError, se.save)
+
+ se.items[0].is_finished_item = 1
+ se.items[1].is_finished_item = 1
+ # SE cannot have multiple FGs
+ self.assertRaises(FinishedGoodError, se.save)
+
+ se.items[0].is_finished_item = 0
+ se.save()
+
+ # Check if FG cost is calculated based on RM total cost
+ # RM total cost = 200, FG rate = 200/4(FG qty) = 50
+ self.assertEqual(se.items[1].basic_rate, 50)
+ self.assertEqual(se.value_difference, 0.0)
+ self.assertEqual(se.total_incoming_value, se.total_outgoing_value)
+
+ # teardown
+ se.delete()
+
+ @change_settings("Stock Settings", {"allow_negative_stock": 0})
+ def test_future_negative_sle(self):
+ # Initialize item, batch, warehouse, opening qty
+ item_code = '_Test Future Neg Item'
+ batch_no = '_Test Future Neg Batch'
+ warehouses = [
+ '_Test Future Neg Warehouse Source',
+ '_Test Future Neg Warehouse Destination'
+ ]
+ warehouse_names = initialize_records_for_future_negative_sle_test(
+ item_code, batch_no, warehouses,
+ opening_qty=2, posting_date='2021-07-01'
+ )
+
+ # Executing an illegal sequence should raise an error
+ sequence_of_entries = [
+ dict(item_code=item_code,
+ qty=2,
+ from_warehouse=warehouse_names[0],
+ to_warehouse=warehouse_names[1],
+ batch_no=batch_no,
+ posting_date='2021-07-03',
+ purpose='Material Transfer'),
+ dict(item_code=item_code,
+ qty=2,
+ from_warehouse=warehouse_names[1],
+ to_warehouse=warehouse_names[0],
+ batch_no=batch_no,
+ posting_date='2021-07-04',
+ purpose='Material Transfer'),
+ dict(item_code=item_code,
+ qty=2,
+ from_warehouse=warehouse_names[0],
+ to_warehouse=warehouse_names[1],
+ batch_no=batch_no,
+ posting_date='2021-07-02', # Illegal SE
+ purpose='Material Transfer')
+ ]
+
+ self.assertRaises(NegativeStockError, create_stock_entries, sequence_of_entries)
+
+ @change_settings("Stock Settings", {"allow_negative_stock": 0})
+ def test_future_negative_sle_batch(self):
+ from erpnext.stock.doctype.batch.test_batch import TestBatch
+
+ # Initialize item, batch, warehouse, opening qty
+ item_code = '_Test MultiBatch Item'
+ TestBatch.make_batch_item(item_code)
+
+ batch_nos = [] # store generate batches
+ warehouse = '_Test Warehouse - _TC'
+
+ se1 = make_stock_entry(
+ item_code=item_code,
+ qty=2,
+ to_warehouse=warehouse,
+ posting_date='2021-09-01',
+ purpose='Material Receipt'
+ )
+ batch_nos.append(se1.items[0].batch_no)
+ se2 = make_stock_entry(
+ item_code=item_code,
+ qty=2,
+ to_warehouse=warehouse,
+ posting_date='2021-09-03',
+ purpose='Material Receipt'
+ )
+ batch_nos.append(se2.items[0].batch_no)
+
+ with self.assertRaises(NegativeStockError) as nse:
+ make_stock_entry(item_code=item_code,
+ qty=1,
+ from_warehouse=warehouse,
+ batch_no=batch_nos[1],
+ posting_date='2021-09-02', # backdated consumption of 2nd batch
+ purpose='Material Issue')
+
def make_serialized_item(**args):
args = frappe._dict(args)
se = frappe.copy_doc(test_records[0])
@@ -938,3 +1111,31 @@
]
test_records = frappe.get_test_records('Stock Entry')
+
+def initialize_records_for_future_negative_sle_test(
+ item_code, batch_no, warehouses, opening_qty, posting_date):
+ from erpnext.stock.doctype.batch.test_batch import TestBatch, make_new_batch
+ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
+ create_stock_reconciliation,
+ )
+ from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
+
+ TestBatch.make_batch_item(item_code)
+ make_new_batch(item_code=item_code, batch_id=batch_no)
+ warehouse_names = [create_warehouse(w) for w in warehouses]
+ create_stock_reconciliation(
+ purpose='Opening Stock',
+ posting_date=posting_date,
+ posting_time='20:00:20',
+ item_code=item_code,
+ warehouse=warehouse_names[0],
+ valuation_rate=100,
+ qty=opening_qty,
+ batch_no=batch_no,
+ )
+ return warehouse_names
+
+
+def create_stock_entries(sequence_of_entries):
+ for entry_detail in sequence_of_entries:
+ make_stock_entry(**entry_detail)
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
deleted file mode 100644
index e51c90c..0000000
--- a/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_manufacture.js
+++ /dev/null
@@ -1,26 +0,0 @@
-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
deleted file mode 100644
index a87a7fb..0000000
--- a/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_material_issue.js
+++ /dev/null
@@ -1,30 +0,0 @@
-QUnit.module('Stock');
-
-QUnit.test("test material request", function(assert) {
- assert.expect(2);
- let done = assert.async();
- frappe.run_serially([
- () => {
- return frappe.tests.make('Stock Entry', [
- {from_warehouse:'Stores - '+frappe.get_abbr(frappe.defaults.get_default('Company'))},
- {items: [
- [
- {'item_code': 'Test Product 1'},
- {'qty': 5},
- ]
- ]},
- ]);
- },
- () => cur_frm.save(),
- () => frappe.click_button('Update Rate and Availability'),
- () => {
- // get_item_details
- assert.ok(cur_frm.doc.items[0].item_name=='Test Product 1', "Item name correct");
- assert.ok(cur_frm.doc.total_outgoing_value==500, " Outgoing Value 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_with_serialize_item.js b/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_material_issue_with_serialize_item.js
deleted file mode 100644
index cae318d..0000000
--- a/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_material_issue_with_serialize_item.js
+++ /dev/null
@@ -1,34 +0,0 @@
-QUnit.module('Stock');
-
-QUnit.test("test material issue", function(assert) {
- assert.expect(2);
- let done = assert.async();
- frappe.run_serially([
- () => {
- return frappe.tests.make('Stock Entry', [
- {from_warehouse:'Stores - '+frappe.get_abbr(frappe.defaults.get_default('Company'))},
- {items: [
- [
- {'item_code': 'Test Product 4'},
- {'qty': 1},
- {'batch_no':'TEST-BATCH-001'},
- {'serial_no':'Test-Product-003'},
- {'basic_rate':100},
- ]
- ]},
- ]);
- },
- () => cur_frm.save(),
- () => frappe.click_button('Close'),
- () => frappe.click_button('Update Rate and Availability'),
- () => {
- // get_item_details
- assert.ok(cur_frm.doc.items[0].item_name=='Test Product 4', "Item name correct");
- assert.ok(cur_frm.doc.total_outgoing_value==100, " Outgoing Value 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_receipt.js b/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_material_receipt.js
deleted file mode 100644
index ef0286f..0000000
--- a/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_material_receipt.js
+++ /dev/null
@@ -1,31 +0,0 @@
-QUnit.module('Stock');
-
-QUnit.test("test material request", function(assert) {
- assert.expect(2);
- let done = assert.async();
- frappe.run_serially([
- () => {
- return frappe.tests.make('Stock Entry', [
- {purpose:'Material Receipt'},
- {to_warehouse:'Stores - '+frappe.get_abbr(frappe.defaults.get_default('Company'))},
- {items: [
- [
- {'item_code': 'Test Product 1'},
- {'qty': 5},
- ]
- ]},
- ]);
- },
- () => cur_frm.save(),
- () => frappe.click_button('Update Rate and Availability'),
- () => {
- // get_item_details
- assert.ok(cur_frm.doc.items[0].item_name=='Test Product 1', "Item name correct");
- assert.ok(cur_frm.doc.total_incoming_value==500, " Incoming Value 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_receipt_for_serialize_item.js b/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_material_receipt_for_serialize_item.js
deleted file mode 100644
index 54e1ac8..0000000
--- a/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_material_receipt_for_serialize_item.js
+++ /dev/null
@@ -1,34 +0,0 @@
-QUnit.module('Stock');
-
-QUnit.test("test material receipt", function(assert) {
- assert.expect(2);
- let done = assert.async();
- frappe.run_serially([
- () => {
- return frappe.tests.make('Stock Entry', [
- {purpose:'Material Receipt'},
- {to_warehouse:'Stores - '+frappe.get_abbr(frappe.defaults.get_default('Company'))},
- {items: [
- [
- {'item_code': 'Test Product 4'},
- {'qty': 5},
- {'batch_no':'TEST-BATCH-001'},
- {'serial_no':'Test-Product-001\nTest-Product-002\nTest-Product-003\nTest-Product-004\nTest-Product-005'},
- {'basic_rate':100},
- ]
- ]},
- ]);
- },
- () => cur_frm.save(),
- () => frappe.click_button('Update Rate and Availability'),
- () => {
- // get_item_details
- assert.ok(cur_frm.doc.items[0].item_name=='Test Product 4', "Item name correct");
- assert.ok(cur_frm.doc.total_incoming_value==500, " Incoming Value 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_transfer.js b/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_material_transfer.js
deleted file mode 100644
index fac0b4b..0000000
--- a/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_material_transfer.js
+++ /dev/null
@@ -1,33 +0,0 @@
-QUnit.module('Stock');
-
-QUnit.test("test material request", function(assert) {
- assert.expect(3);
- let done = assert.async();
- frappe.run_serially([
- () => {
- return frappe.tests.make('Stock Entry', [
- {purpose:'Material Transfer'},
- {from_warehouse:'Stores - '+frappe.get_abbr(frappe.defaults.get_default('Company'))},
- {to_warehouse:'Work In Progress - '+frappe.get_abbr(frappe.defaults.get_default('Company'))},
- {items: [
- [
- {'item_code': 'Test Product 1'},
- {'qty': 5},
- ]
- ]},
- ]);
- },
- () => cur_frm.save(),
- () => frappe.click_button('Update Rate and Availability'),
- () => {
- // get_item_details
- assert.ok(cur_frm.doc.items[0].item_name=='Test Product 1', "Item name correct");
- assert.ok(cur_frm.doc.total_outgoing_value==500, " Outgoing Value correct");
- assert.ok(cur_frm.doc.total_incoming_value==500, " Incoming Value 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_transfer_for_manufacture.js b/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_material_transfer_for_manufacture.js
deleted file mode 100644
index 9f85307..0000000
--- a/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_material_transfer_for_manufacture.js
+++ /dev/null
@@ -1,33 +0,0 @@
-QUnit.module('Stock');
-
-QUnit.test("test material Transfer to manufacture", function(assert) {
- assert.expect(3);
- let done = assert.async();
- frappe.run_serially([
- () => {
- return frappe.tests.make('Stock Entry', [
- {purpose:'Material Transfer for Manufacture'},
- {from_warehouse:'Stores - '+frappe.get_abbr(frappe.defaults.get_default('Company'))},
- {to_warehouse:'Work In Progress - '+frappe.get_abbr(frappe.defaults.get_default('Company'))},
- {items: [
- [
- {'item_code': 'Test Product 1'},
- {'qty': 1},
- ]
- ]},
- ]);
- },
- () => cur_frm.save(),
- () => frappe.click_button('Update Rate and Availability'),
- () => {
- // get_item_details
- assert.ok(cur_frm.doc.items[0].item_name=='Test Product 1', "Item name correct");
- assert.ok(cur_frm.doc.total_outgoing_value==100, " Outgoing Value correct");
- assert.ok(cur_frm.doc.total_incoming_value==100, " Incoming Value 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_repack.js b/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_repack.js
deleted file mode 100644
index 20f119a..0000000
--- a/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_repack.js
+++ /dev/null
@@ -1,41 +0,0 @@
-QUnit.module('Stock');
-
-QUnit.test("test repack", function(assert) {
- assert.expect(2);
- let done = assert.async();
- frappe.run_serially([
- () => {
- return frappe.tests.make('Stock Entry', [
- {purpose:'Repack'},
- {items: [
- [
- {'item_code': 'Test Product 1'},
- {'qty': 1},
- {'s_warehouse':'Stores - '+frappe.get_abbr(frappe.defaults.get_default('Company'))},
- ],
- [
- {'item_code': 'Test Product 2'},
- {'qty': 1},
- {'s_warehouse':'Stores - '+frappe.get_abbr(frappe.defaults.get_default('Company'))},
- ],
- [
- {'item_code': 'Test Product 3'},
- {'qty': 1},
- {'t_warehouse':'Work In Progress - '+frappe.get_abbr(frappe.defaults.get_default('Company'))},
- ],
- ]},
- ]);
- },
- () => cur_frm.save(),
- () => frappe.click_button('Update Rate and Availability'),
- () => {
- // get_item_details
- assert.ok(cur_frm.doc.total_outgoing_value==250, " Outgoing Value correct");
- assert.ok(cur_frm.doc.total_incoming_value==250, " Incoming Value 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_subcontract.js b/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_subcontract.js
deleted file mode 100644
index 8243426..0000000
--- a/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_subcontract.js
+++ /dev/null
@@ -1,33 +0,0 @@
-QUnit.module('Stock');
-
-QUnit.test("test material Transfer to manufacture", function(assert) {
- assert.expect(3);
- let done = assert.async();
- frappe.run_serially([
- () => {
- return frappe.tests.make('Stock Entry', [
- {purpose:'Send to Subcontractor'},
- {from_warehouse:'Work In Progress - '+frappe.get_abbr(frappe.defaults.get_default('Company'))},
- {to_warehouse:'Finished Goods - '+frappe.get_abbr(frappe.defaults.get_default('Company'))},
- {items: [
- [
- {'item_code': 'Test Product 1'},
- {'qty': 1},
- ]
- ]},
- ]);
- },
- () => cur_frm.save(),
- () => frappe.click_button('Update Rate and Availability'),
- () => {
- // get_item_details
- assert.ok(cur_frm.doc.items[0].item_name=='Test Product 1', "Item name correct");
- assert.ok(cur_frm.doc.total_outgoing_value==100, " Outgoing Value correct");
- assert.ok(cur_frm.doc.total_incoming_value==100, " Incoming Value correct");
- },
- () => frappe.tests.click_button('Submit'),
- () => frappe.tests.click_button('Yes'),
- () => frappe.timeout(0.3),
- () => done()
- ]);
-});
diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json
index 2651407..46ce9de 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json
+++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json
@@ -150,7 +150,7 @@
"fieldtype": "Float",
"in_filter": 1,
"in_list_view": 1,
- "label": "Actual Quantity",
+ "label": "Qty Change",
"oldfieldname": "actual_qty",
"oldfieldtype": "Currency",
"print_width": "150px",
@@ -189,7 +189,7 @@
"fieldname": "qty_after_transaction",
"fieldtype": "Float",
"in_filter": 1,
- "label": "Actual Qty After Transaction",
+ "label": "Qty After Transaction",
"oldfieldname": "bin_aqat",
"oldfieldtype": "Currency",
"print_width": "150px",
@@ -210,7 +210,7 @@
{
"fieldname": "stock_value",
"fieldtype": "Currency",
- "label": "Stock Value",
+ "label": "Balance Stock Value",
"oldfieldname": "stock_value",
"oldfieldtype": "Currency",
"options": "Company:company:default_currency",
@@ -219,14 +219,14 @@
{
"fieldname": "stock_value_difference",
"fieldtype": "Currency",
- "label": "Stock Value Difference",
+ "label": "Change in Stock Value",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fieldname": "stock_queue",
"fieldtype": "Text",
- "label": "Stock Queue (FIFO)",
+ "label": "FIFO Stock Queue (qty, rate)",
"oldfieldname": "fcfs_stack",
"oldfieldtype": "Text",
"print_hide": 1,
@@ -317,10 +317,11 @@
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2021-10-08 13:42:51.857631",
+ "modified": "2021-12-21 06:25:30.040801",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Ledger Entry",
+ "naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
@@ -338,5 +339,6 @@
}
],
"sort_field": "modified",
- "sort_order": "DESC"
-}
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
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 93bca7a..c538307 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
+++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
@@ -8,7 +8,7 @@
from frappe import _
from frappe.core.doctype.role.role import get_users
from frappe.model.document import Document
-from frappe.utils import add_days, cint, flt, formatdate, get_datetime, getdate
+from frappe.utils import add_days, cint, formatdate, get_datetime, getdate
from erpnext.accounts.utils import get_fiscal_year
from erpnext.controllers.item_variant import ItemTemplateCannotHaveStock
@@ -43,7 +43,6 @@
def on_submit(self):
self.check_stock_frozen_date()
- self.actual_amt_check()
self.calculate_batch_qty()
if not self.get("via_landed_cost_voucher"):
@@ -57,18 +56,6 @@
"sum(actual_qty)") or 0
frappe.db.set_value("Batch", self.batch_no, "batch_qty", batch_qty)
- 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 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:
- frappe.throw(_("Stock balance in Batch {0} will become negative {1} for Item {2} at Warehouse {3}")
- .format(self.batch_no, batch_bal_after_transaction, self.item_code, self.warehouse))
-
def validate_mandatory(self):
mandatory = ['warehouse','posting_date','voucher_type','voucher_no','company']
for k in mandatory:
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json
index b7d1497..3402972 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"autoname": "naming_series:",
"creation": "2013-03-28 10:35:31",
"description": "This tool helps you to update or fix the quantity and valuation of stock in the system. It is typically used to synchronise the system values and what actually exists in your warehouses.",
@@ -153,11 +154,12 @@
"icon": "fa fa-upload-alt",
"idx": 1,
"is_submittable": 1,
- "max_attachments": 1,
- "modified": "2020-04-08 17:02:47.196206",
+ "links": [],
+ "modified": "2021-11-30 01:33:51.437194",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Reconciliation",
+ "naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.js
deleted file mode 100644
index 666d2c7..0000000
--- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.js
+++ /dev/null
@@ -1,31 +0,0 @@
-QUnit.module('Stock');
-
-QUnit.test("test Stock Reconciliation", function(assert) {
- assert.expect(1);
- let done = assert.async();
- frappe.run_serially([
- () => frappe.set_route('List', 'Stock Reconciliation'),
- () => frappe.timeout(1),
- () => frappe.click_button('New'),
- () => cur_frm.set_value('company','For Testing'),
- () => frappe.click_button('Items'),
- () => {cur_dialog.set_value('warehouse','Stores - FT'); },
- () => frappe.timeout(0.5),
- () => frappe.click_button('Update'),
- () => {
- cur_frm.doc.items[0].qty = 150;
- cur_frm.refresh_fields('items');},
- () => frappe.timeout(0.5),
- () => cur_frm.set_value('expense_account','Stock Adjustment - FT'),
- () => cur_frm.set_value('cost_center','Main - FT'),
- () => cur_frm.save(),
- () => {
- // get_item_details
- assert.ok(cur_frm.doc.expense_account=='Stock Adjustment - FT', "expense_account correct");
- },
- () => frappe.tests.click_button('Submit'),
- () => frappe.tests.click_button('Yes'),
- () => frappe.timeout(0.3),
- () => done()
- ]);
-});
diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
index 48e339a..c4ddc9e 100644
--- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
@@ -24,11 +24,15 @@
class TestStockReconciliation(ERPNextTestCase):
@classmethod
- def setUpClass(self):
+ def setUpClass(cls):
super().setUpClass()
create_batch_or_serial_no_items()
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1)
+ def tearDown(self):
+ frappe.flags.dont_execute_stock_reposts = None
+
+
def test_reco_for_fifo(self):
self._test_reco_sle_gle("FIFO")
@@ -392,6 +396,41 @@
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")
+ def test_intermediate_sr_bin_update(self):
+ """Bin should show correct qty even for backdated entries.
+
+ -------------------------------------------
+ | creation | Var | Doc | Qty | balance qty
+ -------------------------------------------
+ | 1 | SR | Reco | 10 | 10 (posting date: today+10)
+ | 3 | SR2 | Reco | 11 | 11 (posting date: today+11)
+ | 2 | DN | DN | 5 | 6 <-- assert in BIN (posting date: today+12)
+ """
+ from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
+
+ # repost will make this test useless, qty should update in realtime without reposts
+ frappe.flags.dont_execute_stock_reposts = True
+ frappe.db.rollback()
+
+ 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=10, rate=100,
+ posting_date=add_days(nowdate(), 10))
+
+ dn = create_delivery_note(item_code=item_code, warehouse=warehouse, qty=5, rate=120,
+ posting_date=add_days(nowdate(), 12))
+ old_bin_qty = frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, "actual_qty")
+
+ sr2 = create_stock_reconciliation(item_code=item_code, warehouse=warehouse, qty=11, rate=100,
+ posting_date=add_days(nowdate(), 11))
+ new_bin_qty = frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, "actual_qty")
+
+ self.assertEqual(old_bin_qty + 1, new_bin_qty)
+ 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")
diff --git a/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.json b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.json
index 2474059..0facae8 100644
--- a/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.json
+++ b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.json
@@ -1,6 +1,7 @@
{
"actions": [],
"allow_rename": 1,
+ "beta": 1,
"creation": "2021-10-01 10:56:30.814787",
"doctype": "DocType",
"editable_grid": 1,
@@ -10,7 +11,8 @@
"limit_reposting_timeslot",
"start_time",
"end_time",
- "limits_dont_apply_on"
+ "limits_dont_apply_on",
+ "item_based_reposting"
],
"fields": [
{
@@ -44,12 +46,18 @@
"fieldname": "limit_reposting_timeslot",
"fieldtype": "Check",
"label": "Limit timeslot for Stock Reposting"
+ },
+ {
+ "default": "0",
+ "fieldname": "item_based_reposting",
+ "fieldtype": "Check",
+ "label": "Use Item based reposting"
}
],
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2021-10-01 11:27:28.981594",
+ "modified": "2021-11-02 01:22:45.155841",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Reposting Settings",
diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.py b/erpnext/stock/doctype/stock_settings/stock_settings.py
index 1de48b6..c1293cb 100644
--- a/erpnext/stock/doctype/stock_settings/stock_settings.py
+++ b/erpnext/stock/doctype/stock_settings/stock_settings.py
@@ -11,6 +11,8 @@
from frappe.utils import cint
from frappe.utils.html_utils import clean_html
+from erpnext.stock.utils import check_pending_reposting
+
class StockSettings(Document):
def validate(self):
@@ -36,6 +38,7 @@
self.validate_warehouses()
self.cant_change_valuation_method()
self.validate_clean_description_html()
+ self.validate_pending_reposts()
def validate_warehouses(self):
warehouse_fields = ["default_warehouse", "sample_retention_warehouse"]
@@ -64,6 +67,11 @@
# changed to text
frappe.enqueue('erpnext.stock.doctype.stock_settings.stock_settings.clean_all_descriptions', now=frappe.flags.in_test)
+ def validate_pending_reposts(self):
+ if self.stock_frozen_upto:
+ check_pending_reposting(self.stock_frozen_upto)
+
+
def on_update(self):
self.toggle_warehouse_field_for_inter_warehouse_transfer()
diff --git a/erpnext/stock/doctype/warehouse/test_warehouse.js b/erpnext/stock/doctype/warehouse/test_warehouse.js
deleted file mode 100644
index 850da1e..0000000
--- a/erpnext/stock/doctype/warehouse/test_warehouse.js
+++ /dev/null
@@ -1,19 +0,0 @@
-QUnit.test("test: warehouse", function (assert) {
- assert.expect(0);
- let done = assert.async();
-
- frappe.run_serially([
- // test warehouse creation
- () => frappe.set_route("List", "Warehouse"),
-
- // Create a Laptop Scrap Warehouse
- () => frappe.tests.make(
- "Warehouse", [
- {warehouse_name: "Laptop Scrap Warehouse"},
- {company: "For Testing"}
- ]
- ),
-
- () => done()
- ]);
-});
diff --git a/erpnext/stock/doctype/warehouse/test_warehouse.py b/erpnext/stock/doctype/warehouse/test_warehouse.py
index ca92936..26db264 100644
--- a/erpnext/stock/doctype/warehouse/test_warehouse.py
+++ b/erpnext/stock/doctype/warehouse/test_warehouse.py
@@ -33,65 +33,6 @@
self.assertEqual(p_warehouse.name, child_warehouse.parent_warehouse)
self.assertEqual(child_warehouse.is_group, 0)
- def test_warehouse_renaming(self):
- create_warehouse("Test Warehouse for Renaming 1", company="_Test Company with perpetual inventory")
- account = get_inventory_account("_Test Company with perpetual inventory", "Test Warehouse for Renaming 1 - TCP1")
- self.assertTrue(frappe.db.get_value("Warehouse", filters={"account": account}))
-
- # Rename with abbr
- if frappe.db.exists("Warehouse", "Test Warehouse for Renaming 2 - TCP1"):
- frappe.delete_doc("Warehouse", "Test Warehouse for Renaming 2 - TCP1")
- frappe.rename_doc("Warehouse", "Test Warehouse for Renaming 1 - TCP1", "Test Warehouse for Renaming 2 - TCP1")
-
- self.assertTrue(frappe.db.get_value("Warehouse",
- filters={"account": "Test Warehouse for Renaming 1 - TCP1"}))
-
- # Rename without abbr
- if frappe.db.exists("Warehouse", "Test Warehouse for Renaming 3 - TCP1"):
- frappe.delete_doc("Warehouse", "Test Warehouse for Renaming 3 - TCP1")
-
- frappe.rename_doc("Warehouse", "Test Warehouse for Renaming 2 - TCP1", "Test Warehouse for Renaming 3")
-
- self.assertTrue(frappe.db.get_value("Warehouse",
- filters={"account": "Test Warehouse for Renaming 1 - TCP1"}))
-
- # Another rename with multiple dashes
- if frappe.db.exists("Warehouse", "Test - Warehouse - Company - TCP1"):
- frappe.delete_doc("Warehouse", "Test - Warehouse - Company - TCP1")
- frappe.rename_doc("Warehouse", "Test Warehouse for Renaming 3 - TCP1", "Test - Warehouse - Company")
-
- def test_warehouse_merging(self):
- company = "_Test Company with perpetual inventory"
- create_warehouse("Test Warehouse for Merging 1", company=company,
- properties={"parent_warehouse": "All Warehouses - TCP1"})
- create_warehouse("Test Warehouse for Merging 2", company=company,
- properties={"parent_warehouse": "All Warehouses - TCP1"})
-
- make_stock_entry(item_code="_Test Item", target="Test Warehouse for Merging 1 - TCP1",
- qty=1, rate=100, company=company)
- make_stock_entry(item_code="_Test Item", target="Test Warehouse for Merging 2 - TCP1",
- qty=1, rate=100, company=company)
-
- existing_bin_qty = (
- cint(frappe.db.get_value("Bin",
- {"item_code": "_Test Item", "warehouse": "Test Warehouse for Merging 1 - TCP1"}, "actual_qty"))
- + cint(frappe.db.get_value("Bin",
- {"item_code": "_Test Item", "warehouse": "Test Warehouse for Merging 2 - TCP1"}, "actual_qty"))
- )
-
- frappe.rename_doc("Warehouse", "Test Warehouse for Merging 1 - TCP1",
- "Test Warehouse for Merging 2 - TCP1", merge=True)
-
- self.assertFalse(frappe.db.exists("Warehouse", "Test Warehouse for Merging 1 - TCP1"))
-
- bin_qty = frappe.db.get_value("Bin",
- {"item_code": "_Test Item", "warehouse": "Test Warehouse for Merging 2 - TCP1"}, "actual_qty")
-
- self.assertEqual(bin_qty, existing_bin_qty)
-
- self.assertTrue(frappe.db.get_value("Warehouse",
- filters={"account": "Test Warehouse for Merging 2 - TCP1"}))
-
def test_unlinking_warehouse_from_item_defaults(self):
company = "_Test Company"
diff --git a/erpnext/stock/doctype/warehouse/warehouse.json b/erpnext/stock/doctype/warehouse/warehouse.json
index 9b90932..05076b5 100644
--- a/erpnext/stock/doctype/warehouse/warehouse.json
+++ b/erpnext/stock/doctype/warehouse/warehouse.json
@@ -1,7 +1,6 @@
{
"actions": [],
"allow_import": 1,
- "allow_rename": 1,
"creation": "2013-03-07 18:50:32",
"description": "A logical Warehouse against which stock entries are made.",
"doctype": "DocType",
@@ -245,7 +244,7 @@
"idx": 1,
"is_tree": 1,
"links": [],
- "modified": "2021-04-09 19:54:56.263965",
+ "modified": "2021-12-03 04:40:06.414630",
"modified_by": "Administrator",
"module": "Stock",
"name": "Warehouse",
diff --git a/erpnext/stock/doctype/warehouse/warehouse.py b/erpnext/stock/doctype/warehouse/warehouse.py
index b9dbc38..9cfad86 100644
--- a/erpnext/stock/doctype/warehouse/warehouse.py
+++ b/erpnext/stock/doctype/warehouse/warehouse.py
@@ -10,7 +10,6 @@
from frappe.utils import cint, flt
from frappe.utils.nestedset import NestedSet
-import erpnext
from erpnext.stock import get_warehouse_account
@@ -68,57 +67,6 @@
return frappe.db.sql("""select name from `tabWarehouse`
where parent_warehouse = %s limit 1""", self.name)
- def before_rename(self, old_name, new_name, merge=False):
- super(Warehouse, self).before_rename(old_name, new_name, merge)
-
- # Add company abbr if not provided
- new_warehouse = erpnext.encode_company_abbr(new_name, self.company)
-
- if merge:
- if not frappe.db.exists("Warehouse", new_warehouse):
- frappe.throw(_("Warehouse {0} does not exist").format(new_warehouse))
-
- if self.company != frappe.db.get_value("Warehouse", new_warehouse, "company"):
- frappe.throw(_("Both Warehouse must belong to same Company"))
-
- return new_warehouse
-
- def after_rename(self, old_name, new_name, merge=False):
- super(Warehouse, self).after_rename(old_name, new_name, merge)
-
- new_warehouse_name = self.get_new_warehouse_name_without_abbr(new_name)
- self.db_set("warehouse_name", new_warehouse_name)
-
- if merge:
- self.recalculate_bin_qty(new_name)
-
- def get_new_warehouse_name_without_abbr(self, name):
- company_abbr = frappe.get_cached_value('Company', self.company, "abbr")
- parts = name.rsplit(" - ", 1)
-
- if parts[-1].lower() == company_abbr.lower():
- name = parts[0]
-
- return name
-
- def recalculate_bin_qty(self, new_name):
- from erpnext.stock.stock_balance import repost_stock
- frappe.db.auto_commit_on_many_writes = 1
- existing_allow_negative_stock = frappe.db.get_value("Stock Settings", None, "allow_negative_stock")
- frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1)
-
- repost_stock_for_items = frappe.db.sql_list("""select distinct item_code
- from tabBin where warehouse=%s""", new_name)
-
- # Delete all existing bins to avoid duplicate bins for the same item and warehouse
- frappe.db.sql("delete from `tabBin` where warehouse=%s", new_name)
-
- for item_code in repost_stock_for_items:
- repost_stock(item_code, new_name)
-
- frappe.db.set_value("Stock Settings", None, "allow_negative_stock", existing_allow_negative_stock)
- frappe.db.auto_commit_on_many_writes = 0
-
def convert_to_group_or_ledger(self):
if self.is_group:
self.convert_to_ledger()
diff --git a/erpnext/stock/form_tour/item/item.json b/erpnext/stock/form_tour/item/item.json
index 821e91b..5369366 100644
--- a/erpnext/stock/form_tour/item/item.json
+++ b/erpnext/stock/form_tour/item/item.json
@@ -2,15 +2,17 @@
"creation": "2021-08-24 17:56:40.754909",
"docstatus": 0,
"doctype": "Form Tour",
+ "first_document": 0,
"idx": 0,
+ "include_name_field": 0,
"is_standard": 1,
- "modified": "2021-08-24 18:04:50.928431",
+ "modified": "2021-11-24 17:59:44.559001",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item",
"owner": "Administrator",
"reference_doctype": "Item",
- "save_on_complete": 0,
+ "save_on_complete": 1,
"steps": [
{
"description": "Enter code for Asset Item",
@@ -37,13 +39,26 @@
"title": "Asset Item Name"
},
{
+ "description": "Select an Item Group",
+ "field": "",
+ "fieldname": "item_group",
+ "fieldtype": "Link",
+ "has_next_condition": 0,
+ "is_table_field": 0,
+ "label": "Item Group",
+ "parent_field": "",
+ "position": "Right",
+ "title": "Item Group"
+ },
+ {
"description": "Check this field to make this an Asset Item",
"field": "",
"fieldname": "is_fixed_asset",
"fieldtype": "Check",
- "has_next_condition": 0,
+ "has_next_condition": 1,
"is_table_field": 0,
"label": "Is Fixed Asset",
+ "next_step_condition": "eval:doc.is_fixed_asset",
"parent_field": "",
"position": "Bottom",
"title": "Is this a Fixed Asset?"
@@ -53,9 +68,10 @@
"field": "",
"fieldname": "auto_create_assets",
"fieldtype": "Check",
- "has_next_condition": 0,
+ "has_next_condition": 1,
"is_table_field": 0,
"label": "Auto Create Assets on Purchase",
+ "next_step_condition": "eval:doc.auto_create_assets",
"parent_field": "",
"position": "Bottom",
"title": "Auto Create Asset on Purchase"
@@ -69,7 +85,7 @@
"is_table_field": 0,
"label": "Asset Category",
"parent_field": "",
- "position": "Bottom",
+ "position": "Left",
"title": "Asset Category"
},
{
@@ -81,9 +97,9 @@
"is_table_field": 0,
"label": "Asset Naming Series",
"parent_field": "",
- "position": "Bottom",
+ "position": "Left",
"title": "Asset Naming Series"
}
],
"title": "Item"
-}
+}
\ No newline at end of file
diff --git a/erpnext/stock/form_tour/item_general/item_general.json b/erpnext/stock/form_tour/item_general/item_general.json
new file mode 100644
index 0000000..b468d27
--- /dev/null
+++ b/erpnext/stock/form_tour/item_general/item_general.json
@@ -0,0 +1,79 @@
+{
+ "creation": "2021-12-02 10:37:55.433087",
+ "docstatus": 0,
+ "doctype": "Form Tour",
+ "first_document": 0,
+ "idx": 0,
+ "include_name_field": 0,
+ "is_standard": 1,
+ "modified": "2021-12-02 10:37:55.433087",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Item General",
+ "owner": "Administrator",
+ "reference_doctype": "Item",
+ "save_on_complete": 1,
+ "steps": [
+ {
+ "description": "Enter code for the Item",
+ "field": "",
+ "fieldname": "item_code",
+ "fieldtype": "Data",
+ "has_next_condition": 0,
+ "is_table_field": 0,
+ "label": "Item Code",
+ "parent_field": "",
+ "position": "Right",
+ "title": "Item Code"
+ },
+ {
+ "description": "Enter name for the Item",
+ "field": "",
+ "fieldname": "item_name",
+ "fieldtype": "Data",
+ "has_next_condition": 0,
+ "is_table_field": 0,
+ "label": "Item Name",
+ "parent_field": "",
+ "position": "Right",
+ "title": "Item Name"
+ },
+ {
+ "description": "Select an Item Group",
+ "field": "",
+ "fieldname": "item_group",
+ "fieldtype": "Link",
+ "has_next_condition": 0,
+ "is_table_field": 0,
+ "label": "Item Group",
+ "parent_field": "",
+ "position": "Right",
+ "title": "Item Group"
+ },
+ {
+ "description": "This is the default measuring unit that you will use for your product. It could be Nos, Kgs, Meters, etc.",
+ "field": "",
+ "fieldname": "stock_uom",
+ "fieldtype": "Link",
+ "has_next_condition": 0,
+ "is_table_field": 0,
+ "label": "Default Unit of Measure",
+ "parent_field": "",
+ "position": "Right",
+ "title": "Default Unit of Measurement"
+ },
+ {
+ "description": "When creating an Item, entering a value for this field will automatically create an Item Price at the backend. Entering a value after the Item has been saved will not work. In this case, the Item Price is created from any transactions with the Item.",
+ "field": "",
+ "fieldname": "standard_rate",
+ "fieldtype": "Currency",
+ "has_next_condition": 0,
+ "is_table_field": 0,
+ "label": "Standard Selling Rate",
+ "parent_field": "",
+ "position": "Left",
+ "title": "Standard Selling Rate"
+ }
+ ],
+ "title": "Item General"
+}
\ No newline at end of file
diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py
index e00382b..06f8fa7 100644
--- a/erpnext/stock/get_item_details.py
+++ b/erpnext/stock/get_item_details.py
@@ -299,7 +299,7 @@
"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),
+ "discount_account": 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,
@@ -317,6 +317,7 @@
"net_rate": 0.0,
"net_amount": 0.0,
"discount_percentage": 0.0,
+ "discount_amount": 0.0,
"supplier": get_default_supplier(args, item_defaults, item_group_defaults, brand_defaults),
"update_stock": args.get("update_stock") if args.get('doctype') in ['Sales Invoice', 'Purchase Invoice'] else 0,
"delivered_by_supplier": item.delivered_by_supplier if args.get("doctype") in ["Sales Order", "Sales Invoice"] else 0,
@@ -326,7 +327,8 @@
"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_uom": args.get("weight_uom") or item.get("weight_uom"),
+ "grant_commission": item.get("grant_commission")
})
if item.get("enable_deferred_revenue") or item.get("enable_deferred_expense"):
@@ -1095,7 +1097,7 @@
}
def apply_price_list_on_item(args):
- item_doc = frappe.get_doc("Item", args.item_code)
+ item_doc = frappe.db.get_value("Item", args.item_code, ['name', 'variant_of'], as_dict=1)
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/page/warehouse_capacity_summary/warehouse_capacity_summary.html b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.html
index de7e38e..adab478 100644
--- a/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.html
+++ b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.html
@@ -1,19 +1,19 @@
{% for d in data %}
<div class="dashboard-list-item" style="padding: 7px 15px;">
<div class="row">
- <div class="col-sm-2 small" style="margin-top: 8px;">
+ <div class="col-sm-2" style="margin-top: 8px;">
<a data-type="warehouse" data-name="{{ d.warehouse }}">{{ d.warehouse }}</a>
</div>
- <div class="col-sm-2 small" style="margin-top: 8px; ">
+ <div class="col-sm-2" style="margin-top: 8px; ">
<a data-type="item" data-name="{{ d.item_code }}">{{ d.item_code }}</a>
</div>
- <div class="col-sm-1 small" style="margin-top: 8px; ">
+ <div class="col-sm-1" style="margin-top: 8px; ">
{{ d.stock_capacity }}
</div>
- <div class="col-sm-2 small" style="margin-top: 8px; ">
+ <div class="col-sm-2" style="margin-top: 8px; ">
{{ d.actual_qty }}
</div>
- <div class="col-sm-2 small">
+ <div class="col-sm-2">
<div class="progress" title="Occupied Qty: {{ d.actual_qty }}" style="margin-bottom: 4px; height: 7px; margin-top: 14px;">
<div class="progress-bar" role="progressbar"
aria-valuenow="{{ d.percent_occupied }}"
@@ -23,16 +23,19 @@
</div>
</div>
</div>
- <div class="col-sm-1 small" style="margin-top: 8px;">
+ <div class="col-sm-1" style="margin-top: 8px;">
{{ d.percent_occupied }}%
</div>
{% if can_write %}
- <div class="col-sm-1 text-right" style="margin-top: 2px;">
- <button class="btn btn-default btn-xs btn-edit"
- style="margin-top: 4px;margin-bottom: 4px;"
- data-warehouse="{{ d.warehouse }}"
- data-item="{{ escape(d.item_code) }}"
- data-company="{{ escape(d.company) }}">{{ __("Edit Capacity") }}</a>
+ <div class="col-sm-2 text-right" style="margin-top: 2px;">
+ <button
+ class="btn btn-default btn-xs btn-edit"
+ style="margin: 4px 0; float: left;"
+ data-warehouse="{{ d.warehouse }}"
+ data-item="{{ escape(d.item_code) }}"
+ data-company="{{ escape(d.company) }}">
+ {{ __("Edit Capacity") }}
+ </button>
</div>
{% endif %}
</div>
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 c0ffdc9..ea27dd2 100644
--- a/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.js
+++ b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.js
@@ -4,7 +4,7 @@
title: 'Warehouse Capacity Summary',
single_column: true
});
- page.set_secondary_action('Refresh', () => page.capacity_dashboard.refresh(), 'octicon octicon-sync');
+ page.set_secondary_action('Refresh', () => page.capacity_dashboard.refresh(), 'refresh');
page.start = 0;
page.company_field = page.add_field({
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 7ac5e64..1183ad4 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
@@ -1,18 +1,18 @@
<div class="dashboard-list-item" style="padding: 12px 15px;">
<div class="row">
- <div class="col-sm-2 small text-muted" style="margin-top: 8px;">
+ <div class="col-sm-2 text-muted" style="margin-top: 8px;">
Warehouse
</div>
- <div class="col-sm-2 small text-muted" style="margin-top: 8px;">
+ <div class="col-sm-2 text-muted" style="margin-top: 8px;">
Item
</div>
- <div class="col-sm-1 small text-muted" style="margin-top: 8px;">
+ <div class="col-sm-1 text-muted" style="margin-top: 8px;">
Stock Capacity
</div>
- <div class="col-sm-2 small text-muted" style="margin-top: 8px;">
+ <div class="col-sm-2 text-muted" style="margin-top: 8px;">
Balance Stock Qty
</div>
- <div class="col-sm-2 small text-muted" style="margin-top: 8px;">
+ <div class="col-sm-2 text-muted" style="margin-top: 8px;">
% Occupied
</div>
</div>
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 d452ffd..be8597d 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
@@ -73,7 +73,7 @@
fields = ['name', 'voucher_type', 'voucher_no', 'item_code', 'serial_no as serial_nos', 'actual_qty',
'posting_date', 'posting_time', 'company', 'warehouse', '(stock_value_difference / actual_qty) as valuation_rate']
- filters = {'serial_no': ("is", "set")}
+ filters = {'serial_no': ("is", "set"), "is_cancelled": 0}
if report_filters.get('item_code'):
filters['item_code'] = report_filters.get('item_code')
diff --git a/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py b/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py
index 314f160..3f49065 100644
--- a/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py
+++ b/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py
@@ -48,6 +48,7 @@
conditions = [get_item_group_condition(filters.get("item_group"))]
if filters.get("brand"):
conditions.append("item.brand=%(brand)s")
+ conditions.append("is_stock_item = 1")
return frappe.db.sql("""select name, item_name, description, brand, item_group,
safety_stock, lead_time_days from `tabItem` item where {}"""
diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py
index 0ebe4f9..e6dfc97 100644
--- a/erpnext/stock/report/stock_ageing/stock_ageing.py
+++ b/erpnext/stock/report/stock_ageing/stock_ageing.py
@@ -3,6 +3,7 @@
from operator import itemgetter
+from typing import Dict, List, Tuple, Union
import frappe
from frappe import _
@@ -10,19 +11,29 @@
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
+Filters = frappe._dict
-def execute(filters=None):
- columns = get_columns(filters)
- item_details = get_fifo_queue(filters)
+def execute(filters: Filters = None) -> Tuple:
to_date = filters["to_date"]
- _func = itemgetter(1)
+ columns = get_columns(filters)
+ item_details = FIFOSlots(filters).generate()
+ data = format_report_data(filters, item_details, to_date)
+
+ chart_data = get_chart_data(data, filters)
+
+ return columns, data, None, chart_data
+
+def format_report_data(filters: Filters, item_details: Dict, to_date: str) -> List[Dict]:
+ "Returns ordered, formatted data with ranges."
+ _func = itemgetter(1)
data = []
+
for item, item_dict in item_details.items():
earliest_age, latest_age = 0, 0
+ details = item_dict["details"]
fifo_queue = sorted(filter(_func, item_dict["fifo_queue"]), key=_func)
- details = item_dict["details"]
if not fifo_queue: continue
@@ -31,23 +42,22 @@
latest_age = date_diff(to_date, fifo_queue[-1][1])
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]
+ row = [details.name, details.item_name, details.description,
+ details.item_group, details.brand]
if filters.get("show_warehouse_wise_stock"):
row.append(details.warehouse)
row.extend([item_dict.get("total_qty"), average_age,
range1, range2, range3, above_range3,
- earliest_age, latest_age, details.stock_uom])
+ earliest_age, latest_age,
+ details.stock_uom])
data.append(row)
- chart_data = get_chart_data(data, filters)
+ return data
- return columns, data, None, chart_data
-
-def get_average_age(fifo_queue, to_date):
+def get_average_age(fifo_queue: List, to_date: str) -> float:
batch_age = age_qty = total_qty = 0.0
for batch in fifo_queue:
batch_age = date_diff(to_date, batch[1])
@@ -61,7 +71,7 @@
return flt(age_qty / total_qty, 2) if total_qty else 0.0
-def get_range_age(filters, fifo_queue, to_date, item_dict):
+def get_range_age(filters: Filters, fifo_queue: List, to_date: str, item_dict: Dict) -> Tuple:
range1 = range2 = range3 = above_range3 = 0.0
for item in fifo_queue:
@@ -79,7 +89,7 @@
return range1, range2, range3, above_range3
-def get_columns(filters):
+def get_columns(filters: Filters) -> List[Dict]:
range_columns = []
setup_ageing_columns(filters, range_columns)
columns = [
@@ -164,106 +174,7 @@
return columns
-def get_fifo_queue(filters, sle=None):
- item_details = {}
- transferred_item_details = {}
- serial_no_batch_purchase_details = {}
-
- if sle == None:
- sle = get_stock_ledger_entries(filters)
-
- for d in sle:
- key = (d.name, d.warehouse) if filters.get('show_warehouse_wise_stock') else d.name
- item_details.setdefault(key, {"details": d, "fifo_queue": []})
- fifo_queue = item_details[key]["fifo_queue"]
-
- transferred_item_key = (d.voucher_no, d.name, d.warehouse)
- transferred_item_details.setdefault(transferred_item_key, [])
-
- if d.voucher_type == "Stock Reconciliation":
- d.actual_qty = flt(d.qty_after_transaction) - flt(item_details[key].get("qty_after_transaction", 0))
-
- serial_no_list = get_serial_nos(d.serial_no) if d.serial_no else []
-
- if d.actual_qty > 0:
- if transferred_item_details.get(transferred_item_key):
- batch = transferred_item_details[transferred_item_key][0]
- fifo_queue.append(batch)
- transferred_item_details[transferred_item_key].pop(0)
- else:
- if serial_no_list:
- for serial_no in serial_no_list:
- if serial_no_batch_purchase_details.get(serial_no):
- fifo_queue.append([serial_no, serial_no_batch_purchase_details.get(serial_no)])
- else:
- serial_no_batch_purchase_details.setdefault(serial_no, d.posting_date)
- fifo_queue.append([serial_no, d.posting_date])
- else:
- fifo_queue.append([d.actual_qty, d.posting_date])
- else:
- if serial_no_list:
- 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:
- batch = fifo_queue[0] if fifo_queue else [0, None]
- if 0 < flt(batch[0]) <= qty_to_pop:
- # if batch qty > 0
- # not enough or exactly same qty in current batch, clear batch
- qty_to_pop -= flt(batch[0])
- transferred_item_details[transferred_item_key].append(fifo_queue.pop(0))
- else:
- # all from current batch
- batch[0] = flt(batch[0]) - qty_to_pop
- transferred_item_details[transferred_item_key].append([qty_to_pop, batch[1]])
- qty_to_pop = 0
-
- item_details[key]["qty_after_transaction"] = d.qty_after_transaction
-
- if "total_qty" not in item_details[key]:
- item_details[key]["total_qty"] = d.actual_qty
- 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.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, has_serial_no
- from `tabItem` {item_conditions}) item
- where item_code = item.name and
- company = %(company)s and
- posting_date <= %(to_date)s and
- is_cancelled != 1
- {sle_conditions}
- order by posting_date, posting_time, sle.creation, actual_qty""" #nosec
- .format(item_conditions=get_item_conditions(filters),
- sle_conditions=get_sle_conditions(filters)), filters, as_dict=True)
-
-def get_item_conditions(filters):
- conditions = []
- if filters.get("item_code"):
- conditions.append("item_code=%(item_code)s")
- if filters.get("brand"):
- conditions.append("brand=%(brand)s")
-
- return "where {}".format(" and ".join(conditions)) if conditions else ""
-
-def get_sle_conditions(filters):
- conditions = []
- if filters.get("warehouse"):
- lft, rgt = frappe.db.get_value('Warehouse', filters.get("warehouse"), ['lft', 'rgt'])
- conditions.append("""warehouse in (select wh.name from `tabWarehouse` wh
- where wh.lft >= {0} and rgt <= {1})""".format(lft, rgt))
-
- return "and {}".format(" and ".join(conditions)) if conditions else ""
-
-def get_chart_data(data, filters):
+def get_chart_data(data: List, filters: Filters) -> Dict:
if not data:
return []
@@ -294,17 +205,201 @@
"type" : "bar"
}
-def setup_ageing_columns(filters, range_columns):
- for i, label in enumerate(["0-{range1}".format(range1=filters["range1"]),
- "{range1}-{range2}".format(range1=cint(filters["range1"])+ 1, range2=filters["range2"]),
- "{range2}-{range3}".format(range2=cint(filters["range2"])+ 1, range3=filters["range3"]),
- "{range3}-{above}".format(range3=cint(filters["range3"])+ 1, above=_("Above"))]):
- add_column(range_columns, label="Age ("+ label +")", fieldname='range' + str(i+1))
+def setup_ageing_columns(filters: Filters, range_columns: List):
+ ranges = [
+ f"0 - {filters['range1']}",
+ f"{cint(filters['range1']) + 1} - {cint(filters['range2'])}",
+ f"{cint(filters['range2']) + 1} - {cint(filters['range3'])}",
+ f"{cint(filters['range3']) + 1} - {_('Above')}"
+ ]
+ for i, label in enumerate(ranges):
+ fieldname = 'range' + str(i+1)
+ add_column(range_columns, label=f"Age ({label})",fieldname=fieldname)
-def add_column(range_columns, label, fieldname, fieldtype='Float', width=140):
+def add_column(range_columns: List, label:str, fieldname: str, fieldtype: str = 'Float', width: int = 140):
range_columns.append(dict(
label=label,
fieldname=fieldname,
fieldtype=fieldtype,
width=width
))
+
+
+class FIFOSlots:
+ "Returns FIFO computed slots of inwarded stock as per date."
+
+ def __init__(self, filters: Dict = None , sle: List = None):
+ self.item_details = {}
+ self.transferred_item_details = {}
+ self.serial_no_batch_purchase_details = {}
+ self.filters = filters
+ self.sle = sle
+
+ def generate(self) -> Dict:
+ """
+ Returns dict of the foll.g structure:
+ Key = Item A / (Item A, Warehouse A)
+ Key: {
+ 'details' -> Dict: ** item details **,
+ 'fifo_queue' -> List: ** list of lists containing entries/slots for existing stock,
+ consumed/updated and maintained via FIFO. **
+ }
+ """
+ if self.sle is None:
+ self.sle = self.__get_stock_ledger_entries()
+
+ for d in self.sle:
+ key, fifo_queue, transferred_item_key = self.__init_key_stores(d)
+
+ if d.voucher_type == "Stock Reconciliation":
+ prev_balance_qty = self.item_details[key].get("qty_after_transaction", 0)
+ d.actual_qty = flt(d.qty_after_transaction) - flt(prev_balance_qty)
+
+ serial_nos = get_serial_nos(d.serial_no) if d.serial_no else []
+
+ if d.actual_qty > 0:
+ self.__compute_incoming_stock(d, fifo_queue, transferred_item_key, serial_nos)
+ else:
+ self.__compute_outgoing_stock(d, fifo_queue, transferred_item_key, serial_nos)
+
+ self.__update_balances(d, key)
+
+ return self.item_details
+
+ def __init_key_stores(self, row: Dict) -> Tuple:
+ "Initialise keys and FIFO Queue."
+
+ key = (row.name, row.warehouse) if self.filters.get('show_warehouse_wise_stock') else row.name
+ self.item_details.setdefault(key, {"details": row, "fifo_queue": []})
+ fifo_queue = self.item_details[key]["fifo_queue"]
+
+ transferred_item_key = (row.voucher_no, row.name, row.warehouse)
+ self.transferred_item_details.setdefault(transferred_item_key, [])
+
+ return key, fifo_queue, transferred_item_key
+
+ def __compute_incoming_stock(self, row: Dict, fifo_queue: List, transfer_key: Tuple, serial_nos: List):
+ "Update FIFO Queue on inward stock."
+
+ if self.transferred_item_details.get(transfer_key):
+ # inward/outward from same voucher, item & warehouse
+ slot = self.transferred_item_details[transfer_key].pop(0)
+ fifo_queue.append(slot)
+ else:
+ if not serial_nos:
+ if fifo_queue and flt(fifo_queue[0][0]) < 0:
+ # neutralize negative stock by adding positive stock
+ fifo_queue[0][0] += flt(row.actual_qty)
+ fifo_queue[0][1] = row.posting_date
+ else:
+ fifo_queue.append([flt(row.actual_qty), row.posting_date])
+ return
+
+ for serial_no in serial_nos:
+ if self.serial_no_batch_purchase_details.get(serial_no):
+ fifo_queue.append([serial_no, self.serial_no_batch_purchase_details.get(serial_no)])
+ else:
+ self.serial_no_batch_purchase_details.setdefault(serial_no, row.posting_date)
+ fifo_queue.append([serial_no, row.posting_date])
+
+ def __compute_outgoing_stock(self, row: Dict, fifo_queue: List, transfer_key: Tuple, serial_nos: List):
+ "Update FIFO Queue on outward stock."
+ if serial_nos:
+ fifo_queue[:] = [serial_no for serial_no in fifo_queue if serial_no[0] not in serial_nos]
+ return
+
+ qty_to_pop = abs(row.actual_qty)
+ while qty_to_pop:
+ slot = fifo_queue[0] if fifo_queue else [0, None]
+ if 0 < flt(slot[0]) <= qty_to_pop:
+ # qty to pop >= slot qty
+ # if +ve and not enough or exactly same balance in current slot, consume whole slot
+ qty_to_pop -= flt(slot[0])
+ self.transferred_item_details[transfer_key].append(fifo_queue.pop(0))
+ elif not fifo_queue:
+ # negative stock, no balance but qty yet to consume
+ fifo_queue.append([-(qty_to_pop), row.posting_date])
+ self.transferred_item_details[transfer_key].append([row.actual_qty, row.posting_date])
+ qty_to_pop = 0
+ else:
+ # qty to pop < slot qty, ample balance
+ # consume actual_qty from first slot
+ slot[0] = flt(slot[0]) - qty_to_pop
+ self.transferred_item_details[transfer_key].append([qty_to_pop, slot[1]])
+ qty_to_pop = 0
+
+ def __update_balances(self, row: Dict, key: Union[Tuple, str]):
+ self.item_details[key]["qty_after_transaction"] = row.qty_after_transaction
+
+ if "total_qty" not in self.item_details[key]:
+ self.item_details[key]["total_qty"] = row.actual_qty
+ else:
+ self.item_details[key]["total_qty"] += row.actual_qty
+
+ self.item_details[key]["has_serial_no"] = row.has_serial_no
+
+ def __get_stock_ledger_entries(self) -> List[Dict]:
+ sle = frappe.qb.DocType("Stock Ledger Entry")
+ item = self.__get_item_query() # used as derived table in sle query
+
+ sle_query = (
+ frappe.qb.from_(sle).from_(item)
+ .select(
+ item.name, item.item_name, item.item_group,
+ item.brand, item.description,
+ item.stock_uom, item.has_serial_no,
+ sle.actual_qty, sle.posting_date,
+ sle.voucher_type, sle.voucher_no,
+ sle.serial_no, sle.batch_no,
+ sle.qty_after_transaction, sle.warehouse
+ ).where(
+ (sle.item_code == item.name)
+ & (sle.company == self.filters.get("company"))
+ & (sle.posting_date <= self.filters.get("to_date"))
+ & (sle.is_cancelled != 1)
+ )
+ )
+
+ if self.filters.get("warehouse"):
+ sle_query = self.__get_warehouse_conditions(sle, sle_query)
+
+ sle_query = sle_query.orderby(
+ sle.posting_date, sle.posting_time, sle.creation, sle.actual_qty
+ )
+
+ return sle_query.run(as_dict=True)
+
+ def __get_item_query(self) -> str:
+ item_table = frappe.qb.DocType("Item")
+
+ item = frappe.qb.from_("Item").select(
+ "name", "item_name", "description", "stock_uom",
+ "brand", "item_group", "has_serial_no"
+ )
+
+ if self.filters.get("item_code"):
+ item = item.where(item_table.item_code == self.filters.get("item_code"))
+
+ if self.filters.get("brand"):
+ item = item.where(item_table.brand == self.filters.get("brand"))
+
+ return item
+
+ def __get_warehouse_conditions(self, sle, sle_query) -> str:
+ warehouse = frappe.qb.DocType("Warehouse")
+ lft, rgt = frappe.db.get_value(
+ "Warehouse",
+ self.filters.get("warehouse"),
+ ['lft', 'rgt']
+ )
+
+ warehouse_results = (
+ frappe.qb.from_(warehouse)
+ .select("name").where(
+ (warehouse.lft >= lft)
+ & (warehouse.rgt <= rgt)
+ ).run()
+ )
+ warehouse_results = [x[0] for x in warehouse_results]
+
+ return sle_query.where(sle.warehouse.isin(warehouse_results))
diff --git a/erpnext/stock/report/stock_ageing/stock_ageing_fifo_logic.md b/erpnext/stock/report/stock_ageing/stock_ageing_fifo_logic.md
new file mode 100644
index 0000000..5ffe97f
--- /dev/null
+++ b/erpnext/stock/report/stock_ageing/stock_ageing_fifo_logic.md
@@ -0,0 +1,73 @@
+### Concept of FIFO Slots
+
+Since we need to know age-wise remaining stock, we maintain all the inward entries as slots. So each time stock comes in, a slot is added for the same.
+
+Eg. For Item A:
+----------------------
+Date | Qty | Queue
+----------------------
+1st | +50 | [[50, 1-12-2021]]
+2nd | +20 | [[50, 1-12-2021], [20, 2-12-2021]]
+----------------------
+
+Now the queue can tell us the total stock and also how old the stock is.
+Here, the balance qty is 70.
+50 qty is (today-the 1st) days old
+20 qty is (today-the 2nd) days old
+
+### Calculation of FIFO Slots
+
+#### Case 1: Outward from sufficient balance qty
+----------------------
+Date | Qty | Queue
+----------------------
+1st | +50 | [[50, 1-12-2021]]
+2nd | -20 | [[30, 1-12-2021]]
+2nd | +20 | [[30, 1-12-2021], [20, 2-12-2021]]
+
+Here after the first entry, while issuing 20 qty:
+- **since 20 is lesser than the balance**, **qty_to_pop (20)** is simply consumed from first slot (FIFO consumption)
+- Any inward entry after as usual will get its own slot added to the queue
+
+#### Case 2: Outward from sufficient cumulative (slots) balance qty
+----------------------
+Date | Qty | Queue
+----------------------
+1st | +50 | [[50, 1-12-2021]]
+2nd | +20 | [[50, 1-12-2021], [20, 2-12-2021]]
+2nd | -60 | [[10, 2-12-2021]]
+
+- Consumption happens slot wise. First slot 1 is consumed
+- Since **qty_to_pop (60) is greater than slot 1 qty (50)**, the entire slot is consumed and popped
+- Now the queue is [[20, 2-12-2021]], and **qty_to_pop=10** (remaining qty to pop)
+- It then goes ahead to the next slot and consumes 10 from it
+- Now the queue is [[10, 2-12-2021]]
+
+#### Case 3: Outward from insufficient balance qty
+> This case is possible only if **Allow Negative Stock** was enabled at some point/is enabled.
+
+----------------------
+Date | Qty | Queue
+----------------------
+1st | +50 | [[50, 1-12-2021]]
+2nd | -60 | [[-10, 1-12-2021]]
+
+- Since **qty_to_pop (60)** is more than the balance in slot 1, the entire slot is consumed and popped
+- Now the queue is **empty**, and **qty_to_pop=10** (remaining qty to pop)
+- Since we still have more to consume, we append the balance since 60 is issued from 50 i.e. -10.
+- We register this negative value, since the stock issue has caused the balance to become negative
+
+Now when stock is inwarded:
+- Instead of adding a slot we check if there are any negative balances.
+- If yes, we keep adding positive stock to it until we make the balance positive.
+- Once the balance is positive, the next inward entry will add a new slot in the queue
+
+Eg:
+----------------------
+Date | Qty | Queue
+----------------------
+1st | +50 | [[50, 1-12-2021]]
+2nd | -60 | [[-10, 1-12-2021]]
+3rd | +5 | [[-5, 3-12-2021]]
+4th | +10 | [[5, 4-12-2021]]
+4th | +20 | [[5, 4-12-2021], [20, 4-12-2021]]
\ No newline at end of file
diff --git a/erpnext/stock/report/stock_ageing/test_stock_ageing.py b/erpnext/stock/report/stock_ageing/test_stock_ageing.py
new file mode 100644
index 0000000..949bb7c
--- /dev/null
+++ b/erpnext/stock/report/stock_ageing/test_stock_ageing.py
@@ -0,0 +1,126 @@
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+import frappe
+
+from erpnext.stock.report.stock_ageing.stock_ageing import FIFOSlots
+from erpnext.tests.utils import ERPNextTestCase
+
+
+class TestStockAgeing(ERPNextTestCase):
+ def setUp(self) -> None:
+ self.filters = frappe._dict(
+ company="_Test Company",
+ to_date="2021-12-10"
+ )
+
+ def test_normal_inward_outward_queue(self):
+ "Reference: Case 1 in stock_ageing_fifo_logic.md"
+ sle = [
+ frappe._dict(
+ name="Flask Item",
+ actual_qty=30, qty_after_transaction=30,
+ posting_date="2021-12-01", voucher_type="Stock Entry",
+ voucher_no="001",
+ has_serial_no=False, serial_no=None
+ ),
+ frappe._dict(
+ name="Flask Item",
+ actual_qty=20, qty_after_transaction=50,
+ posting_date="2021-12-02", voucher_type="Stock Entry",
+ voucher_no="002",
+ has_serial_no=False, serial_no=None
+ ),
+ frappe._dict(
+ name="Flask Item",
+ actual_qty=(-10), qty_after_transaction=40,
+ posting_date="2021-12-03", voucher_type="Stock Entry",
+ voucher_no="003",
+ has_serial_no=False, serial_no=None
+ )
+ ]
+
+ slots = FIFOSlots(self.filters, sle).generate()
+
+ self.assertTrue(slots["Flask Item"]["fifo_queue"])
+ result = slots["Flask Item"]
+ queue = result["fifo_queue"]
+
+ self.assertEqual(result["qty_after_transaction"], result["total_qty"])
+ self.assertEqual(queue[0][0], 20.0)
+
+ def test_insufficient_balance(self):
+ "Reference: Case 3 in stock_ageing_fifo_logic.md"
+ sle = [
+ frappe._dict(
+ name="Flask Item",
+ actual_qty=(-30), qty_after_transaction=(-30),
+ posting_date="2021-12-01", voucher_type="Stock Entry",
+ voucher_no="001",
+ has_serial_no=False, serial_no=None
+ ),
+ frappe._dict(
+ name="Flask Item",
+ actual_qty=20, qty_after_transaction=(-10),
+ posting_date="2021-12-02", voucher_type="Stock Entry",
+ voucher_no="002",
+ has_serial_no=False, serial_no=None
+ ),
+ frappe._dict(
+ name="Flask Item",
+ actual_qty=20, qty_after_transaction=10,
+ posting_date="2021-12-03", voucher_type="Stock Entry",
+ voucher_no="003",
+ has_serial_no=False, serial_no=None
+ ),
+ frappe._dict(
+ name="Flask Item",
+ actual_qty=10, qty_after_transaction=20,
+ posting_date="2021-12-03", voucher_type="Stock Entry",
+ voucher_no="004",
+ has_serial_no=False, serial_no=None
+ )
+ ]
+
+ slots = FIFOSlots(self.filters, sle).generate()
+
+ result = slots["Flask Item"]
+ queue = result["fifo_queue"]
+
+ self.assertEqual(result["qty_after_transaction"], result["total_qty"])
+ self.assertEqual(queue[0][0], 10.0)
+ self.assertEqual(queue[1][0], 10.0)
+
+ def test_stock_reconciliation(self):
+ sle = [
+ frappe._dict(
+ name="Flask Item",
+ actual_qty=30, qty_after_transaction=30,
+ posting_date="2021-12-01", voucher_type="Stock Entry",
+ voucher_no="001",
+ has_serial_no=False, serial_no=None
+ ),
+ frappe._dict(
+ name="Flask Item",
+ actual_qty=0, qty_after_transaction=50,
+ posting_date="2021-12-02", voucher_type="Stock Reconciliation",
+ voucher_no="002",
+ has_serial_no=False, serial_no=None
+ ),
+ frappe._dict(
+ name="Flask Item",
+ actual_qty=(-10), qty_after_transaction=40,
+ posting_date="2021-12-03", voucher_type="Stock Entry",
+ voucher_no="003",
+ has_serial_no=False, serial_no=None
+ )
+ ]
+
+ slots = FIFOSlots(self.filters, sle).generate()
+
+ result = slots["Flask Item"]
+ queue = result["fifo_queue"]
+
+ self.assertEqual(result["qty_after_transaction"], result["total_qty"])
+ self.assertEqual(queue[0][0], 20.0)
+ self.assertEqual(queue[1][0], 20.0)
diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py
index c0b89fd..b4f43a7 100644
--- a/erpnext/stock/report/stock_balance/stock_balance.py
+++ b/erpnext/stock/report/stock_balance/stock_balance.py
@@ -9,7 +9,7 @@
from frappe.utils import cint, date_diff, flt, getdate
import erpnext
-from erpnext.stock.report.stock_ageing.stock_ageing import get_average_age, get_fifo_queue
+from erpnext.stock.report.stock_ageing.stock_ageing import FIFOSlots, get_average_age
from erpnext.stock.report.stock_ledger.stock_ledger import get_item_group_condition
from erpnext.stock.utils import add_additional_uom_columns, is_reposting_item_valuation_in_progress
@@ -33,7 +33,7 @@
if filters.get('show_stock_ageing_data'):
filters['show_warehouse_wise_stock'] = True
- item_wise_fifo_queue = get_fifo_queue(filters, sle)
+ item_wise_fifo_queue = FIFOSlots(filters, sle).generate()
# if no stock ledger entry found return
if not sle:
@@ -167,7 +167,7 @@
sle.company, sle.voucher_type, sle.qty_after_transaction, sle.stock_value_difference,
sle.item_code as name, sle.voucher_no, sle.stock_value, sle.batch_no
from
- `tabStock Ledger Entry` sle force index (posting_sort_index)
+ `tabStock Ledger Entry` sle
where sle.docstatus < 2 %s %s
and is_cancelled = 0
order by sle.posting_date, sle.posting_time, sle.creation, sle.actual_qty""" % #nosec
diff --git a/erpnext/agriculture/doctype/agriculture_task/__init__.py b/erpnext/stock/report/stock_ledger_invariant_check/__init__.py
similarity index 100%
rename from erpnext/agriculture/doctype/agriculture_task/__init__.py
rename to erpnext/stock/report/stock_ledger_invariant_check/__init__.py
diff --git a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.js b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.js
new file mode 100644
index 0000000..31f389f
--- /dev/null
+++ b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.js
@@ -0,0 +1,44 @@
+// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+/* eslint-disable */
+
+const DIFFERNCE_FIELD_NAMES = [
+ "difference_in_qty",
+ "fifo_qty_diff",
+ "fifo_value_diff",
+ "fifo_valuation_diff",
+ "valuation_diff",
+ "fifo_difference_diff",
+ "diff_value_diff"
+];
+
+frappe.query_reports["Stock Ledger Invariant Check"] = {
+ "filters": [
+ {
+ "fieldname": "item_code",
+ "fieldtype": "Link",
+ "label": "Item",
+ "mandatory": 1,
+ "options": "Item",
+ get_query: function() {
+ return {
+ filters: {is_stock_item: 1, has_serial_no: 0}
+ }
+ }
+ },
+ {
+ "fieldname": "warehouse",
+ "fieldtype": "Link",
+ "label": "Warehouse",
+ "mandatory": 1,
+ "options": "Warehouse",
+ }
+ ],
+ formatter (value, row, column, data, default_formatter) {
+ value = default_formatter(value, row, column, data);
+ if (DIFFERNCE_FIELD_NAMES.includes(column.fieldname) && Math.abs(data[column.fieldname]) > 0.001) {
+ value = "<span style='color:red'>" + value + "</span>";
+ }
+ return value;
+ },
+};
diff --git a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.json b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.json
new file mode 100644
index 0000000..d28fe0f
--- /dev/null
+++ b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.json
@@ -0,0 +1,26 @@
+{
+ "add_total_row": 0,
+ "columns": [],
+ "creation": "2021-12-16 06:31:23.290916",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "modified": "2021-12-16 09:55:58.341764",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Stock Ledger Invariant Check",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "Stock Ledger Entry",
+ "report_name": "Stock Ledger Invariant Check",
+ "report_type": "Script Report",
+ "roles": [
+ {
+ "role": "System Manager"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py
new file mode 100644
index 0000000..48753b0
--- /dev/null
+++ b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py
@@ -0,0 +1,249 @@
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# License: GNU GPL v3. See LICENSE
+
+import json
+
+import frappe
+
+SLE_FIELDS = (
+ "name",
+ "posting_date",
+ "posting_time",
+ "creation",
+ "voucher_type",
+ "voucher_no",
+ "actual_qty",
+ "qty_after_transaction",
+ "incoming_rate",
+ "outgoing_rate",
+ "stock_queue",
+ "batch_no",
+ "stock_value",
+ "stock_value_difference",
+ "valuation_rate",
+)
+
+
+def execute(filters=None):
+ columns = get_columns()
+ data = get_data(filters)
+ return columns, data
+
+
+def get_data(filters):
+ sles = get_stock_ledger_entries(filters)
+ return add_invariant_check_fields(sles)
+
+
+def get_stock_ledger_entries(filters):
+ return frappe.get_all(
+ "Stock Ledger Entry",
+ fields=SLE_FIELDS,
+ filters={
+ "item_code": filters.item_code,
+ "warehouse": filters.warehouse,
+ "is_cancelled": 0
+ },
+ order_by="timestamp(posting_date, posting_time), creation",
+ )
+
+
+def add_invariant_check_fields(sles):
+ balance_qty = 0.0
+ balance_stock_value = 0.0
+ for idx, sle in enumerate(sles):
+ queue = json.loads(sle.stock_queue)
+
+ fifo_qty = 0.0
+ fifo_value = 0.0
+ for qty, rate in queue:
+ fifo_qty += qty
+ fifo_value += qty * rate
+
+ balance_qty += sle.actual_qty
+ balance_stock_value += sle.stock_value_difference
+ if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no:
+ balance_qty = sle.qty_after_transaction
+
+ sle.fifo_queue_qty = fifo_qty
+ sle.fifo_stock_value = fifo_value
+ sle.fifo_valuation_rate = fifo_value / fifo_qty if fifo_qty else None
+ sle.balance_value_by_qty = (
+ sle.stock_value / sle.qty_after_transaction if sle.qty_after_transaction else None
+ )
+ sle.expected_qty_after_transaction = balance_qty
+ sle.stock_value_from_diff = balance_stock_value
+
+ # set difference fields
+ sle.difference_in_qty = sle.qty_after_transaction - sle.expected_qty_after_transaction
+ sle.fifo_qty_diff = sle.qty_after_transaction - fifo_qty
+ sle.fifo_value_diff = sle.stock_value - fifo_value
+ sle.fifo_valuation_diff = (
+ sle.valuation_rate - sle.fifo_valuation_rate if sle.fifo_valuation_rate else None
+ )
+ sle.valuation_diff = (
+ sle.valuation_rate - sle.balance_value_by_qty if sle.balance_value_by_qty else None
+ )
+ sle.diff_value_diff = sle.stock_value_from_diff - sle.stock_value
+
+ if idx > 0:
+ sle.fifo_stock_diff = sle.fifo_stock_value - sles[idx - 1].fifo_stock_value
+ sle.fifo_difference_diff = sle.fifo_stock_diff - sle.stock_value_difference
+
+ return sles
+
+
+def get_columns():
+ return [
+ {
+ "fieldname": "name",
+ "fieldtype": "Link",
+ "label": "Stock Ledger Entry",
+ "options": "Stock Ledger Entry",
+ },
+ {
+ "fieldname": "posting_date",
+ "fieldtype": "Date",
+ "label": "Posting Date",
+ },
+ {
+ "fieldname": "posting_time",
+ "fieldtype": "Time",
+ "label": "Posting Time",
+ },
+ {
+ "fieldname": "creation",
+ "fieldtype": "Datetime",
+ "label": "Creation",
+ },
+ {
+ "fieldname": "voucher_type",
+ "fieldtype": "Link",
+ "label": "Voucher Type",
+ "options": "DocType",
+ },
+ {
+ "fieldname": "voucher_no",
+ "fieldtype": "Dynamic Link",
+ "label": "Voucher No",
+ "options": "voucher_type",
+ },
+ {
+ "fieldname": "batch_no",
+ "fieldtype": "Link",
+ "label": "Batch",
+ "options": "Batch",
+ },
+ {
+ "fieldname": "actual_qty",
+ "fieldtype": "Float",
+ "label": "Qty Change",
+ },
+ {
+ "fieldname": "incoming_rate",
+ "fieldtype": "Float",
+ "label": "Incoming Rate",
+ },
+ {
+ "fieldname": "outgoing_rate",
+ "fieldtype": "Float",
+ "label": "Outgoing Rate",
+ },
+ {
+ "fieldname": "qty_after_transaction",
+ "fieldtype": "Float",
+ "label": "(A) Qty After Transaction",
+ },
+ {
+ "fieldname": "expected_qty_after_transaction",
+ "fieldtype": "Float",
+ "label": "(B) Expected Qty After Transaction",
+ },
+ {
+ "fieldname": "difference_in_qty",
+ "fieldtype": "Float",
+ "label": "A - B",
+ },
+ {
+ "fieldname": "stock_queue",
+ "fieldtype": "Data",
+ "label": "FIFO Queue",
+ },
+
+ {
+ "fieldname": "fifo_queue_qty",
+ "fieldtype": "Float",
+ "label": "(C) Total qty in queue",
+ },
+ {
+ "fieldname": "fifo_qty_diff",
+ "fieldtype": "Float",
+ "label": "A - C",
+ },
+ {
+ "fieldname": "stock_value",
+ "fieldtype": "Float",
+ "label": "(D) Balance Stock Value",
+ },
+ {
+ "fieldname": "fifo_stock_value",
+ "fieldtype": "Float",
+ "label": "(E) Balance Stock Value in Queue",
+ },
+ {
+ "fieldname": "fifo_value_diff",
+ "fieldtype": "Float",
+ "label": "D - E",
+ },
+ {
+ "fieldname": "stock_value_difference",
+ "fieldtype": "Float",
+ "label": "(F) Stock Value Difference",
+ },
+ {
+ "fieldname": "stock_value_from_diff",
+ "fieldtype": "Float",
+ "label": "Balance Stock Value using (F)",
+ },
+ {
+ "fieldname": "diff_value_diff",
+ "fieldtype": "Float",
+ "label": "K - D",
+ },
+ {
+ "fieldname": "fifo_stock_diff",
+ "fieldtype": "Float",
+ "label": "(G) Stock Value difference (FIFO queue)",
+ },
+ {
+ "fieldname": "fifo_difference_diff",
+ "fieldtype": "Float",
+ "label": "F - G",
+ },
+ {
+ "fieldname": "valuation_rate",
+ "fieldtype": "Float",
+ "label": "(H) Valuation Rate",
+ },
+ {
+ "fieldname": "fifo_valuation_rate",
+ "fieldtype": "Float",
+ "label": "(I) Valuation Rate as per FIFO",
+ },
+
+ {
+ "fieldname": "fifo_valuation_diff",
+ "fieldtype": "Float",
+ "label": "H - I",
+ },
+ {
+ "fieldname": "balance_value_by_qty",
+ "fieldtype": "Float",
+ "label": "(J) Valuation = Value (D) ÷ Qty (A)",
+ },
+ {
+ "fieldname": "valuation_diff",
+ "fieldtype": "Float",
+ "label": "H - J",
+ },
+ ]
diff --git a/erpnext/stock/report/test_reports.py b/erpnext/stock/report/test_reports.py
index d7fb5b2..1dcf863 100644
--- a/erpnext/stock/report/test_reports.py
+++ b/erpnext/stock/report/test_reports.py
@@ -41,6 +41,12 @@
("Total Stock Summary", {"group_by": "warehouse",}),
("Batch Item Expiry Status", {}),
("Stock Ageing", {"range1": 30, "range2": 60, "range3": 90, "_optional": True}),
+ ("Stock Ledger Invariant Check",
+ {
+ "warehouse": "_Test Warehouse - _TC",
+ "item": "_Test Item"
+ }
+ ),
]
OPTIONAL_FILTERS = {
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 90648f1..88054aa 100644
--- a/erpnext/stock/report/total_stock_summary/total_stock_summary.js
+++ b/erpnext/stock/report/total_stock_summary/total_stock_summary.js
@@ -10,23 +10,8 @@
"fieldtype": "Select",
"width": "80",
"reqd": 1,
- "options": ["", "Warehouse", "Company"],
- "change": function() {
- let group_by = frappe.query_report.get_filter_value("group_by")
- let company_filter = frappe.query_report.get_filter("company")
- if (group_by == "Company") {
- company_filter.df.reqd = 0;
- company_filter.df.hidden = 1;
- frappe.query_report.set_filter_value("company", "");
- company_filter.refresh();
- }
- else {
- company_filter.df.reqd = 1;
- company_filter.df.hidden = 0;
- company_filter.refresh();
- frappe.query_report.refresh();
- }
- }
+ "options": ["Warehouse", "Company"],
+ "default": "Warehouse",
},
{
"fieldname": "company",
@@ -34,8 +19,9 @@
"fieldtype": "Link",
"width": "80",
"options": "Company",
+ "reqd": 1,
"default": frappe.defaults.get_user_default("Company"),
- "reqd": 1
+ "depends_on": "eval: doc.group_by != 'Company'",
},
]
}
diff --git a/erpnext/stock/report/total_stock_summary/total_stock_summary.py b/erpnext/stock/report/total_stock_summary/total_stock_summary.py
index 7e47b50..6f27558 100644
--- a/erpnext/stock/report/total_stock_summary/total_stock_summary.py
+++ b/erpnext/stock/report/total_stock_summary/total_stock_summary.py
@@ -7,8 +7,9 @@
def execute(filters=None):
- if not filters: filters = {}
- validate_filters(filters)
+
+ if not filters:
+ filters = {}
columns = get_columns()
stock = get_total_stock(filters)
@@ -53,9 +54,3 @@
ON warehouse.name = ledger.warehouse
WHERE
ledger.actual_qty != 0 %s""" % (columns, conditions))
-
-def validate_filters(filters):
- if filters.get("group_by") == 'Company' and \
- filters.get("company"):
-
- frappe.throw(_("Please set Company filter blank if Group By is 'Company'"))
diff --git a/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py b/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py
index d3af5f6..22bdb89 100644
--- a/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py
+++ b/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py
@@ -9,7 +9,7 @@
from frappe import _
from frappe.utils import flt
-from erpnext.stock.report.stock_ageing.stock_ageing import get_average_age, get_fifo_queue
+from erpnext.stock.report.stock_ageing.stock_ageing import FIFOSlots, get_average_age
from erpnext.stock.report.stock_balance.stock_balance import (
get_item_details,
get_item_warehouse_map,
@@ -33,7 +33,7 @@
item_map = get_item_details(items, sle, filters)
iwb_map = get_item_warehouse_map(filters, sle)
warehouse_list = get_warehouse_list(filters)
- item_ageing = get_fifo_queue(filters)
+ item_ageing = FIFOSlots(filters).generate()
data = []
item_balance = {}
item_value = {}
@@ -46,8 +46,8 @@
item_balance.setdefault((item, item_map[item]["item_group"]), [])
total_stock_value = 0.00
for wh in warehouse_list:
- row += [qty_dict.bal_qty] if wh.name in warehouse else [0.00]
- total_stock_value += qty_dict.bal_val if wh.name in warehouse else 0.00
+ row += [qty_dict.bal_qty] if wh.name == warehouse else [0.00]
+ total_stock_value += qty_dict.bal_val if wh.name == warehouse else 0.00
item_balance[(item, item_map[item]["item_group"])].append(row)
item_value.setdefault((item, item_map[item]["item_group"]),[])
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index 9c4c676..107bb23 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -7,29 +7,27 @@
import frappe
from frappe import _
from frappe.model.meta import get_field_precision
-from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, now
+from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, now, nowdate
import erpnext
+from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty
from erpnext.stock.utils import (
get_incoming_outgoing_rate_for_cancel,
get_or_make_bin,
get_valuation_method,
)
+from erpnext.stock.valuation import FIFOValuation
-# future reposting
class NegativeStockError(frappe.ValidationError): pass
class SerialNoExistsInFutureTransaction(frappe.ValidationError):
pass
_exceptions = frappe.local('stockledger_exceptions')
-# _exceptions = []
def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False):
from erpnext.controllers.stock_controller import future_sle_exists
if sl_entries:
- from erpnext.stock.utils import update_bin
-
cancel = sl_entries[0].get("is_cancelled")
if cancel:
validate_cancellation(sl_entries)
@@ -64,7 +62,38 @@
# preserve previous_qty_after_transaction for qty reposting
args.previous_qty_after_transaction = sle.get("previous_qty_after_transaction")
- update_bin(args, allow_negative_stock, via_landed_cost_voucher)
+ is_stock_item = frappe.get_cached_value('Item', args.get("item_code"), 'is_stock_item')
+ if is_stock_item:
+ bin_name = get_or_make_bin(args.get("item_code"), args.get("warehouse"))
+ repost_current_voucher(args, allow_negative_stock, via_landed_cost_voucher)
+ update_bin_qty(bin_name, args)
+ else:
+ frappe.msgprint(_("Item {0} ignored since it is not a stock item").format(args.get("item_code")))
+
+def repost_current_voucher(args, allow_negative_stock=False, via_landed_cost_voucher=False):
+ if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation":
+ if not args.get("posting_date"):
+ args["posting_date"] = nowdate()
+
+ if args.get("is_cancelled") and via_landed_cost_voucher:
+ return
+
+ # Reposts only current voucher SL Entries
+ # Updates valuation rate, stock value, stock queue for current transaction
+ update_entries_after({
+ "item_code": args.get('item_code'),
+ "warehouse": args.get('warehouse'),
+ "posting_date": args.get("posting_date"),
+ "posting_time": args.get("posting_time"),
+ "voucher_type": args.get("voucher_type"),
+ "voucher_no": args.get("voucher_no"),
+ "sle_id": args.get('name'),
+ "creation": args.get('creation')
+ }, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher)
+
+ # update qty in future sle and Validate negative qty
+ update_qty_in_future_sle(args, allow_negative_stock)
+
def get_args_for_future_sle(row):
return frappe._dict({
@@ -111,6 +140,7 @@
frappe.throw(_("Cannot cancel the transaction. Reposting of item valuation on submission is not completed yet."))
if repost_entry.status == 'Queued':
doc = frappe.get_doc("Repost Item Valuation", repost_entry.name)
+ doc.flags.ignore_permissions = True
doc.cancel()
doc.delete()
@@ -427,9 +457,8 @@
self.wh_data.qty_after_transaction += flt(sle.actual_qty)
self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(self.wh_data.valuation_rate)
else:
- self.get_fifo_values(sle)
+ self.update_fifo_values(sle)
self.wh_data.qty_after_transaction += flt(sle.actual_qty)
- self.wh_data.stock_value = sum(flt(batch[0]) * flt(batch[1]) for batch in self.wh_data.stock_queue)
# rounding as per precision
self.wh_data.stock_value = flt(self.wh_data.stock_value, self.precision)
@@ -667,87 +696,39 @@
sle.voucher_type, sle.voucher_no, self.allow_zero_rate,
currency=erpnext.get_company_currency(sle.company), company=sle.company)
- def get_fifo_values(self, sle):
+ def update_fifo_values(self, sle):
incoming_rate = flt(sle.incoming_rate)
actual_qty = flt(sle.actual_qty)
outgoing_rate = flt(sle.outgoing_rate)
+ fifo_queue = FIFOValuation(self.wh_data.stock_queue)
if actual_qty > 0:
- if not self.wh_data.stock_queue:
- self.wh_data.stock_queue.append([0, 0])
-
- # last row has the same rate, just updated the qty
- if self.wh_data.stock_queue[-1][1]==incoming_rate:
- self.wh_data.stock_queue[-1][0] += actual_qty
- else:
- # Item has a positive balance qty, add new entry
- if self.wh_data.stock_queue[-1][0] > 0:
- self.wh_data.stock_queue.append([actual_qty, incoming_rate])
- else: # negative balance qty
- qty = self.wh_data.stock_queue[-1][0] + actual_qty
- if qty > 0: # new balance qty is positive
- self.wh_data.stock_queue[-1] = [qty, incoming_rate]
- else: # new balance qty is still negative, maintain same rate
- self.wh_data.stock_queue[-1][0] = qty
+ fifo_queue.add_stock(qty=actual_qty, rate=incoming_rate)
else:
- qty_to_pop = abs(actual_qty)
- while qty_to_pop:
- if not self.wh_data.stock_queue:
- # Get valuation rate from last sle if exists or from valuation rate field in item master
- allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(sle.voucher_type, sle.voucher_detail_no)
- if not allow_zero_valuation_rate:
- _rate = get_valuation_rate(sle.item_code, sle.warehouse,
- sle.voucher_type, sle.voucher_no, self.allow_zero_rate,
- currency=erpnext.get_company_currency(sle.company), company=sle.company)
- else:
- _rate = 0
-
- self.wh_data.stock_queue.append([0, _rate])
-
- index = None
- if outgoing_rate > 0:
- # Find the entry where rate matched with outgoing rate
- for i, v in enumerate(self.wh_data.stock_queue):
- if v[1] == outgoing_rate:
- index = i
- break
-
- # If no entry found with outgoing rate, collapse stack
- if index is None: # nosemgrep
- new_stock_value = sum(d[0]*d[1] for d in self.wh_data.stock_queue) - qty_to_pop*outgoing_rate
- new_stock_qty = sum(d[0] for d in self.wh_data.stock_queue) - qty_to_pop
- self.wh_data.stock_queue = [[new_stock_qty, new_stock_value/new_stock_qty if new_stock_qty > 0 else outgoing_rate]]
- break
+ def rate_generator() -> float:
+ allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(sle.voucher_type, sle.voucher_detail_no)
+ if not allow_zero_valuation_rate:
+ return get_valuation_rate(sle.item_code, sle.warehouse,
+ sle.voucher_type, sle.voucher_no, self.allow_zero_rate,
+ currency=erpnext.get_company_currency(sle.company), company=sle.company)
else:
- index = 0
+ return 0.0
- # select first batch or the batch with same rate
- batch = self.wh_data.stock_queue[index]
- if qty_to_pop >= batch[0]:
- # consume current batch
- qty_to_pop = _round_off_if_near_zero(qty_to_pop - batch[0])
- self.wh_data.stock_queue.pop(index)
- if not self.wh_data.stock_queue and qty_to_pop:
- # stock finished, qty still remains to be withdrawn
- # negative stock, keep in as a negative batch
- self.wh_data.stock_queue.append([-qty_to_pop, outgoing_rate or batch[1]])
- break
+ fifo_queue.remove_stock(qty=abs(actual_qty), outgoing_rate=outgoing_rate, rate_generator=rate_generator)
- else:
- # qty found in current batch
- # consume it and exit
- batch[0] = batch[0] - qty_to_pop
- qty_to_pop = 0
+ stock_qty, stock_value = fifo_queue.get_total_stock_and_value()
- stock_value = _round_off_if_near_zero(sum(flt(batch[0]) * flt(batch[1]) for batch in self.wh_data.stock_queue))
- stock_qty = _round_off_if_near_zero(sum(flt(batch[0]) for batch in self.wh_data.stock_queue))
-
+ self.wh_data.stock_queue = fifo_queue.get_state()
+ self.wh_data.stock_value = stock_value
if stock_qty:
- self.wh_data.valuation_rate = stock_value / flt(stock_qty)
+ self.wh_data.valuation_rate = stock_value / stock_qty
+
if not self.wh_data.stock_queue:
self.wh_data.stock_queue.append([0, sle.incoming_rate or sle.outgoing_rate or self.wh_data.valuation_rate])
+
+
def check_if_allow_zero_valuation_rate(self, voucher_type, voucher_detail_no):
ref_item_dt = ""
@@ -802,9 +783,9 @@
def update_bin(self):
# update bin for each warehouse
for warehouse, data in self.data.items():
- bin_record = get_or_make_bin(self.item_code, warehouse)
+ bin_name = get_or_make_bin(self.item_code, warehouse)
- frappe.db.set_value('Bin', bin_record, {
+ frappe.db.set_value('Bin', bin_name, {
"valuation_rate": data.valuation_rate,
"actual_qty": data.qty_after_transaction,
"stock_value": data.stock_value
@@ -1060,17 +1041,36 @@
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:
- sle = get_future_sle_with_negative_qty(args)
- if sle:
- message = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format(
- abs(sle[0]["qty_after_transaction"]),
- frappe.get_desk_link('Item', args.item_code),
- frappe.get_desk_link('Warehouse', args.warehouse),
- sle[0]["posting_date"], sle[0]["posting_time"],
- frappe.get_desk_link(sle[0]["voucher_type"], sle[0]["voucher_no"]))
+ if allow_negative_stock:
+ return
+ if not (args.actual_qty < 0 or args.voucher_type == "Stock Reconciliation"):
+ return
- frappe.throw(message, NegativeStockError, title='Insufficient Stock')
+ neg_sle = get_future_sle_with_negative_qty(args)
+ if neg_sle:
+ message = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format(
+ abs(neg_sle[0]["qty_after_transaction"]),
+ frappe.get_desk_link('Item', args.item_code),
+ frappe.get_desk_link('Warehouse', args.warehouse),
+ neg_sle[0]["posting_date"], neg_sle[0]["posting_time"],
+ frappe.get_desk_link(neg_sle[0]["voucher_type"], neg_sle[0]["voucher_no"]))
+
+ frappe.throw(message, NegativeStockError, title='Insufficient Stock')
+
+
+ if not args.batch_no:
+ return
+
+ neg_batch_sle = get_future_sle_with_negative_batch_qty(args)
+ if neg_batch_sle:
+ message = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format(
+ abs(neg_batch_sle[0]["cumulative_total"]),
+ frappe.get_desk_link('Batch', args.batch_no),
+ frappe.get_desk_link('Warehouse', args.warehouse),
+ neg_batch_sle[0]["posting_date"], neg_batch_sle[0]["posting_time"],
+ frappe.get_desk_link(neg_batch_sle[0]["voucher_type"], neg_batch_sle[0]["voucher_no"]))
+ frappe.throw(message, NegativeStockError, title="Insufficient Stock for Batch")
+
def get_future_sle_with_negative_qty(args):
return frappe.db.sql("""
@@ -1089,11 +1089,24 @@
limit 1
""", args, as_dict=1)
-def _round_off_if_near_zero(number: float, precision: int = 6) -> float:
- """ Rounds off the number to zero only if number is close to zero for decimal
- specified in precision. Precision defaults to 6.
- """
- if flt(number) < (1.0 / (10**precision)):
- return 0
- return flt(number)
+def get_future_sle_with_negative_batch_qty(args):
+ return frappe.db.sql("""
+ with batch_ledger as (
+ select
+ posting_date, posting_time, voucher_type, voucher_no,
+ sum(actual_qty) over (order by posting_date, posting_time, creation) as cumulative_total
+ from `tabStock Ledger Entry`
+ where
+ item_code = %(item_code)s
+ and warehouse = %(warehouse)s
+ and batch_no=%(batch_no)s
+ and is_cancelled = 0
+ order by posting_date, posting_time, creation
+ )
+ select * from batch_ledger
+ where
+ cumulative_total < 0.0
+ and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
+ limit 1
+ """, args, as_dict=1)
diff --git a/erpnext/stock/tests/test_valuation.py b/erpnext/stock/tests/test_valuation.py
new file mode 100644
index 0000000..85788ba
--- /dev/null
+++ b/erpnext/stock/tests/test_valuation.py
@@ -0,0 +1,166 @@
+import unittest
+
+from hypothesis import given
+from hypothesis import strategies as st
+
+from erpnext.stock.valuation import FIFOValuation, _round_off_if_near_zero
+
+qty_gen = st.floats(min_value=-1e6, max_value=1e6)
+value_gen = st.floats(min_value=1, max_value=1e6)
+stock_queue_generator = st.lists(st.tuples(qty_gen, value_gen), min_size=10)
+
+
+class TestFifoValuation(unittest.TestCase):
+
+ def setUp(self):
+ self.queue = FIFOValuation([])
+
+ def tearDown(self):
+ qty, value = self.queue.get_total_stock_and_value()
+ self.assertTotalQty(qty)
+ self.assertTotalValue(value)
+
+ def assertTotalQty(self, qty):
+ self.assertAlmostEqual(sum(q for q, _ in self.queue), qty, msg=f"queue: {self.queue}", places=4)
+
+ def assertTotalValue(self, value):
+ self.assertAlmostEqual(sum(q * r for q, r in self.queue), value, msg=f"queue: {self.queue}", places=2)
+
+ def test_simple_addition(self):
+ self.queue.add_stock(1, 10)
+ self.assertTotalQty(1)
+
+ def test_simple_removal(self):
+ self.queue.add_stock(1, 10)
+ self.queue.remove_stock(1)
+ self.assertTotalQty(0)
+
+ def test_merge_new_stock(self):
+ self.queue.add_stock(1, 10)
+ self.queue.add_stock(1, 10)
+ self.assertEqual(self.queue, [[2, 10]])
+
+ def test_adding_negative_stock_keeps_rate(self):
+ self.queue = FIFOValuation([[-5.0, 100]])
+ self.queue.add_stock(1, 10)
+ self.assertEqual(self.queue, [[-4, 100]])
+
+ def test_adding_negative_stock_updates_rate(self):
+ self.queue = FIFOValuation([[-5.0, 100]])
+ self.queue.add_stock(6, 10)
+ self.assertEqual(self.queue, [[1, 10]])
+
+
+ def test_negative_stock(self):
+ self.queue.remove_stock(1, 5)
+ self.assertEqual(self.queue, [[-1, 5]])
+
+ # XXX
+ self.queue.remove_stock(1, 10)
+ self.assertTotalQty(-2)
+
+ self.queue.add_stock(2, 10)
+ self.assertTotalQty(0)
+ self.assertTotalValue(0)
+
+ def test_removing_specified_rate(self):
+ self.queue.add_stock(1, 10)
+ self.queue.add_stock(1, 20)
+
+ self.queue.remove_stock(1, 20)
+ self.assertEqual(self.queue, [[1, 10]])
+
+
+ def test_remove_multiple_bins(self):
+ self.queue.add_stock(1, 10)
+ self.queue.add_stock(2, 20)
+ self.queue.add_stock(1, 20)
+ self.queue.add_stock(5, 20)
+
+ self.queue.remove_stock(4)
+ self.assertEqual(self.queue, [[5, 20]])
+
+
+ def test_remove_multiple_bins_with_rate(self):
+ self.queue.add_stock(1, 10)
+ self.queue.add_stock(2, 20)
+ self.queue.add_stock(1, 20)
+ self.queue.add_stock(5, 20)
+
+ self.queue.remove_stock(3, 20)
+ self.assertEqual(self.queue, [[1, 10], [5, 20]])
+
+ def test_collapsing_of_queue(self):
+ self.queue.add_stock(1, 1)
+ self.queue.add_stock(1, 2)
+ self.queue.add_stock(1, 3)
+ self.queue.add_stock(1, 4)
+
+ self.assertTotalValue(10)
+
+ self.queue.remove_stock(3, 1)
+ # XXX
+ self.assertEqual(self.queue, [[1, 7]])
+
+ def test_rounding_off(self):
+ self.queue.add_stock(1.0, 1.0)
+ self.queue.remove_stock(1.0 - 1e-9)
+ self.assertTotalQty(0)
+
+ def test_rounding_off_near_zero(self):
+ self.assertEqual(_round_off_if_near_zero(0), 0)
+ self.assertEqual(_round_off_if_near_zero(1), 1)
+ self.assertEqual(_round_off_if_near_zero(-1), -1)
+ self.assertEqual(_round_off_if_near_zero(-1e-8), 0)
+ self.assertEqual(_round_off_if_near_zero(1e-8), 0)
+
+ def test_totals(self):
+ self.queue.add_stock(1, 10)
+ self.queue.add_stock(2, 13)
+ self.queue.add_stock(1, 17)
+ self.queue.remove_stock(1)
+ self.queue.remove_stock(1)
+ self.queue.remove_stock(1)
+ self.queue.add_stock(5, 17)
+ self.queue.add_stock(8, 11)
+
+ @given(stock_queue_generator)
+ def test_fifo_qty_hypothesis(self, stock_queue):
+ self.queue = FIFOValuation([])
+ total_qty = 0
+
+ for qty, rate in stock_queue:
+ if qty == 0:
+ continue
+ if qty > 0:
+ self.queue.add_stock(qty, rate)
+ total_qty += qty
+ else:
+ qty = abs(qty)
+ consumed = self.queue.remove_stock(qty)
+ self.assertAlmostEqual(qty, sum(q for q, _ in consumed), msg=f"incorrect consumption {consumed}")
+ total_qty -= qty
+ self.assertTotalQty(total_qty)
+
+ @given(stock_queue_generator)
+ def test_fifo_qty_value_nonneg_hypothesis(self, stock_queue):
+ self.queue = FIFOValuation([])
+ total_qty = 0.0
+ total_value = 0.0
+
+ for qty, rate in stock_queue:
+ # don't allow negative stock
+ if qty == 0 or total_qty + qty < 0 or abs(qty) < 0.1:
+ continue
+ if qty > 0:
+ self.queue.add_stock(qty, rate)
+ total_qty += qty
+ total_value += qty * rate
+ else:
+ qty = abs(qty)
+ consumed = self.queue.remove_stock(qty)
+ self.assertAlmostEqual(qty, sum(q for q, _ in consumed), msg=f"incorrect consumption {consumed}")
+ total_qty -= qty
+ total_value -= sum(q * r for q, r in consumed)
+ self.assertTotalQty(total_qty)
+ self.assertTotalValue(total_value)
diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py
index 8031c58..3c70b41 100644
--- a/erpnext/stock/utils.py
+++ b/erpnext/stock/utils.py
@@ -12,6 +12,7 @@
class InvalidWarehouseCompany(frappe.ValidationError): pass
+class PendingRepostingError(frappe.ValidationError): pass
def get_stock_value_from_bin(warehouse=None, item_code=None):
values = {}
@@ -85,8 +86,8 @@
from erpnext.stock.stock_ledger import get_previous_sle
- if not posting_date: posting_date = nowdate()
- if not posting_time: posting_time = nowtime()
+ if posting_date is None: posting_date = nowdate()
+ if posting_time is None: posting_time = nowtime()
args = {
"item_code": item_code,
@@ -187,7 +188,7 @@
bin_obj.flags.ignore_permissions = True
return bin_obj
-def get_or_make_bin(item_code, warehouse) -> str:
+def get_or_make_bin(item_code: str , warehouse: str) -> str:
bin_record = frappe.db.get_value('Bin', {'item_code': item_code, 'warehouse': warehouse})
if not bin_record:
@@ -203,11 +204,12 @@
return bin_record
def update_bin(args, allow_negative_stock=False, via_landed_cost_voucher=False):
+ """WARNING: This function is deprecated. Inline this function instead of using it."""
from erpnext.stock.doctype.bin.bin import update_stock
is_stock_item = frappe.get_cached_value('Item', args.get("item_code"), 'is_stock_item')
if is_stock_item:
- bin_record = get_or_make_bin(args.get("item_code"), args.get("warehouse"))
- update_stock(bin_record, args, allow_negative_stock, via_landed_cost_voucher)
+ bin_name = get_or_make_bin(args.get("item_code"), args.get("warehouse"))
+ update_stock(bin_name, args, allow_negative_stock, via_landed_cost_voucher)
else:
frappe.msgprint(_("Item {0} ignored since it is not a stock item").format(args.get("item_code")))
@@ -416,3 +418,28 @@
{'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)
+
+def check_pending_reposting(posting_date: str, throw_error: bool = True) -> bool:
+ """Check if there are pending reposting job till the specified posting date."""
+
+ filters = {
+ "docstatus": 1,
+ "status": ["in", ["Queued","In Progress", "Failed"]],
+ "posting_date": ["<=", posting_date],
+ }
+
+ reposting_pending = frappe.db.exists("Repost Item Valuation", filters)
+ if reposting_pending and throw_error:
+ msg = _("Stock/Accounts can not be frozen as processing of backdated entries is going on. Please try again later.")
+ frappe.msgprint(msg,
+ raise_exception=PendingRepostingError,
+ title="Stock Reposting Ongoing",
+ indicator="red",
+ primary_action={
+ "label": _("Show pending entries"),
+ "client_action": "erpnext.route_to_pending_reposts",
+ "args": filters,
+ }
+ )
+
+ return bool(reposting_pending)
diff --git a/erpnext/stock/valuation.py b/erpnext/stock/valuation.py
new file mode 100644
index 0000000..45c5083
--- /dev/null
+++ b/erpnext/stock/valuation.py
@@ -0,0 +1,146 @@
+from typing import Callable, List, NewType, Optional, Tuple
+
+from frappe.utils import flt
+
+FifoBin = NewType("FifoBin", List[float])
+
+# Indexes of values inside FIFO bin 2-tuple
+QTY = 0
+RATE = 1
+
+
+class FIFOValuation:
+ """Valuation method where a queue of all the incoming stock is maintained.
+
+ New stock is added at end of the queue.
+ Qty consumption happens on First In First Out basis.
+
+ Queue is implemented using "bins" of [qty, rate].
+
+ ref: https://en.wikipedia.org/wiki/FIFO_and_LIFO_accounting
+ """
+
+ # specifying the attributes to save resources
+ # ref: https://docs.python.org/3/reference/datamodel.html#slots
+ __slots__ = ["queue",]
+
+ def __init__(self, state: Optional[List[FifoBin]]):
+ self.queue: List[FifoBin] = state if state is not None else []
+
+ def __repr__(self):
+ return str(self.queue)
+
+ def __iter__(self):
+ return iter(self.queue)
+
+ def __eq__(self, other):
+ if isinstance(other, list):
+ return self.queue == other
+ return self.queue == other.queue
+
+ def get_state(self) -> List[FifoBin]:
+ """Get current state of queue."""
+ return self.queue
+
+ def get_total_stock_and_value(self) -> Tuple[float, float]:
+ total_qty = 0.0
+ total_value = 0.0
+
+ for qty, rate in self.queue:
+ total_qty += flt(qty)
+ total_value += flt(qty) * flt(rate)
+
+ return _round_off_if_near_zero(total_qty), _round_off_if_near_zero(total_value)
+
+ def add_stock(self, qty: float, rate: float) -> None:
+ """Update fifo queue with new stock.
+
+ args:
+ qty: new quantity to add
+ rate: incoming rate of new quantity"""
+
+ if not len(self.queue):
+ self.queue.append([0, 0])
+
+ # last row has the same rate, merge new bin.
+ if self.queue[-1][RATE] == rate:
+ self.queue[-1][QTY] += qty
+ else:
+ # Item has a positive balance qty, add new entry
+ if self.queue[-1][QTY] > 0:
+ self.queue.append([qty, rate])
+ else: # negative balance qty
+ qty = self.queue[-1][QTY] + qty
+ if qty > 0: # new balance qty is positive
+ self.queue[-1] = [qty, rate]
+ else: # new balance qty is still negative, maintain same rate
+ self.queue[-1][QTY] = qty
+
+ def remove_stock(
+ self, qty: float, outgoing_rate: float = 0.0, rate_generator: Callable[[], float] = None
+ ) -> List[FifoBin]:
+ """Remove stock from the queue and return popped bins.
+
+ args:
+ qty: quantity to remove
+ rate: outgoing rate
+ rate_generator: function to be called if queue is not found and rate is required.
+ """
+ if not rate_generator:
+ rate_generator = lambda : 0.0 # noqa
+
+ consumed_bins = []
+ while qty:
+ if not len(self.queue):
+ # rely on rate generator.
+ self.queue.append([0, rate_generator()])
+
+ index = None
+ if outgoing_rate > 0:
+ # Find the entry where rate matched with outgoing rate
+ for idx, fifo_bin in enumerate(self.queue):
+ if fifo_bin[RATE] == outgoing_rate:
+ index = idx
+ break
+
+ # If no entry found with outgoing rate, collapse queue
+ if index is None: # nosemgrep
+ new_stock_value = sum(d[QTY] * d[RATE] for d in self.queue) - qty * outgoing_rate
+ new_stock_qty = sum(d[QTY] for d in self.queue) - qty
+ self.queue = [[new_stock_qty, new_stock_value / new_stock_qty if new_stock_qty > 0 else outgoing_rate]]
+ consumed_bins.append([qty, outgoing_rate])
+ break
+ else:
+ index = 0
+
+ # select first bin or the bin with same rate
+ fifo_bin = self.queue[index]
+ if qty >= fifo_bin[QTY]:
+ # consume current bin
+ qty = _round_off_if_near_zero(qty - fifo_bin[QTY])
+ to_consume = self.queue.pop(index)
+ consumed_bins.append(list(to_consume))
+
+ if not self.queue and qty:
+ # stock finished, qty still remains to be withdrawn
+ # negative stock, keep in as a negative bin
+ self.queue.append([-qty, outgoing_rate or fifo_bin[RATE]])
+ consumed_bins.append([qty, outgoing_rate or fifo_bin[RATE]])
+ break
+ else:
+ # qty found in current bin consume it and exit
+ fifo_bin[QTY] = _round_off_if_near_zero(fifo_bin[QTY] - qty)
+ consumed_bins.append([qty, fifo_bin[RATE]])
+ qty = 0
+
+ return consumed_bins
+
+
+def _round_off_if_near_zero(number: float, precision: int = 7) -> float:
+ """Rounds off the number to zero only if number is close to zero for decimal
+ specified in precision. Precision defaults to 7.
+ """
+ if abs(0.0 - flt(number)) < (1.0 / (10 ** precision)):
+ return 0.0
+
+ return flt(number)
diff --git a/erpnext/stock/workspace/stock/stock.json b/erpnext/stock/workspace/stock/stock.json
index 9c80515..4df27f5 100644
--- a/erpnext/stock/workspace/stock/stock.json
+++ b/erpnext/stock/workspace/stock/stock.json
@@ -704,59 +704,9 @@
"link_type": "Report",
"onboard": 0,
"type": "Link"
- },
- {
- "dependencies": "Stock Ledger Entry",
- "hidden": 0,
- "is_query_report": 1,
- "label": "Stock and Account Value Comparison",
- "link_count": 0,
- "link_to": "Stock and Account Value Comparison",
- "link_type": "Report",
- "onboard": 0,
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Incorrect Data Report",
- "link_count": 0,
- "link_type": "DocType",
- "onboard": 0,
- "type": "Card Break"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Incorrect Serial No Qty and Valuation",
- "link_count": 0,
- "link_to": "Incorrect Serial No Valuation",
- "link_type": "Report",
- "onboard": 0,
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Incorrect Balance Qty After Transaction",
- "link_count": 0,
- "link_to": "Incorrect Balance Qty After Transaction",
- "link_type": "Report",
- "onboard": 0,
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Stock and Account Value Comparison",
- "link_count": 0,
- "link_to": "Stock and Account Value Comparison",
- "link_type": "Report",
- "onboard": 0,
- "type": "Link"
}
],
- "modified": "2021-08-05 12:16:02.361519",
+ "modified": "2021-11-23 04:34:00.420870",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock",
diff --git a/erpnext/support/doctype/issue/issue.json b/erpnext/support/doctype/issue/issue.json
index 14712f8..3ff7d02 100644
--- a/erpnext/support/doctype/issue/issue.json
+++ b/erpnext/support/doctype/issue/issue.json
@@ -24,12 +24,10 @@
"service_level_section",
"service_level_agreement",
"response_by",
- "response_by_variance",
"reset_service_level_agreement",
"cb",
"agreement_status",
"resolution_by",
- "resolution_by_variance",
"service_level_agreement_creation",
"on_hold_since",
"total_hold_time",
@@ -123,7 +121,6 @@
"search_index": 1
},
{
- "default": "Medium",
"fieldname": "priority",
"fieldtype": "Link",
"in_list_view": 1,
@@ -319,22 +316,6 @@
"label": "Via Customer Portal"
},
{
- "depends_on": "eval: doc.service_level_agreement && doc.status != 'Replied';",
- "fieldname": "response_by_variance",
- "fieldtype": "Duration",
- "hide_seconds": 1,
- "label": "Response By Variance",
- "read_only": 1
- },
- {
- "depends_on": "eval: doc.service_level_agreement && doc.status != 'Replied';",
- "fieldname": "resolution_by_variance",
- "fieldtype": "Duration",
- "hide_seconds": 1,
- "label": "Resolution By Variance",
- "read_only": 1
- },
- {
"fieldname": "service_level_agreement_creation",
"fieldtype": "Datetime",
"hidden": 1,
@@ -391,12 +372,12 @@
"read_only": 1
},
{
- "default": "Ongoing",
+ "default": "First Response Due",
"depends_on": "eval: doc.service_level_agreement",
"fieldname": "agreement_status",
"fieldtype": "Select",
"label": "Service Level Agreement Status",
- "options": "Ongoing\nFulfilled\nFailed",
+ "options": "First Response Due\nResolution Due\nFulfilled\nFailed",
"read_only": 1
},
{
@@ -410,10 +391,11 @@
"icon": "fa fa-ticket",
"idx": 7,
"links": [],
- "modified": "2021-06-10 03:22:27.098898",
+ "modified": "2021-11-24 13:13:10.276630",
"modified_by": "Administrator",
"module": "Support",
"name": "Issue",
+ "naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py
index 0dc3639..e211e24 100644
--- a/erpnext/support/doctype/issue/issue.py
+++ b/erpnext/support/doctype/issue/issue.py
@@ -87,11 +87,9 @@
if replicated_issue.service_level_agreement:
replicated_issue.service_level_agreement_creation = now_datetime()
replicated_issue.service_level_agreement = None
- replicated_issue.agreement_status = "Ongoing"
+ replicated_issue.agreement_status = "First Response Due"
replicated_issue.response_by = None
- replicated_issue.response_by_variance = None
replicated_issue.resolution_by = None
- replicated_issue.resolution_by_variance = None
replicated_issue.reset_issue_metrics()
frappe.get_doc(replicated_issue).insert()
@@ -238,7 +236,7 @@
return False
def calculate_first_response_time(issue, first_responded_on):
- issue_creation_date = issue.creation
+ issue_creation_date = issue.service_level_agreement_creation or issue.creation
issue_creation_time = get_time_in_seconds(issue_creation_date)
first_responded_on_in_seconds = get_time_in_seconds(first_responded_on)
support_hours = frappe.get_cached_doc("Service Level Agreement", issue.service_level_agreement).support_and_resolution
diff --git a/erpnext/support/doctype/issue/issue_list.js b/erpnext/support/doctype/issue/issue_list.js
index e04498e..5bfecb0 100644
--- a/erpnext/support/doctype/issue/issue_list.js
+++ b/erpnext/support/doctype/issue/issue_list.js
@@ -18,7 +18,6 @@
},
get_indicator: function(doc) {
if (doc.status === 'Open') {
- if (!doc.priority) doc.priority = 'Medium';
const color = {
'Low': 'yellow',
'Medium': 'orange',
diff --git a/erpnext/support/doctype/issue/test_issue.py b/erpnext/support/doctype/issue/test_issue.py
index ab9a444b..7a0a5e5 100644
--- a/erpnext/support/doctype/issue/test_issue.py
+++ b/erpnext/support/doctype/issue/test_issue.py
@@ -1,10 +1,10 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and Contributors
# See license.txt
-import datetime
import unittest
import frappe
+from frappe import _
from frappe.core.doctype.user_permission.test_user_permission import create_user
from frappe.utils import flt, get_datetime
@@ -83,30 +83,6 @@
self.assertEqual(issue.agreement_status, 'Fulfilled')
- def test_issue_metrics(self):
- creation = get_datetime("2020-03-04 4:00")
-
- issue = make_issue(creation, index=1)
- create_communication(issue.name, "test@example.com", "Received", creation)
-
- creation = get_datetime("2020-03-04 4:15")
- create_communication(issue.name, "test@admin.com", "Sent", creation)
-
- creation = get_datetime("2020-03-04 5:00")
- create_communication(issue.name, "test@example.com", "Received", creation)
-
- creation = get_datetime("2020-03-04 5:05")
- create_communication(issue.name, "test@admin.com", "Sent", creation)
-
- frappe.flags.current_time = get_datetime("2020-03-04 5:05")
- issue.reload()
- issue.status = 'Closed'
- issue.save()
-
- self.assertEqual(issue.avg_response_time, 600)
- self.assertEqual(issue.resolution_time, 3900)
- self.assertEqual(issue.user_resolution_time, 1200)
-
def test_hold_time_on_replied(self):
creation = get_datetime("2020-03-04 4:00")
@@ -122,6 +98,7 @@
issue.save()
self.assertEqual(issue.on_hold_since, frappe.flags.current_time)
+ self.assertFalse(issue.resolution_by)
creation = get_datetime("2020-03-04 5:00")
frappe.flags.current_time = get_datetime("2020-03-04 5:00")
@@ -142,6 +119,142 @@
issue.reload()
self.assertEqual(flt(issue.total_hold_time, 2), 2700)
+ def test_issue_close_after_on_hold(self):
+ frappe.flags.current_time = get_datetime("2021-11-01 19:00")
+
+ issue = make_issue(frappe.flags.current_time, index=1)
+ create_communication(issue.name, "test@example.com", "Received", frappe.flags.current_time)
+
+ # send a reply within SLA
+ frappe.flags.current_time = get_datetime("2021-11-02 11:00")
+ create_communication(issue.name, "test@admin.com", "Sent", frappe.flags.current_time)
+
+ issue.reload()
+ issue.status = 'Replied'
+ issue.save()
+
+ self.assertEqual(issue.on_hold_since, frappe.flags.current_time)
+
+ # close the issue after being on hold for 20 days
+ frappe.flags.current_time = get_datetime("2021-11-22 01:00")
+ issue.status = 'Closed'
+ issue.save()
+
+ self.assertEqual(issue.resolution_by, get_datetime('2021-11-22 06:00:00'))
+ self.assertEqual(issue.resolution_date, get_datetime('2021-11-22 01:00:00'))
+ self.assertEqual(issue.agreement_status, 'Fulfilled')
+
+ def test_issue_open_after_closed(self):
+
+ # Created on -> 1 pm, Response Time -> 4 hrs, Resolution Time -> 6 hrs
+ frappe.flags.current_time = get_datetime("2021-11-01 13:00")
+ issue = make_issue(frappe.flags.current_time, index=1, issue_type='Critical') # Applies 24hr working time SLA
+ create_communication(issue.name, "test@example.com", "Received", frappe.flags.current_time)
+ self.assertEquals(issue.agreement_status, 'First Response Due')
+ self.assertEquals(issue.response_by, get_datetime("2021-11-01 17:00"))
+ self.assertEquals(issue.resolution_by, get_datetime("2021-11-01 19:00"))
+
+ # Replied on → 2 pm
+ frappe.flags.current_time = get_datetime("2021-11-01 14:00")
+ create_communication(issue.name, "test@admin.com", "Sent", frappe.flags.current_time)
+ issue.reload()
+ issue.status = 'Replied'
+ issue.save()
+ self.assertEquals(issue.agreement_status, 'Resolution Due')
+ self.assertEquals(issue.on_hold_since, frappe.flags.current_time)
+ self.assertEquals(issue.first_responded_on, frappe.flags.current_time)
+
+ # Customer Replied → 3 pm
+ frappe.flags.current_time = get_datetime("2021-11-01 15:00")
+ create_communication(issue.name, "test@example.com", "Received", frappe.flags.current_time)
+ issue.reload()
+ self.assertEquals(issue.status, 'Open')
+ # Hold Time + 1 Hrs
+ self.assertEquals(issue.total_hold_time, 3600)
+ # Resolution By should increase by one hrs
+ self.assertEquals(issue.resolution_by, get_datetime("2021-11-01 20:00"))
+
+ # Replied on → 4 pm, Open → 1 hr, Resolution Due → 8 pm
+ frappe.flags.current_time = get_datetime("2021-11-01 16:00")
+ create_communication(issue.name, "test@admin.com", "Sent", frappe.flags.current_time)
+ issue.reload()
+ issue.status = 'Replied'
+ issue.save()
+ self.assertEquals(issue.agreement_status, 'Resolution Due')
+
+ # Customer Closed → 10 pm
+ frappe.flags.current_time = get_datetime("2021-11-01 22:00")
+ issue.status = 'Closed'
+ issue.save()
+ # Hold Time + 6 Hrs
+ self.assertEquals(issue.total_hold_time, 3600 + 21600)
+ # Resolution By should increase by 6 hrs
+ self.assertEquals(issue.resolution_by, get_datetime("2021-11-02 02:00"))
+ self.assertEquals(issue.agreement_status, 'Fulfilled')
+ self.assertEquals(issue.resolution_date, frappe.flags.current_time)
+
+ # Customer Open → 3 am i.e after resolution by is crossed
+ frappe.flags.current_time = get_datetime("2021-11-02 03:00")
+ create_communication(issue.name, "test@example.com", "Received", frappe.flags.current_time)
+ issue.reload()
+ # Since issue was Resolved, Resolution By should be increased by 5 hrs (3am - 10pm)
+ self.assertEquals(issue.total_hold_time, 3600 + 21600 + 18000)
+ # Resolution By should increase by 5 hrs
+ self.assertEquals(issue.resolution_by, get_datetime("2021-11-02 07:00"))
+ self.assertEquals(issue.agreement_status, 'Resolution Due')
+ self.assertFalse(issue.resolution_date)
+
+ # We Closed → 4 am, SLA should be Fulfilled
+ frappe.flags.current_time = get_datetime("2021-11-02 04:00")
+ issue.status = 'Closed'
+ issue.save()
+ self.assertEquals(issue.resolution_by, get_datetime("2021-11-02 07:00"))
+ self.assertEquals(issue.agreement_status, 'Fulfilled')
+ self.assertEquals(issue.resolution_date, frappe.flags.current_time)
+
+ def test_recording_of_assignment_on_first_reponse_failure(self):
+ from frappe.desk.form.assign_to import add as add_assignment
+
+ frappe.flags.current_time = get_datetime("2021-11-01 19:00")
+
+ issue = make_issue(frappe.flags.current_time, index=1)
+ create_communication(issue.name, "test@example.com", "Received", frappe.flags.current_time)
+ add_assignment({
+ 'doctype': issue.doctype,
+ 'name': issue.name,
+ 'assign_to': ['test@admin.com']
+ })
+ issue.reload()
+
+ # send a reply failing response SLA
+ frappe.flags.current_time = get_datetime("2021-11-02 15:00")
+ create_communication(issue.name, "test@admin.com", "Sent", frappe.flags.current_time)
+
+ # assert if a new timeline item has been added
+ # to record the assignment
+ comment = frappe.db.exists('Comment', {
+ 'reference_doctype': 'Issue',
+ 'reference_name': issue.name,
+ 'comment_type': 'Assigned',
+ 'content': _('First Response SLA Failed by {}').format('test')
+ })
+ self.assertTrue(comment)
+
+ def test_agreement_status_on_response(self):
+ frappe.flags.current_time = get_datetime("2021-11-01 19:00")
+
+ issue = make_issue(frappe.flags.current_time, index=1)
+ create_communication(issue.name, "test@example.com", "Received", frappe.flags.current_time)
+ self.assertTrue(issue.status == 'Open')
+
+ # send a reply within response SLA
+ frappe.flags.current_time = get_datetime("2021-11-02 11:00")
+ create_communication(issue.name, "test@admin.com", "Sent", frappe.flags.current_time)
+
+ issue.reload()
+ self.assertEquals(issue.first_responded_on, frappe.flags.current_time)
+ self.assertEquals(issue.agreement_status, 'Resolution Due')
+
class TestFirstResponseTime(TestSetUp):
# working hours used in all cases: Mon-Fri, 10am to 6pm
# all dates are in the mm-dd-yyyy format
@@ -355,12 +468,18 @@
def create_issue_and_communication(issue_creation, first_responded_on):
issue = make_issue(issue_creation, index=1)
sender = create_user("test@admin.com")
+ frappe.flags.current_time = first_responded_on
create_communication(issue.name, sender.email, "Sent", first_responded_on)
issue.reload()
return issue
def make_issue(creation=None, customer=None, index=0, priority=None, issue_type=None):
+ if issue_type and not frappe.db.exists('Issue Type', issue_type):
+ doc = frappe.new_doc('Issue Type')
+ doc.name = issue_type
+ doc.insert()
+
issue = frappe.get_doc({
"doctype": "Issue",
"subject": "Service Level Agreement Issue {0}".format(index),
diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.js b/erpnext/support/doctype/service_level_agreement/service_level_agreement.js
index ae2080c..bfbffe2 100644
--- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.js
+++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.js
@@ -22,10 +22,41 @@
refresh: function(frm) {
frm.trigger('fetch_status_fields');
frm.trigger('toggle_resolution_fields');
+ frm.trigger('default_service_level_agreement');
+ frm.trigger('entity');
+ },
+
+ default_service_level_agreement: function(frm) {
+ const field = frm.get_field('default_service_level_agreement');
+ if (frm.doc.default_service_level_agreement) {
+ field.set_description(__('SLA will be applied on every {0}', [frm.doc.document_type]));
+ } else {
+ field.set_description(__('Enable to apply SLA on every {0}', [frm.doc.document_type]));
+ }
},
document_type: function(frm) {
frm.trigger('fetch_status_fields');
+ frm.trigger('default_service_level_agreement');
+ },
+
+ entity_type: function(frm) {
+ frm.set_value('entity', undefined);
+ },
+
+ entity: function(frm) {
+ const field = frm.get_field('entity');
+ if (frm.doc.entity) {
+ const and_descendants = frm.doc.entity_type != 'Customer' ? ' ' + __('or its descendants') : '';
+ field.set_description(
+ __('SLA will be applied if {1} is set as {2}{3}', [
+ frm.doc.document_type, frm.doc.entity_type,
+ frm.doc.entity, and_descendants
+ ])
+ );
+ } else {
+ field.set_description('');
+ }
},
fetch_status_fields: function(frm) {
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 5f470aa..1698e23 100644
--- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.json
+++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.json
@@ -6,22 +6,17 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
- "enabled",
- "section_break_2",
"document_type",
- "default_service_level_agreement",
"default_priority",
"column_break_2",
"service_level",
- "holiday_list",
- "entity_section",
- "entity_type",
- "column_break_10",
- "entity",
+ "enabled",
"filters_section",
- "condition",
+ "default_service_level_agreement",
+ "entity_type",
+ "entity",
"column_break_15",
- "condition_description",
+ "condition",
"agreement_details_section",
"start_date",
"column_break_7",
@@ -31,8 +26,10 @@
"priorities",
"status_details",
"sla_fulfilled_on",
+ "column_break_22",
"pause_sla_on",
"support_and_resolution_section_break",
+ "holiday_list",
"support_and_resolution"
],
"fields": [
@@ -42,7 +39,8 @@
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Service Level Name",
- "reqd": 1
+ "reqd": 1,
+ "set_only_once": 1
},
{
"fieldname": "holiday_list",
@@ -56,10 +54,10 @@
"fieldtype": "Column Break"
},
{
- "depends_on": "eval: !doc.default_service_level_agreement",
+ "depends_on": "eval: doc.document_type",
"fieldname": "agreement_details_section",
"fieldtype": "Section Break",
- "label": "Agreement Details"
+ "label": "Valid From"
},
{
"fieldname": "start_date",
@@ -72,7 +70,6 @@
"fieldtype": "Column Break"
},
{
- "depends_on": "eval: !doc.default_service_level_agreement",
"fieldname": "end_date",
"fieldtype": "Date",
"label": "End Date"
@@ -80,7 +77,7 @@
{
"fieldname": "response_and_resolution_time_section",
"fieldtype": "Section Break",
- "label": "Response and Resolution Time"
+ "label": "Response and Resolution"
},
{
"fieldname": "support_and_resolution_section_break",
@@ -90,6 +87,7 @@
{
"fieldname": "support_and_resolution",
"fieldtype": "Table",
+ "label": "Working Hours",
"options": "Service Day",
"reqd": 1
},
@@ -101,10 +99,7 @@
"reqd": 1
},
{
- "fieldname": "column_break_10",
- "fieldtype": "Column Break"
- },
- {
+ "depends_on": "eval: !doc.default_service_level_agreement",
"fieldname": "entity",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
@@ -114,11 +109,6 @@
},
{
"depends_on": "eval: !doc.default_service_level_agreement",
- "fieldname": "entity_section",
- "fieldtype": "Section Break",
- "label": "Entity"
- },
- {
"fieldname": "entity_type",
"fieldtype": "Select",
"in_standard_filter": 1,
@@ -126,11 +116,6 @@
"options": "\nCustomer\nCustomer Group\nTerritory"
},
{
- "fieldname": "section_break_2",
- "fieldtype": "Section Break",
- "hide_border": 1
- },
- {
"default": "0",
"fieldname": "default_service_level_agreement",
"fieldtype": "Check",
@@ -152,7 +137,7 @@
{
"fieldname": "document_type",
"fieldtype": "Link",
- "label": "Document Type",
+ "label": "Apply On",
"options": "DocType",
"reqd": 1,
"set_only_once": 1
@@ -164,6 +149,7 @@
"label": "Enabled"
},
{
+ "depends_on": "document_type",
"fieldname": "status_details",
"fieldtype": "Section Break",
"label": "Status Details"
@@ -182,28 +168,31 @@
"label": "Apply SLA for Resolution Time"
},
{
+ "depends_on": "document_type",
"fieldname": "filters_section",
"fieldtype": "Section Break",
- "label": "Assignment Condition"
+ "label": "Assignment Conditions"
},
{
"fieldname": "column_break_15",
"fieldtype": "Column Break"
},
{
+ "depends_on": "eval: !doc.default_service_level_agreement",
+ "description": "Simple Python Expression, Example: doc.status == 'Open' and doc.issue_type == 'Bug'",
"fieldname": "condition",
"fieldtype": "Code",
"label": "Condition",
- "options": "Python"
+ "max_height": "7rem",
+ "options": "PythonExpression"
},
{
- "fieldname": "condition_description",
- "fieldtype": "HTML",
- "options": "<p><strong>Condition Examples:</strong></p>\n<pre>doc.status==\"Open\"<br>doc.due_date==nowdate()<br>doc.total > 40000\n</pre>"
+ "fieldname": "column_break_22",
+ "fieldtype": "Column Break"
}
],
"links": [],
- "modified": "2021-10-02 11:32:55.556024",
+ "modified": "2021-11-26 15:45:33.289911",
"modified_by": "Administrator",
"module": "Support",
"name": "Service Level Agreement",
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 5f8f83d..ea617fd 100644
--- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py
+++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py
@@ -10,7 +10,6 @@
from frappe.model.document import Document
from frappe.utils import (
add_to_date,
- cint,
get_datetime,
get_datetime_str,
get_link_to_form,
@@ -22,6 +21,7 @@
time_diff_in_seconds,
to_timedelta,
)
+from frappe.utils.nestedset import get_ancestors_of
from frappe.utils.safe_exec import get_safe_globals
from erpnext.support.doctype.issue.issue import get_holidays
@@ -248,7 +248,7 @@
customer = doc.get('customer')
or_filters.append(
- ["Service Level Agreement", "entity", "in", [customer, get_customer_group(customer), get_customer_territory(customer)]]
+ ["Service Level Agreement", "entity", "in", [customer] + get_customer_group(customer) + get_customer_territory(customer)]
)
default_sla_filter = filters + [["Service Level Agreement", "default_service_level_agreement", "=", 1]]
@@ -275,11 +275,23 @@
return {"doc": doc.as_dict(), "nowdate": nowdate, "frappe": frappe._dict(utils=get_safe_globals().get("frappe").get("utils"))}
def get_customer_group(customer):
- return frappe.db.get_value("Customer", customer, "customer_group") if customer else None
+ customer_groups = []
+ customer_group = frappe.db.get_value("Customer", customer, "customer_group") if customer else None
+ if customer_group:
+ ancestors = get_ancestors_of("Customer Group", customer_group)
+ customer_groups = [customer_group] + ancestors
+
+ return customer_groups
def get_customer_territory(customer):
- return frappe.db.get_value("Customer", customer, "territory") if customer else None
+ customer_territories = []
+ customer_territory = frappe.db.get_value("Customer", customer, "territory") if customer else None
+ if customer_territory:
+ ancestors = get_ancestors_of("Territory", customer_territory)
+ customer_territories = [customer_territory] + ancestors
+
+ return customer_territories
@frappe.whitelist()
@@ -299,7 +311,7 @@
if customer:
# Include SLA with No Entity and Entity Type
or_filters.append(
- ["Service Level Agreement", "entity", "in", [customer, get_customer_group(customer), get_customer_territory(customer), ""]]
+ ["Service Level Agreement", "entity", "in", [""] + [customer] + get_customer_group(customer) + get_customer_territory(customer)]
)
return {
@@ -337,84 +349,135 @@
def apply(doc, method=None):
# Applies SLA to document on validate
- if frappe.flags.in_patch or frappe.flags.in_migrate or frappe.flags.in_install or frappe.flags.in_setup_wizard or \
- doc.doctype not in get_documents_with_active_service_level_agreement():
+ if (
+ frappe.flags.in_patch
+ or frappe.flags.in_migrate
+ or frappe.flags.in_install
+ or frappe.flags.in_setup_wizard
+ or doc.doctype not in get_documents_with_active_service_level_agreement()
+ ):
return
- service_level_agreement = get_active_service_level_agreement_for(doc)
+ sla = get_active_service_level_agreement_for(doc)
- if not service_level_agreement:
+ if not sla:
return
- set_sla_properties(doc, service_level_agreement)
+ process_sla(doc, sla)
-def set_sla_properties(doc, service_level_agreement):
- if frappe.db.exists(doc.doctype, doc.name):
- from_db = frappe.get_doc(doc.doctype, doc.name)
- else:
- from_db = frappe._dict({})
-
- meta = frappe.get_meta(doc.doctype)
-
- if meta.has_field("customer") and service_level_agreement.customer and doc.get("customer") and \
- not service_level_agreement.customer == doc.get("customer"):
- frappe.throw(_("Service Level Agreement {0} is specific to Customer {1}").format(service_level_agreement.name,
- service_level_agreement.customer))
-
- doc.service_level_agreement = service_level_agreement.name
- doc.priority = doc.get("priority") or service_level_agreement.default_priority
- priority = get_priority(doc)
+def process_sla(doc, sla):
if not doc.creation:
doc.creation = now_datetime(doc.get("owner"))
-
- if meta.has_field("service_level_agreement_creation"):
+ if doc.meta.has_field("service_level_agreement_creation"):
doc.service_level_agreement_creation = now_datetime(doc.get("owner"))
+ doc.service_level_agreement = sla.name
+ doc.priority = doc.get("priority") or sla.default_priority
+
+ handle_status_change(doc, sla.apply_sla_for_resolution)
+ update_response_and_resolution_metrics(doc, sla.apply_sla_for_resolution)
+ update_agreement_status(doc, sla.apply_sla_for_resolution)
+
+
+def handle_status_change(doc, apply_sla_for_resolution):
+ now_time = frappe.flags.current_time or now_datetime(doc.get("owner"))
+ prev_status = frappe.db.get_value(doc.doctype, doc.name, 'status')
+
+ hold_statuses = get_hold_statuses(doc.service_level_agreement)
+ fulfillment_statuses = get_fulfillment_statuses(doc.service_level_agreement)
+
+ def is_hold_status(status):
+ return status in hold_statuses
+
+ def is_fulfilled_status(status):
+ return status in fulfillment_statuses
+
+ def is_open_status(status):
+ return status not in hold_statuses and status not in fulfillment_statuses
+
+ def set_first_response():
+ if doc.meta.has_field("first_responded_on") and not doc.get('first_responded_on'):
+ doc.first_responded_on = now_time
+ if get_datetime(doc.get('first_responded_on')) > get_datetime(doc.get('response_by')):
+ record_assigned_users_on_failure(doc)
+
+ def calculate_hold_hours():
+ # In case issue was closed and after few days it has been opened
+ # The hold time should be calculated from resolution_date
+
+ on_hold_since = doc.resolution_date or doc.on_hold_since
+ if on_hold_since:
+ current_hold_hours = time_diff_in_seconds(now_time, on_hold_since)
+ doc.total_hold_time = (doc.total_hold_time or 0) + current_hold_hours
+ doc.on_hold_since = None
+
+ if ((is_open_status(prev_status) and not is_open_status(doc.status)) or doc.flags.on_first_reply):
+ set_first_response()
+
+ # Open to Replied
+ if is_open_status(prev_status) and is_hold_status(doc.status):
+ # Issue is on hold -> Set on_hold_since
+ doc.on_hold_since = now_time
+ reset_expected_response_and_resolution(doc)
+
+ # Replied to Open
+ if is_hold_status(prev_status) and is_open_status(doc.status):
+ # Issue was on hold -> Calculate Total Hold Time
+ calculate_hold_hours()
+ # Issue is open -> reset resolution_date
+ reset_resolution_metrics(doc)
+
+ # Open to Closed
+ if is_open_status(prev_status) and is_fulfilled_status(doc.status):
+ # Issue is closed -> Set resolution_date
+ doc.resolution_date = now_time
+ set_resolution_time(doc)
+
+ # Closed to Open
+ if is_fulfilled_status(prev_status) and is_open_status(doc.status):
+ # Issue was closed -> Calculate Total Hold Time from resolution_date
+ calculate_hold_hours()
+ # Issue is open -> reset resolution_date
+ reset_resolution_metrics(doc)
+
+ # Closed to Replied
+ if is_fulfilled_status(prev_status) and is_hold_status(doc.status):
+ # Issue was closed -> Calculate Total Hold Time from resolution_date
+ calculate_hold_hours()
+ # Issue is on hold -> Set on_hold_since
+ doc.on_hold_since = now_time
+ reset_expected_response_and_resolution(doc)
+
+ # Replied to Closed
+ if is_hold_status(prev_status) and is_fulfilled_status(doc.status):
+ # Issue was on hold -> Calculate Total Hold Time
+ calculate_hold_hours()
+ # Issue is closed -> Set resolution_date
+ if apply_sla_for_resolution:
+ doc.resolution_date = now_time
+ set_resolution_time(doc)
+
+
+def get_fulfillment_statuses(service_level_agreement):
+ return [entry.status for entry in frappe.db.get_all("SLA Fulfilled On Status", filters={
+ "parent": service_level_agreement
+ }, fields=["status"])]
+
+
+def get_hold_statuses(service_level_agreement):
+ return [entry.status for entry in frappe.db.get_all("Pause SLA On Status", filters={
+ "parent": service_level_agreement
+ }, fields=["status"])]
+
+
+def update_response_and_resolution_metrics(doc, apply_sla_for_resolution):
+ priority = get_response_and_resolution_duration(doc)
start_date_time = get_datetime(doc.get("service_level_agreement_creation") or doc.creation)
-
- set_response_by_and_variance(doc, meta, start_date_time, priority)
- if service_level_agreement.apply_sla_for_resolution:
- set_resolution_by_and_variance(doc, meta, start_date_time, priority)
-
- update_status(doc, from_db, meta)
-
-
-def update_status(doc, from_db, meta):
- if meta.has_field("status"):
- if meta.has_field("first_responded_on") and doc.status != "Open" and \
- from_db.status == "Open" and not doc.first_responded_on:
- doc.first_responded_on = frappe.flags.current_time or now_datetime(doc.get("owner"))
-
- if meta.has_field("service_level_agreement") and doc.service_level_agreement:
- # mark sla status as fulfilled based on the configuration
- fulfillment_statuses = [entry.status for entry in frappe.db.get_all("SLA Fulfilled On Status", filters={
- "parent": doc.service_level_agreement
- }, fields=["status"])]
-
- if doc.status in fulfillment_statuses and from_db.status not in fulfillment_statuses:
- apply_sla_for_resolution = frappe.db.get_value("Service Level Agreement", doc.service_level_agreement,
- "apply_sla_for_resolution")
-
- if apply_sla_for_resolution and meta.has_field("resolution_date"):
- doc.resolution_date = frappe.flags.current_time or now_datetime(doc.get("owner"))
-
- if meta.has_field("agreement_status") and from_db.agreement_status == "Ongoing":
- set_service_level_agreement_variance(doc.doctype, doc.name)
- update_agreement_status(doc, meta)
-
- if apply_sla_for_resolution:
- set_resolution_time(doc, meta)
- set_user_resolution_time(doc, meta)
-
- if doc.status == "Open" and from_db.status != "Open":
- # if no date, it should be set as None and not a blank string "", as per mysql strict config
- # enable SLA and variance on Reopen
- reset_metrics(doc, meta)
- set_service_level_agreement_variance(doc.doctype, doc.name)
-
- handle_hold_time(doc, meta, from_db.status)
+ set_response_by(doc, start_date_time, priority)
+ if apply_sla_for_resolution and not doc.get('on_hold_since'): # resolution_by is reset if on hold
+ set_resolution_by(doc, start_date_time, priority)
def get_expected_time_for(parameter, service_level, start_date_time):
@@ -485,37 +548,13 @@
return support_days
-def set_service_level_agreement_variance(doctype, doc=None):
+def set_resolution_time(doc):
+ start_date_time = get_datetime(doc.get("service_level_agreement_creation") or doc.creation)
+ if doc.meta.has_field("resolution_time"):
+ doc.resolution_time = time_diff_in_seconds(doc.resolution_date, start_date_time)
- filters = {"status": "Open", "agreement_status": "Ongoing"}
-
- if doc:
- filters = {"name": doc}
-
- for entry in frappe.get_all(doctype, filters=filters):
- current_doc = frappe.get_doc(doctype, entry.name)
- current_time = frappe.flags.current_time or now_datetime(current_doc.get("owner"))
- apply_sla_for_resolution = frappe.db.get_value("Service Level Agreement", current_doc.service_level_agreement,
- "apply_sla_for_resolution")
-
- if not current_doc.first_responded_on: # first_responded_on set when first reply is sent to customer
- variance = round(time_diff_in_seconds(current_doc.response_by, current_time), 2)
- frappe.db.set_value(current_doc.doctype, current_doc.name, "response_by_variance", variance, update_modified=False)
-
- if variance < 0:
- frappe.db.set_value(current_doc.doctype, current_doc.name, "agreement_status", "Failed", update_modified=False)
-
- if apply_sla_for_resolution and not current_doc.get("resolution_date"): # resolution_date set when issue has been closed
- variance = round(time_diff_in_seconds(current_doc.resolution_by, current_time), 2)
- frappe.db.set_value(current_doc.doctype, current_doc.name, "resolution_by_variance", variance, update_modified=False)
-
- if variance < 0:
- frappe.db.set_value(current_doc.doctype, current_doc.name, "agreement_status", "Failed", update_modified=False)
-
-
-def set_user_resolution_time(doc, meta):
# total time taken by a user to close the issue apart from wait_time
- if not meta.has_field("user_resolution_time"):
+ if not doc.meta.has_field("user_resolution_time"):
return
communications = frappe.get_all("Communication", filters={
@@ -531,7 +570,7 @@
pending_time.append(wait_time)
total_pending_time = sum(pending_time)
- resolution_time_in_secs = time_diff_in_seconds(doc.resolution_date, doc.creation)
+ resolution_time_in_secs = time_diff_in_seconds(doc.resolution_date, start_date_time)
doc.user_resolution_time = resolution_time_in_secs - total_pending_time
@@ -548,12 +587,12 @@
frappe.msgprint(_("Service Level Agreement has been changed to {0}.").format(self.service_level_agreement))
-def get_priority(doc):
- service_level_agreement = frappe.get_doc("Service Level Agreement", doc.service_level_agreement)
- priority = service_level_agreement.get_service_level_agreement_priority(doc.priority)
+def get_response_and_resolution_duration(doc):
+ sla = frappe.get_doc("Service Level Agreement", doc.service_level_agreement)
+ priority = sla.get_service_level_agreement_priority(doc.priority)
priority.update({
- "support_and_resolution": service_level_agreement.support_and_resolution,
- "holiday_list": service_level_agreement.holiday_list
+ "support_and_resolution": sla.support_and_resolution,
+ "holiday_list": sla.holiday_list
})
return priority
@@ -572,120 +611,99 @@
}).insert(ignore_permissions=True)
doc.service_level_agreement_creation = now_datetime(doc.get("owner"))
- doc.set_response_and_resolution_time(priority=doc.priority, service_level_agreement=doc.service_level_agreement)
- doc.agreement_status = "Ongoing"
doc.save()
-def reset_metrics(doc, meta):
- if meta.has_field("resolution_date"):
+def reset_resolution_metrics(doc):
+ if doc.meta.has_field("resolution_date"):
doc.resolution_date = None
- if not meta.has_field("resolution_time"):
+ if doc.meta.has_field("resolution_time"):
doc.resolution_time = None
- if not meta.has_field("user_resolution_time"):
+ if doc.meta.has_field("user_resolution_time"):
doc.user_resolution_time = None
- if meta.has_field("agreement_status"):
- doc.agreement_status = "Ongoing"
-
-
-def set_resolution_time(doc, meta):
- # total time taken from issue creation to closing
- if not meta.has_field("resolution_time"):
- return
-
- doc.resolution_time = time_diff_in_seconds(doc.resolution_date, doc.creation)
-
# called via hooks on communication update
-def update_hold_time(doc, status):
+def on_communication_update(doc, status):
+ if doc.communication_type == "Comment":
+ return
+
parent = get_parent_doc(doc)
if not parent:
return
- if doc.communication_type == "Comment":
+ if not parent.meta.has_field('service_level_agreement'):
return
- status_field = parent.meta.get_field("status")
- if status_field:
- options = (status_field.options or "").splitlines()
+ if (
+ doc.sent_or_received == "Received" # a reply is received
+ and parent.get('status') == 'Open' # issue status is set as open from communication.py
+ and parent.get_doc_before_save()
+ and parent.get('status') != parent._doc_before_save.get('status') # status changed
+ ):
+ # undo the status change in db
+ # since prev status is fetched from db
+ frappe.db.set_value(
+ parent.doctype, parent.name,
+ 'status', parent._doc_before_save.get('status'),
+ update_modified=False
+ )
- # if status has a "Replied" option, then handle hold time
- if ("Replied" in options) and doc.sent_or_received == "Received":
- meta = frappe.get_meta(parent.doctype)
- handle_hold_time(parent, meta, 'Replied')
+ elif (
+ doc.sent_or_received == "Sent" # a reply is sent
+ and parent.get('first_responded_on') # first_responded_on is set from communication.py
+ and parent.get_doc_before_save()
+ and not parent._doc_before_save.get('first_responded_on') # first_responded_on was not set
+ ):
+ # reset first_responded_on since it will be handled/set later on
+ parent.first_responded_on = None
+ parent.flags.on_first_reply = True
+
+ else:
+ return
+
+ for_resolution = frappe.db.get_value('Service Level Agreement', parent.service_level_agreement, 'apply_sla_for_resolution')
+
+ handle_status_change(parent, for_resolution)
+ update_response_and_resolution_metrics(parent, for_resolution)
+ update_agreement_status(parent, for_resolution)
+
+ parent.save()
-def handle_hold_time(doc, meta, status):
- if meta.has_field("service_level_agreement") and doc.service_level_agreement:
- # set response and resolution variance as None as the issue is on Hold for status as Replied
- hold_statuses = [entry.status for entry in frappe.db.get_all("Pause SLA On Status", filters={
- "parent": doc.service_level_agreement
- }, fields=["status"])]
-
- if not hold_statuses:
- return
-
- if meta.has_field("status") and doc.status in hold_statuses and status not in hold_statuses:
- apply_hold_status(doc, meta)
-
- # calculate hold time when status is changed from any hold status to any non-hold status
- if meta.has_field("status") and doc.status not in hold_statuses and status in hold_statuses:
- reset_hold_status_and_update_hold_time(doc, meta)
+def reset_expected_response_and_resolution(doc):
+ if doc.meta.has_field("first_responded_on") and not doc.get('first_responded_on'):
+ doc.response_by = None
+ if doc.meta.has_field("resolution_by") and not doc.get('resolution_date'):
+ doc.resolution_by = None
-def apply_hold_status(doc, meta):
- update_values = {'on_hold_since': frappe.flags.current_time or now_datetime(doc.get("owner"))}
-
- if meta.has_field("first_responded_on") and not doc.first_responded_on:
- update_values['response_by'] = None
- update_values['response_by_variance'] = 0
-
- update_values['resolution_by'] = None
- update_values['resolution_by_variance'] = 0
-
- doc.db_set(update_values)
+def set_response_by(doc, start_date_time, priority):
+ if doc.meta.has_field("response_by"):
+ doc.response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time)
+ if doc.meta.has_field("total_hold_time") and doc.get('total_hold_time') and not doc.get('first_responded_on'):
+ doc.response_by = add_to_date(doc.response_by, seconds=round(doc.get('total_hold_time')))
-def reset_hold_status_and_update_hold_time(doc, meta):
- hold_time = doc.total_hold_time if meta.has_field("total_hold_time") and doc.total_hold_time else 0
- now_time = frappe.flags.current_time or now_datetime(doc.get("owner"))
- last_hold_time = 0
- update_values = {}
+def set_resolution_by(doc, start_date_time, priority):
+ if doc.meta.has_field("resolution_by"):
+ doc.resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time)
+ if doc.meta.has_field("total_hold_time") and doc.get('total_hold_time'):
+ doc.resolution_by = add_to_date(doc.resolution_by, seconds=round(doc.get('total_hold_time')))
- if meta.has_field("on_hold_since") and doc.on_hold_since:
- # last_hold_time will be added to the sla variables
- last_hold_time = time_diff_in_seconds(now_time, doc.on_hold_since)
- update_values['total_hold_time'] = hold_time + last_hold_time
- # re-calculate SLA variables after issue changes from any hold status to any non-hold status
- start_date_time = get_datetime(doc.get("service_level_agreement_creation") or doc.creation)
- priority = get_priority(doc)
- now_time = frappe.flags.current_time or now_datetime(doc.get("owner"))
-
- # add hold time to response by variance
- if meta.has_field("first_responded_on") and not doc.first_responded_on:
- response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time)
- response_by = add_to_date(response_by, seconds=round(last_hold_time))
- response_by_variance = round(time_diff_in_seconds(response_by, now_time))
-
- update_values['response_by'] = response_by
- update_values['response_by_variance'] = response_by_variance + last_hold_time
-
- # add hold time to resolution by variance
- if frappe.db.get_value("Service Level Agreement", doc.service_level_agreement, "apply_sla_for_resolution"):
- resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time)
- resolution_by = add_to_date(resolution_by, seconds=round(last_hold_time))
- resolution_by_variance = round(time_diff_in_seconds(resolution_by, now_time))
-
- update_values['resolution_by'] = resolution_by
- update_values['resolution_by_variance'] = resolution_by_variance + last_hold_time
-
- update_values['on_hold_since'] = None
-
- doc.db_set(update_values)
+def record_assigned_users_on_failure(doc):
+ assigned_users = doc.get_assigned_users()
+ if assigned_users:
+ from frappe.utils import get_fullname
+ assigned_users = ', '.join((get_fullname(user) for user in assigned_users))
+ message = _('First Response SLA Failed by {}').format(assigned_users)
+ doc.add_comment(
+ comment_type='Assigned',
+ text=message
+ )
def get_service_level_agreement_fields():
@@ -715,16 +733,10 @@
"read_only": 1
},
{
- "fieldname": "response_by_variance",
- "fieldtype": "Duration",
- "hide_seconds": 1,
- "label": "Response By Variance",
- "read_only": 1
- },
- {
"fieldname": "first_responded_on",
"fieldtype": "Datetime",
"label": "First Responded On",
+ "no_copy": 1,
"read_only": 1
},
{
@@ -746,11 +758,11 @@
"read_only": 1
},
{
- "default": "Ongoing",
+ "default": "First Response Due",
"fieldname": "agreement_status",
"fieldtype": "Select",
"label": "Service Level Agreement Status",
- "options": "Ongoing\nFulfilled\nFailed",
+ "options": "First Response Due\nResolution Due\nFulfilled\nFailed",
"read_only": 1
},
{
@@ -760,13 +772,6 @@
"read_only": 1
},
{
- "fieldname": "resolution_by_variance",
- "fieldtype": "Duration",
- "hide_seconds": 1,
- "label": "Resolution By Variance",
- "read_only": 1
- },
- {
"fieldname": "service_level_agreement_creation",
"fieldtype": "Datetime",
"hidden": 1,
@@ -786,43 +791,28 @@
def update_agreement_status_on_custom_status(doc):
# Update Agreement Fulfilled status using Custom Scripts for Custom Status
-
- meta = frappe.get_meta(doc.doctype)
- now_time = frappe.flags.current_time or now_datetime(doc.get("owner"))
- if meta.has_field("first_responded_on") and not doc.first_responded_on:
- # first_responded_on set when first reply is sent to customer
- doc.response_by_variance = round(time_diff_in_seconds(doc.response_by, now_time), 2)
-
- if meta.has_field("resolution_date") and not doc.resolution_date:
- # resolution_date set when issue has been closed
- doc.resolution_by_variance = round(time_diff_in_seconds(doc.resolution_by, now_time), 2)
-
- if meta.has_field("agreement_status"):
- doc.agreement_status = "Fulfilled" if doc.response_by_variance > 0 and doc.resolution_by_variance > 0 else "Failed"
+ update_agreement_status(doc)
-def update_agreement_status(doc, meta):
- if meta.has_field("service_level_agreement") and meta.has_field("agreement_status") and \
- doc.service_level_agreement and doc.agreement_status == "Ongoing":
-
- apply_sla_for_resolution = frappe.db.get_value("Service Level Agreement", doc.service_level_agreement,
- "apply_sla_for_resolution")
-
+def update_agreement_status(doc, apply_sla_for_resolution):
+ if (doc.meta.has_field("agreement_status")):
# if SLA is applied for resolution check for response and resolution, else only response
if apply_sla_for_resolution:
- if meta.has_field("response_by_variance") and meta.has_field("resolution_by_variance"):
- if cint(frappe.db.get_value(doc.doctype, doc.name, "response_by_variance")) < 0 or \
- cint(frappe.db.get_value(doc.doctype, doc.name, "resolution_by_variance")) < 0:
-
- doc.agreement_status = "Failed"
- else:
- doc.agreement_status = "Fulfilled"
- else:
- if meta.has_field("response_by_variance") and \
- cint(frappe.db.get_value(doc.doctype, doc.name, "response_by_variance")) < 0:
- doc.agreement_status = "Failed"
- else:
+ if doc.meta.has_field("first_responded_on") and not doc.get('first_responded_on'):
+ doc.agreement_status = "First Response Due"
+ elif doc.meta.has_field("resolution_date") and not doc.get('resolution_date'):
+ doc.agreement_status = "Resolution Due"
+ elif get_datetime(doc.get('resolution_date')) <= get_datetime(doc.get('resolution_by')):
doc.agreement_status = "Fulfilled"
+ else:
+ doc.agreement_status = "Failed"
+ else:
+ if doc.meta.has_field("first_responded_on") and not doc.get('first_responded_on'):
+ doc.agreement_status = "First Response Due"
+ elif get_datetime(doc.get('first_responded_on')) <= get_datetime(doc.get('response_by')):
+ doc.agreement_status = "Fulfilled"
+ else:
+ doc.agreement_status = "Failed"
def is_holiday(date, holidays):
@@ -835,23 +825,6 @@
return datetime.timedelta(hours=time.hour, minutes=time.minute, seconds=time.second)
-def set_response_by_and_variance(doc, meta, start_date_time, priority):
- if meta.has_field("response_by"):
- doc.response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time)
-
- if meta.has_field("response_by_variance") and not doc.get('first_responded_on'):
- now_time = frappe.flags.current_time or now_datetime(doc.get("owner"))
- doc.response_by_variance = round(time_diff_in_seconds(doc.response_by, now_time), 2)
-
-def set_resolution_by_and_variance(doc, meta, start_date_time, priority):
- if meta.has_field("resolution_by"):
- doc.resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time)
-
- if meta.has_field("resolution_by_variance") and not doc.get("resolution_date"):
- now_time = frappe.flags.current_time or now_datetime(doc.get("owner"))
- doc.resolution_by_variance = round(time_diff_in_seconds(doc.resolution_by, now_time), 2)
-
-
def now_datetime(user):
dt = convert_utc_to_user_timezone(datetime.utcnow(), user)
return dt.replace(tzinfo=None)
@@ -880,7 +853,7 @@
@frappe.whitelist()
def get_sla_doctypes():
doctypes = []
- data = frappe.get_list('Service Level Agreement',
+ data = frappe.get_all('Service Level Agreement',
{'enabled': 1},
['document_type'],
distinct=1
diff --git a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py
index cfbe744..b07c862 100644
--- a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py
+++ b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py
@@ -220,42 +220,6 @@
lead.reload()
self.assertEqual(lead.agreement_status, 'Fulfilled')
- def test_changing_of_variance_after_response(self):
- # create lead
- doctype = "Lead"
- lead_sla = create_service_level_agreement(
- default_service_level_agreement=1,
- holiday_list="__Test Holiday List",
- entity_type=None, entity=None,
- response_time=14400,
- doctype=doctype,
- sla_fulfilled_on=[{"status": "Replied"}],
- apply_sla_for_resolution=0
- )
- creation = datetime.datetime(2019, 3, 4, 12, 0)
- lead = make_lead(creation=creation, index=2)
- self.assertEqual(lead.service_level_agreement, lead_sla.name)
-
- # set lead as replied to set first responded on
- frappe.flags.current_time = datetime.datetime(2019, 3, 4, 15, 30)
- lead.reload()
- lead.status = 'Replied'
- lead.save()
- lead.reload()
- self.assertEqual(lead.agreement_status, 'Fulfilled')
-
- # check response_by_variance
- self.assertEqual(lead.first_responded_on, frappe.flags.current_time)
- self.assertEqual(lead.response_by_variance, 1800.0)
-
- # make a change on the document &
- # check response_by_variance is unchanged
- frappe.flags.current_time = datetime.datetime(2019, 3, 4, 18, 30)
- lead.status = 'Open'
- lead.save()
- lead.reload()
- self.assertEqual(lead.response_by_variance, 1800.0)
-
def test_service_level_agreement_filters(self):
doctype = "Lead"
lead_sla = create_service_level_agreement(
@@ -295,7 +259,8 @@
return service_level_agreement
def create_service_level_agreement(default_service_level_agreement, holiday_list, response_time, entity_type,
- entity, resolution_time=0, doctype="Issue", condition="", sla_fulfilled_on=[], pause_sla_on=[], apply_sla_for_resolution=1):
+ entity, resolution_time=0, doctype="Issue", condition="", sla_fulfilled_on=[], pause_sla_on=[], apply_sla_for_resolution=1,
+ service_level=None, start_time="10:00:00", end_time="18:00:00"):
make_holiday_list()
make_priorities()
@@ -312,7 +277,7 @@
"doctype": "Service Level Agreement",
"enabled": 1,
"document_type": doctype,
- "service_level": "__Test {} SLA".format(entity_type if entity_type else "Default"),
+ "service_level": service_level or "__Test {} SLA".format(entity_type if entity_type else "Default"),
"default_service_level_agreement": default_service_level_agreement,
"condition": condition,
"default_priority": "Medium",
@@ -345,28 +310,28 @@
"support_and_resolution": [
{
"workday": "Monday",
- "start_time": "10:00:00",
- "end_time": "18:00:00",
+ "start_time": start_time,
+ "end_time": end_time,
},
{
"workday": "Tuesday",
- "start_time": "10:00:00",
- "end_time": "18:00:00",
+ "start_time": start_time,
+ "end_time": end_time,
},
{
"workday": "Wednesday",
- "start_time": "10:00:00",
- "end_time": "18:00:00",
+ "start_time": start_time,
+ "end_time": end_time,
},
{
"workday": "Thursday",
- "start_time": "10:00:00",
- "end_time": "18:00:00",
+ "start_time": start_time,
+ "end_time": end_time,
},
{
"workday": "Friday",
- "start_time": "10:00:00",
- "end_time": "18:00:00",
+ "start_time": start_time,
+ "end_time": end_time,
}
]
})
@@ -386,7 +351,7 @@
if sla:
frappe.delete_doc("Service Level Agreement", sla, force=1)
- return frappe.get_doc(service_level_agreement).insert(ignore_permissions=True)
+ return frappe.get_doc(service_level_agreement).insert(ignore_permissions=True, ignore_if_duplicate=True)
def create_customer():
@@ -443,6 +408,13 @@
create_service_level_agreement(default_service_level_agreement=0, holiday_list="__Test Holiday List",
entity_type="Territory", entity="_Test SLA Territory", response_time=7200, resolution_time=10800)
+ create_service_level_agreement(
+ default_service_level_agreement=0, holiday_list="__Test Holiday List",
+ entity_type=None, entity=None, response_time=14400, resolution_time=21600,
+ service_level="24-hour-SLA", start_time="00:00:00", end_time="23:59:59",
+ condition="doc.issue_type == 'Critical'"
+ )
+
def make_holiday_list():
holiday_list = frappe.db.exists("Holiday List", "__Test Holiday List")
if not holiday_list:
diff --git a/erpnext/support/report/issue_summary/issue_summary.py b/erpnext/support/report/issue_summary/issue_summary.py
index 39a5c40..67fe345 100644
--- a/erpnext/support/report/issue_summary/issue_summary.py
+++ b/erpnext/support/report/issue_summary/issue_summary.py
@@ -82,7 +82,8 @@
self.sla_status_map = {
'SLA Failed': 'failed',
'SLA Fulfilled': 'fulfilled',
- 'SLA Ongoing': 'ongoing'
+ 'First Response Due': 'first_response_due',
+ 'Resolution Due': 'resolution_due'
}
for label, fieldname in self.sla_status_map.items():
diff --git a/erpnext/tests/test_init.py b/erpnext/tests/test_init.py
index 36a9bf5..6184972 100644
--- a/erpnext/tests/test_init.py
+++ b/erpnext/tests/test_init.py
@@ -8,13 +8,8 @@
class TestInit(unittest.TestCase):
def test_encode_company_abbr(self):
- company = frappe.new_doc("Company")
- company.company_name = "New from Existing Company For Test"
- company.abbr = "NFECT"
- company.default_currency = "INR"
- company.save()
- abbr = company.abbr
+ abbr = "NFECT"
names = [
"Warehouse Name", "ERPNext Foundation India", "Gold - Member - {a}".format(a=abbr),
@@ -32,7 +27,7 @@
]
for i in range(len(names)):
- enc_name = encode_company_abbr(names[i], company.name)
+ enc_name = encode_company_abbr(names[i], abbr=abbr)
self.assertTrue(
enc_name == expected_names[i],
"{enc} is not same as {exp}".format(enc=enc_name, exp=expected_names[i])
diff --git a/erpnext/tests/utils.py b/erpnext/tests/utils.py
index 91df548..bc9f04e 100644
--- a/erpnext/tests/utils.py
+++ b/erpnext/tests/utils.py
@@ -2,6 +2,7 @@
# License: GNU General Public License v3. See license.txt
import copy
+import signal
import unittest
from contextlib import contextmanager
from typing import Any, Dict, NewType, Optional
@@ -124,14 +125,40 @@
if default_filters is None:
default_filters = {}
+ test_filters = []
report_execute_fn = frappe.get_attr(get_report_module_dotted_path(module, report_name) + ".execute")
report_filters = frappe._dict(default_filters).copy().update(filters)
- report_data = report_execute_fn(report_filters)
+ test_filters.append(report_filters)
if optional_filters:
for key, value in optional_filters.items():
- filter_with_optional_param = report_filters.copy().update({key: value})
- report_execute_fn(filter_with_optional_param)
+ test_filters.append(report_filters.copy().update({key: value}))
- return report_data
+ for test_filter in test_filters:
+ try:
+ report_execute_fn(test_filter)
+ except Exception:
+ print(f"Report failed to execute with filters: {test_filter}")
+ raise
+
+
+
+def timeout(seconds=30, error_message="Test timed out."):
+ """ Timeout decorator to ensure a test doesn't run for too long.
+
+ adapted from https://stackoverflow.com/a/2282656"""
+ def decorator(func):
+ def _handle_timeout(signum, frame):
+ raise Exception(error_message)
+
+ def wrapper(*args, **kwargs):
+ signal.signal(signal.SIGALRM, _handle_timeout)
+ signal.alarm(seconds)
+ try:
+ result = func(*args, **kwargs)
+ finally:
+ signal.alarm(0)
+ return result
+ return wrapper
+ return decorator
diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv
index ca03a78..0aca1a0 100644
--- a/erpnext/translations/de.csv
+++ b/erpnext/translations/de.csv
@@ -242,7 +242,7 @@
Appointment Confirmation,Terminbestätigung,
Appointment Duration (mins),Termindauer (Min.),
Appointment Type,Termin-Typ,
-Appointment {0} and Sales Invoice {1} cancelled,Termin {0} und Verkaufsrechnung {1} wurden storniert,
+Appointment {0} and Sales Invoice {1} cancelled,Termin {0} und Ausgangsrechnung {1} wurden storniert,
Appointments and Encounters,Termine und Begegnungen,
Appointments and Patient Encounters,Termine und Patienten-Begegnungen,
Appraisal {0} created for Employee {1} in the given date range,Bewertung {0} für Mitarbeiter {1} im angegebenen Datumsbereich erstellt,
@@ -427,7 +427,7 @@
Buying Rate,Kaufrate,
"Buying must be checked, if Applicable For is selected as {0}","Einkauf muss ausgewählt sein, wenn ""Anwenden auf"" auf {0} gesetzt wurde",
By {0},Von {0},
-Bypass credit check at Sales Order ,Kreditprüfung im Kundenauftrag umgehen,
+Bypass credit check at Sales Order ,Kreditprüfung im Auftrag umgehen,
C-Form records,Kontakt-Formular Datensätze,
C-form is not applicable for Invoice: {0},Kontaktformular nicht anwendbar auf Rechnung: {0},
CEO,CEO,
@@ -474,11 +474,11 @@
"Cannot delete Serial No {0}, as it is used in stock transactions","Die Seriennummer {0} kann nicht gelöscht werden, da sie in Lagertransaktionen verwendet wird",
Cannot enroll more than {0} students for this student group.,Kann nicht mehr als {0} Studenten für diese Studentengruppe einschreiben.,
Cannot find active Leave Period,Aktive Abwesenheitszeit kann nicht gefunden werden,
-Cannot produce more Item {0} than Sales Order quantity {1},"Es können nicht mehr Artikel {0} produziert werden, als die über Kundenaufträge bestellte Stückzahl {1}",
+Cannot produce more Item {0} than Sales Order quantity {1},"Es können nicht mehr Artikel {0} produziert werden, als die über den Auftrag bestellte Stückzahl {1}",
Cannot promote Employee with status Left,Mitarbeiter mit Status "Links" kann nicht gefördert werden,
Cannot refer row number greater than or equal to current row number for this Charge type,"Für diese Berechnungsart kann keine Zeilennummern zugeschrieben werden, die größer oder gleich der aktuellen Zeilennummer ist",
Cannot select charge type as 'On Previous Row Amount' or 'On Previous Row Total' for first row,"Die Berechnungsart kann für die erste Zeile nicht auf ""bezogen auf Menge der vorhergenden Zeile"" oder auf ""bezogen auf Gesamtmenge der vorhergenden Zeile"" gesetzt werden",
-Cannot set as Lost as Sales Order is made.,"Kann nicht als verloren gekennzeichnet werden, da ein Kundenauftrag dazu existiert.",
+Cannot set as Lost as Sales Order is made.,"Kann nicht als verloren gekennzeichnet werden, da ein Auftrag dazu existiert.",
Cannot set authorization on basis of Discount for {0},Genehmigung kann nicht auf der Basis des Rabattes für {0} festgelegt werden,
Cannot set multiple Item Defaults for a company.,Es können nicht mehrere Artikelstandards für ein Unternehmen festgelegt werden.,
Cannot set quantity less than delivered quantity,Menge kann nicht kleiner als gelieferte Menge sein,
@@ -663,7 +663,7 @@
Create Maintenance Visit,Wartungsbesuch anlegen,
Create Material Request,Materialanforderung erstellen,
Create Multiple,Erstellen Sie mehrere,
-Create Opening Sales and Purchase Invoices,Erstellen Sie Eingangsverkaufs- und Einkaufsrechnungen,
+Create Opening Sales and Purchase Invoices,Erstellen Sie die eröffnungs Ein- und Ausgangsrechnungen,
Create Payment Entries,Zahlungseinträge erstellen,
Create Payment Entry,Zahlungseintrag erstellen,
Create Print Format,Druckformat erstellen,
@@ -672,9 +672,9 @@
Create Quotation,Angebot erstellen,
Create Salary Slip,Gehaltsabrechnung erstellen,
Create Salary Slips,Gehaltszettel erstellen,
-Create Sales Invoice,Verkaufsrechnung erstellen,
-Create Sales Order,Kundenauftrag anlegen,
-Create Sales Orders to help you plan your work and deliver on-time,"Erstellen Sie Kundenaufträge, um Ihre Arbeit zu planen und pünktlich zu liefern",
+Create Sales Invoice,Ausgangsrechnung erstellen,
+Create Sales Order,Auftrag anlegen,
+Create Sales Orders to help you plan your work and deliver on-time,"Erstellen Sie Aufträge, um Ihre Arbeit zu planen und pünktlich zu liefern",
Create Sample Retention Stock Entry,Legen Sie einen Muster-Retention-Stock-Eintrag an,
Create Student,Schüler erstellen,
Create Student Batch,Studentenstapel erstellen,
@@ -808,7 +808,7 @@
Delivery Note,Lieferschein,
Delivery Note {0} is not submitted,Lieferschein {0} ist nicht gebucht,
Delivery Note {0} must not be submitted,Lieferschein {0} darf nicht gebucht sein,
-Delivery Notes {0} must be cancelled before cancelling this Sales Order,Lieferscheine {0} müssen vor Löschung dieser Kundenaufträge storniert werden,
+Delivery Notes {0} must be cancelled before cancelling this Sales Order,Lieferscheine {0} müssen vor Löschung dieser Aufträge storniert werden,
Delivery Notes {0} updated,Lieferhinweise {0} aktualisiert,
Delivery Status,Lieferstatus,
Delivery Trip,Liefertrip,
@@ -981,7 +981,7 @@
Executive Search,Direktsuche,
Expand All,Alle ausklappen,
Expected Delivery Date,Geplanter Liefertermin,
-Expected Delivery Date should be after Sales Order Date,Voraussichtlicher Liefertermin sollte nach Kundenauftragsdatum erfolgen,
+Expected Delivery Date should be after Sales Order Date,Voraussichtlicher Liefertermin sollte nach Auftragsdatum erfolgen,
Expected End Date,Voraussichtliches Enddatum,
Expected Hrs,Erwartete Stunden,
Expected Start Date,Voraussichtliches Startdatum,
@@ -1235,7 +1235,7 @@
Identifying Decision Makers,Entscheidungsträger identifizieren,
"If Auto Opt In is checked, then the customers will be automatically linked with the concerned Loyalty Program (on save)","Wenn Automatische Anmeldung aktiviert ist, werden die Kunden automatisch mit dem betreffenden Treueprogramm verknüpft (beim Speichern)",
"If multiple Pricing Rules continue to prevail, users are asked to set Priority manually to resolve conflict.","Wenn mehrere Preisregeln weiterhin gleichrangig gelten, werden die Benutzer aufgefordert, Vorrangregelungen manuell zu erstellen, um den Konflikt zu lösen.",
-"If selected Pricing Rule is made for 'Rate', it will overwrite Price List. Pricing Rule rate is the final rate, so no further discount should be applied. Hence, in transactions like Sales Order, Purchase Order etc, it will be fetched in 'Rate' field, rather than 'Price List Rate' field.","Wenn die ausgewählte Preisregel für "Rate" festgelegt wurde, wird die Preisliste überschrieben. Der Preisregelpreis ist der Endpreis, daher sollte kein weiterer Rabatt angewendet werden. Daher wird es in Transaktionen wie Kundenauftrag, Bestellung usw. im Feld 'Preis' und nicht im Feld 'Preislistenpreis' abgerufen.",
+"If selected Pricing Rule is made for 'Rate', it will overwrite Price List. Pricing Rule rate is the final rate, so no further discount should be applied. Hence, in transactions like Sales Order, Purchase Order etc, it will be fetched in 'Rate' field, rather than 'Price List Rate' field.","Wenn die ausgewählte Preisregel für "Rate" festgelegt wurde, wird die Preisliste überschrieben. Der Preisregelpreis ist der Endpreis, daher sollte kein weiterer Rabatt angewendet werden. Daher wird es in Transaktionen wie Auftrag, Bestellung usw. im Feld 'Preis' und nicht im Feld 'Preislistenpreis' abgerufen.",
"If two or more Pricing Rules are found based on the above conditions, Priority is applied. Priority is a number between 0 to 20 while default value is zero (blank). Higher number means it will take precedence if there are multiple Pricing Rules with same conditions.","Wenn zwei oder mehrere Preisregeln basierend auf den oben genannten Bedingungen gefunden werden, wird eine Vorrangregelung angewandt. Priorität ist eine Zahl zwischen 0 und 20, wobei der Standardwert Null (leer) ist. Die höhere Zahl hat Vorrang, wenn es mehrere Preisregeln zu den gleichen Bedingungen gibt.",
"If unlimited expiry for the Loyalty Points, keep the Expiry Duration empty or 0.","Wenn die Treuepunkte unbegrenzt ablaufen, lassen Sie die Ablaufdauer leer oder 0.",
"If you have any questions, please get back to us.","Wenn Sie Fragen haben, wenden Sie sich bitte an uns.",
@@ -1386,7 +1386,7 @@
Item {0} must be a non-stock item,Artikel {0} darf kein Lagerartikel sein,
Item {0} must be a stock Item,Artikel {0} muss ein Lagerartikel sein,
Item {0} not found,Artikel {0} nicht gefunden,
-Item {0} not found in 'Raw Materials Supplied' table in Purchase Order {1},"Artikel {0} in Tabelle ""Rohmaterialien geliefert"" des Lieferantenauftrags {1} nicht gefunden",
+Item {0} not found in 'Raw Materials Supplied' table in Purchase Order {1},"Artikel {0} in Tabelle ""Rohmaterialien geliefert"" der Bestellung {1} nicht gefunden",
Item {0}: Ordered qty {1} cannot be less than minimum order qty {2} (defined in Item).,Artikel {0}: Bestellmenge {1} kann nicht weniger als Mindestbestellmenge {2} (im Artikel definiert) sein.,
Item: {0} does not exist in the system,Artikel: {0} ist nicht im System vorhanden,
Items,Artikel,
@@ -1489,7 +1489,7 @@
Loyalty Amount,Loyalitätsbetrag,
Loyalty Point Entry,Loyalitätspunkteintrag,
Loyalty Points,Treuepunkte,
-"Loyalty Points will be calculated from the spent done (via the Sales Invoice), based on collection factor mentioned.","Treuepunkte werden aus dem ausgegebenen Betrag (über die Verkaufsrechnung) berechnet, basierend auf dem genannten Sammelfaktor.",
+"Loyalty Points will be calculated from the spent done (via the Sales Invoice), based on collection factor mentioned.","Treuepunkte werden aus dem ausgegebenen Betrag (über die Ausgangsrechnung) berechnet, basierend auf dem genannten Sammelfaktor.",
Loyalty Points: {0},Treuepunkte: {0},
Loyalty Program,Treueprogramm,
Main,Haupt,
@@ -1499,11 +1499,11 @@
Maintenance Schedule,Wartungsplan,
Maintenance Schedule is not generated for all the items. Please click on 'Generate Schedule',"Wartungsplan wird nicht für alle Elemente erzeugt. Bitte klicken Sie auf ""Zeitplan generieren""",
Maintenance Schedule {0} exists against {1},Wartungsplan {0} existiert gegen {1},
-Maintenance Schedule {0} must be cancelled before cancelling this Sales Order,Wartungsplan {0} muss vor Stornierung dieses Kundenauftrages aufgehoben werden,
+Maintenance Schedule {0} must be cancelled before cancelling this Sales Order,Wartungsplan {0} muss vor Stornierung dieses Auftrags aufgehoben werden,
Maintenance Status has to be Cancelled or Completed to Submit,Der Wartungsstatus muss abgebrochen oder zum Senden abgeschlossen werden,
Maintenance User,Nutzer Instandhaltung,
Maintenance Visit,Wartungsbesuch,
-Maintenance Visit {0} must be cancelled before cancelling this Sales Order,Wartungsbesuch {0} muss vor Stornierung dieses Kundenauftrages abgebrochen werden,
+Maintenance Visit {0} must be cancelled before cancelling this Sales Order,Wartungsbesuch {0} muss vor Stornierung dieses Auftrags abgebrochen werden,
Maintenance start date can not be before delivery date for Serial No {0},Startdatum der Wartung kann nicht vor dem Liefertermin für Seriennummer {0} liegen,
Make,Erstellen,
Make Payment,Zahlung ausführen,
@@ -1549,8 +1549,8 @@
Material Request Date,Material Auftragsdatum,
Material Request No,Materialanfragenr.,
"Material Request not created, as quantity for Raw Materials already available.","Materialanforderung nicht angelegt, da Menge für Rohstoffe bereits vorhanden.",
-Material Request of maximum {0} can be made for Item {1} against Sales Order {2},Materialanfrage von maximal {0} kann für Artikel {1} zum Kundenauftrag {2} gemacht werden,
-Material Request to Purchase Order,Von der Materialanfrage zum Lieferantenauftrag,
+Material Request of maximum {0} can be made for Item {1} against Sales Order {2},Materialanfrage von maximal {0} kann für Artikel {1} zum Auftrag {2} gemacht werden,
+Material Request to Purchase Order,Von der Materialanfrage zur Bestellung,
Material Request {0} is cancelled or stopped,Materialanfrage {0} wird storniert oder gestoppt,
Material Request {0} submitted.,Materialanfrage {0} gesendet.,
Material Transfer,Materialübertrag,
@@ -1847,7 +1847,7 @@
Overlap in scoring between {0} and {1},Überlappung beim Scoring zwischen {0} und {1},
Overlapping conditions found between:,Überlagernde Bedingungen gefunden zwischen:,
Owner,Besitzer,
-PAN,PFANNE,
+PAN,PAN,
POS,Verkaufsstelle,
POS Profile,Verkaufsstellen-Profil,
POS Profile is required to use Point-of-Sale,"POS-Profil ist erforderlich, um Point-of-Sale zu verwenden",
@@ -2224,7 +2224,7 @@
Purchase,Einkauf,
Purchase Amount,Gesamtbetrag des Einkaufs,
Purchase Date,Kaufdatum,
-Purchase Invoice,Einkaufsrechnung,
+Purchase Invoice,Eingangsrechnung,
Purchase Invoice {0} is already submitted,Eingangsrechnung {0} wurde bereits übertragen,
Purchase Manager,Einkaufsleiter,
Purchase Master Manager,Einkaufsstammdaten-Manager,
@@ -2233,11 +2233,11 @@
Purchase Order Amount(Company Currency),Bestellbetrag (Firmenwährung),
Purchase Order Date,Bestelldatum,
Purchase Order Items not received on time,Bestellpositionen nicht rechtzeitig erhalten,
-Purchase Order number required for Item {0},Lieferantenauftragsnummer ist für den Artikel {0} erforderlich,
-Purchase Order to Payment,Vom Lieferantenauftrag zur Zahlung,
-Purchase Order {0} is not submitted,Lieferantenauftrag {0} wurde nicht übertragen,
+Purchase Order number required for Item {0},Bestellnummer ist für den Artikel {0} erforderlich,
+Purchase Order to Payment,Von der Bestellung zur Zahlung,
+Purchase Order {0} is not submitted,Bestellung {0} wurde nicht übertragen,
Purchase Orders are not allowed for {0} due to a scorecard standing of {1}.,Kaufaufträge sind für {0} wegen einer Scorecard von {1} nicht erlaubt.,
-Purchase Orders given to Suppliers.,An Lieferanten erteilte Lieferantenaufträge,
+Purchase Orders given to Suppliers.,An Lieferanten erteilte Bestellungen,
Purchase Price List,Einkaufspreisliste,
Purchase Receipt,Kaufbeleg,
Purchase Receipt {0} is not submitted,Kaufbeleg {0} wurde nicht übertragen,
@@ -2440,7 +2440,7 @@
Row #{0}: Expected Delivery Date cannot be before Purchase Order Date,Row # {0}: Voraussichtlicher Liefertermin kann nicht vor Bestelldatum sein,
Row #{0}: Item added,Zeile # {0}: Element hinzugefügt,
Row #{0}: Journal Entry {1} does not have account {2} or already matched against another voucher,Row # {0}: Journal Entry {1} nicht Konto {2} oder bereits abgestimmt gegen einen anderen Gutschein,
-Row #{0}: Not allowed to change Supplier as Purchase Order already exists,"Zeile #{0}: Es ist nicht erlaubt den Lieferanten zu wechseln, da bereits ein Lieferantenauftrag vorhanden ist",
+Row #{0}: Not allowed to change Supplier as Purchase Order already exists,"Zeile #{0}: Es ist nicht erlaubt den Lieferanten zu wechseln, da bereits eine Bestellung vorhanden ist",
Row #{0}: Please set reorder quantity,Zeile #{0}: Bitte Nachbestellmenge angeben,
Row #{0}: Please specify Serial No for Item {1},Zeile #{0}: Bitte Seriennummer für Artikel {1} angeben,
Row #{0}: Qty increased by 1,Zeile # {0}: Menge um 1 erhöht,
@@ -2483,7 +2483,7 @@
Row {0}: Invalid reference {1},Zeile {0}: Ungültige Referenz {1},
Row {0}: Party / Account does not match with {1} / {2} in {3} {4},Zeile {0}: Gruppe / Konto stimmt nicht mit {1} / {2} in {3} {4} überein,
Row {0}: Party Type and Party is required for Receivable / Payable account {1},Zeile {0}: Gruppen-Typ und Gruppe sind für Forderungen-/Verbindlichkeiten-Konto {1} zwingend erforderlich,
-Row {0}: Payment against Sales/Purchase Order should always be marked as advance,"Zeile {0}: ""Zahlung zu Kunden-/Lieferantenauftrag"" sollte immer als ""Vorkasse"" eingestellt werden",
+Row {0}: Payment against Sales/Purchase Order should always be marked as advance,"Zeile {0}: ""Zahlung zu Auftrag bzw. Bestellung"" sollte immer als ""Vorkasse"" eingestellt werden",
Row {0}: Please check 'Is Advance' against Account {1} if this is an advance entry.,"Zeile {0}: Wenn es sich um eine Vorkasse-Buchung handelt, bitte ""Ist Vorkasse"" zu Konto {1} anklicken, .",
Row {0}: Please set at Tax Exemption Reason in Sales Taxes and Charges,Zeile {0}: Bitte setzen Sie den Steuerbefreiungsgrund in den Umsatzsteuern und -gebühren,
Row {0}: Please set the Mode of Payment in Payment Schedule,Zeile {0}: Bitte legen Sie die Zahlungsart im Zahlungsplan fest,
@@ -2518,19 +2518,19 @@
Sales Account,Verkaufskonto,
Sales Expenses,Vertriebskosten,
Sales Funnel,Verkaufstrichter,
-Sales Invoice,Verkaufsrechnung,
+Sales Invoice,Ausgangsrechnung,
Sales Invoice {0} has already been submitted,Ausgangsrechnung {0} wurde bereits übertragen,
-Sales Invoice {0} must be cancelled before cancelling this Sales Order,Ausgangsrechnung {0} muss vor Stornierung dieses Kundenauftrags abgebrochen werden,
+Sales Invoice {0} must be cancelled before cancelling this Sales Order,Ausgangsrechnung {0} muss vor Stornierung dieses Auftrags abgebrochen werden,
Sales Manager,Vertriebsleiter,
Sales Master Manager,Hauptvertriebsleiter,
-Sales Order,Auftragsbestätigung,
-Sales Order Item,Kundenauftrags-Artikel,
-Sales Order required for Item {0},Kundenauftrag für den Artikel {0} erforderlich,
-Sales Order to Payment,Vom Kundenauftrag zum Zahlungseinang,
-Sales Order {0} is not submitted,Kundenauftrag {0} wurde nicht übertragen,
-Sales Order {0} is not valid,Kundenauftrag {0} ist nicht gültig,
-Sales Order {0} is {1},Kundenauftrag {0} ist {1},
-Sales Orders,Kundenaufträge,
+Sales Order,Auftrag,
+Sales Order Item,Auftrags-Artikel,
+Sales Order required for Item {0},Auftrag für den Artikel {0} erforderlich,
+Sales Order to Payment,Vom Auftrag zum Zahlungseinang,
+Sales Order {0} is not submitted,Auftrag {0} wurde nicht übertragen,
+Sales Order {0} is not valid,Auftrag {0} ist nicht gültig,
+Sales Order {0} is {1},Auftrag {0} ist {1},
+Sales Orders,Aufträge,
Sales Partner,Vertriebspartner,
Sales Pipeline,Vertriebspipeline,
Sales Price List,Verkaufspreisliste,
@@ -2541,7 +2541,7 @@
Sales User,Nutzer Vertrieb,
Sales and Returns,Verkauf und Retouren,
Sales campaigns.,Vertriebskampagnen,
-Sales orders are not available for production,Kundenaufträge sind für die Produktion nicht verfügbar,
+Sales orders are not available for production,Aufträge sind für die Produktion nicht verfügbar,
Salutation,Anrede,
Same Company is entered more than once,Das selbe Unternehmen wurde mehrfach angegeben,
Same item cannot be entered multiple times.,Das gleiche Einzelteil kann nicht mehrfach eingegeben werden.,
@@ -2650,7 +2650,7 @@
Serial No {0} not in stock,Seriennummer {0} ist nicht auf Lager,
Serial No {0} quantity {1} cannot be a fraction,Seriennummer {0} mit Menge {1} kann nicht eine Teilmenge sein,
Serial Nos Required for Serialized Item {0},Seriennummern sind erforderlich für den Artikel mit Seriennummer {0},
-Serial Number: {0} is already referenced in Sales Invoice: {1},Seriennummer: {0} wird bereits in der Verkaufsrechnung referenziert: {1},
+Serial Number: {0} is already referenced in Sales Invoice: {1},Seriennummer: {0} wird bereits in der Ausgangsrechnung referenziert: {1},
Serial Numbers,Seriennummer,
Serial Numbers in row {0} does not match with Delivery Note,Seriennummern in Zeile {0} stimmt nicht mit der Lieferschein überein,
Serial no {0} has been already returned,Seriennr. {0} wurde bereits zurückgegeben,
@@ -3278,7 +3278,7 @@
Warning: Invalid attachment {0},Warnung: Ungültige Anlage {0},
Warning: Leave application contains following block dates,Achtung: Die Urlaubsverwaltung enthält die folgenden gesperrten Daten,
Warning: Material Requested Qty is less than Minimum Order Qty,Achtung : Materialanfragemenge ist geringer als die Mindestbestellmenge,
-Warning: Sales Order {0} already exists against Customer's Purchase Order {1},Warnung: Kundenauftrag {0} zu Kunden-Bestellung bereits vorhanden {1},
+Warning: Sales Order {0} already exists against Customer's Purchase Order {1},Warnung: Auftrag {0} zu Kunden-Bestellung bereits vorhanden {1},
Warning: System will not check overbilling since amount for Item {0} in {1} is zero,"Achtung: Das System erkennt keine überhöhten Rechnungen, da der Betrag für Artikel {0} in {1} gleich Null ist",
Warranty,Garantie,
Warranty Claim,Garantieanspruch,
@@ -3308,7 +3308,7 @@
Work Order cannot be raised against a Item Template,Arbeitsauftrag kann nicht gegen eine Artikelbeschreibungsvorlage ausgelöst werden,
Work Order has been {0},Arbeitsauftrag wurde {0},
Work Order not created,Arbeitsauftrag wurde nicht erstellt,
-Work Order {0} must be cancelled before cancelling this Sales Order,Der Arbeitsauftrag {0} muss vor dem Stornieren dieses Kundenauftrags storniert werden,
+Work Order {0} must be cancelled before cancelling this Sales Order,Der Arbeitsauftrag {0} muss vor dem Stornieren dieses Auftrags storniert werden,
Work Order {0} must be submitted,Arbeitsauftrag {0} muss eingereicht werden,
Work Orders Created: {0},Arbeitsaufträge erstellt: {0},
Work Summary for {0},Arbeitszusammenfassung für {0},
@@ -3382,9 +3382,9 @@
{0} Student Groups created.,{0} Schülergruppen erstellt.,
{0} Students have been enrolled,{0} Studenten wurden angemeldet,
{0} against Bill {1} dated {2},{0} zu Rechnung {1} vom {2},
-{0} against Purchase Order {1},{0} zu Lieferantenauftrag {1},
-{0} against Sales Invoice {1},{0} zu Verkaufsrechnung {1},
-{0} against Sales Order {1},{0} zu Kundenauftrag{1},
+{0} against Purchase Order {1},{0} zu Bestellung {1},
+{0} against Sales Invoice {1},{0} zu Ausgangsrechnung {1},
+{0} against Sales Order {1},{0} zu Auftrag{1},
{0} already allocated for Employee {1} for period {2} to {3},{0} bereits an Mitarbeiter {1} zugeteilt für den Zeitraum {2} bis {3},
{0} applicable after {1} working days,{0} gilt nach {1} Werktagen,
{0} asset cannot be transferred,{0} Anlagevermögen kann nicht übertragen werden,
@@ -3833,7 +3833,7 @@
Log Type is required for check-ins falling in the shift: {0}.,Der Protokolltyp ist für Eincheckvorgänge in der Schicht erforderlich: {0}.,
Looks like someone sent you to an incomplete URL. Please ask them to look into it.,"Sieht aus wie jemand, den Sie zu einer unvollständigen URL gesendet. Bitte fragen Sie sie, sich in sie.",
Make Journal Entry,Buchungssatz erstellen,
-Make Purchase Invoice,Einkaufsrechnung erstellen,
+Make Purchase Invoice,Eingangsrechnung erstellen,
Manufactured,Hergestellt,
Mark Work From Home,Markieren Sie Work From Home,
Master,Vorlage,
@@ -4302,7 +4302,7 @@
Assets not created for {0}. You will have to create asset manually.,Assets nicht für {0} erstellt. Sie müssen das Asset manuell erstellen.,
{0} {1} has accounting entries in currency {2} for company {3}. Please select a receivable or payable account with currency {2}.,{0} {1} hat Buchhaltungseinträge in Währung {2} für Firma {3}. Bitte wählen Sie ein Debitoren- oder Kreditorenkonto mit der Währung {2} aus.,
Invalid Account,Ungültiger Account,
-Purchase Order Required,Lieferantenauftrag erforderlich,
+Purchase Order Required,Bestellung erforderlich,
Purchase Receipt Required,Kaufbeleg notwendig,
Account Missing,Konto fehlt,
Requested,Angefordert,
@@ -4889,7 +4889,7 @@
Subscription Plans,Abonnementpläne,
SWIFT Number,SWIFT-Nummer,
Recipient Message And Payment Details,Empfänger der Nachricht und Zahlungsdetails,
-Make Sales Invoice,Verkaufsrechnung erstellen,
+Make Sales Invoice,Ausgangsrechnung erstellen,
Mute Email,Mute Email,
payment_url,payment_url,
Payment Gateway Details,Payment Gateway-Details,
@@ -4988,7 +4988,7 @@
Accounting Dimensions ,Buchhaltung Dimensionen,
Supplier Invoice Details,Lieferant Rechnungsdetails,
Supplier Invoice Date,Lieferantenrechnungsdatum,
-Return Against Purchase Invoice,Zurück zur Einkaufsrechnung,
+Return Against Purchase Invoice,Zurück zur Eingangsrechnung,
Select Supplier Address,Lieferantenadresse auswählen,
Contact Person,Kontaktperson,
Select Shipping Address,Lieferadresse auswählen,
@@ -5081,7 +5081,7 @@
Allow Zero Valuation Rate,Nullbewertung zulassen,
Item Tax Rate,Artikelsteuersatz,
Tax detail table fetched from item master as a string and stored in this field.\nUsed for Taxes and Charges,Die Tabelle Steuerdetails wird aus dem Artikelstamm als Zeichenfolge entnommen und in diesem Feld gespeichert. Wird verwendet für Steuern und Abgaben,
-Purchase Order Item,Lieferantenauftrags-Artikel,
+Purchase Order Item,Bestellartikel,
Purchase Receipt Detail,Kaufbelegdetail,
Item Weight Details,Artikel Gewicht Details,
Weight Per Unit,Gewicht pro Einheit,
@@ -5110,10 +5110,10 @@
Offline POS Name,Offline-Verkaufsstellen-Name,
Is Return (Credit Note),ist Rücklieferung (Gutschrift),
Return Against Sales Invoice,Zurück zur Kundenrechnung,
-Update Billed Amount in Sales Order,Aktualisierung des Rechnungsbetrags im Kundenauftrag,
-Customer PO Details,Kundenauftragsdetails,
-Customer's Purchase Order,Kundenauftrag,
-Customer's Purchase Order Date,Kundenauftragsdatum,
+Update Billed Amount in Sales Order,Aktualisierung des Rechnungsbetrags im Auftrag,
+Customer PO Details,Auftragsdetails,
+Customer's Purchase Order,Bestellung des Kunden,
+Customer's Purchase Order Date,Bestelldatum des Kunden,
Customer Address,Kundenadresse,
Shipping Address Name,Lieferadresse Bezeichnung,
Company Address Name,Bezeichnung der Anschrift des Unternehmens,
@@ -5483,7 +5483,7 @@
Supply Raw Materials,Rohmaterial bereitstellen,
Purchase Order Pricing Rule,Preisregel für Bestellungen,
Set Reserve Warehouse,Legen Sie das Reservelager fest,
-In Words will be visible once you save the Purchase Order.,"""In Worten"" wird sichtbar, sobald Sie den Lieferantenauftrag speichern.",
+In Words will be visible once you save the Purchase Order.,"""In Worten"" wird sichtbar, sobald Sie die Bestellung speichern.",
Advance Paid,Angezahlt,
Tracking,Verfolgung,
% Billed,% verrechnet,
@@ -5500,7 +5500,7 @@
Blanket Order,Blankoauftrag,
Blanket Order Rate,Pauschale Bestellrate,
Returned Qty,Zurückgegebene Menge,
-Purchase Order Item Supplied,Lieferantenauftrags-Artikel geliefert,
+Purchase Order Item Supplied,Bestellartikel geliefert,
BOM Detail No,Stückliste Detailnr.,
Stock Uom,Lagermaßeinheit,
Raw Material Item Code,Rohmaterial-Artikelnummer,
@@ -6011,7 +6011,7 @@
Sync Products,Produkte synchronisieren,
Always sync your products from Amazon MWS before synching the Orders details,"Synchronisieren Sie Ihre Produkte immer mit Amazon MWS, bevor Sie die Bestelldetails synchronisieren",
Sync Orders,Bestellungen synchronisieren,
-Click this button to pull your Sales Order data from Amazon MWS.,"Klicken Sie auf diese Schaltfläche, um Ihre Kundenauftragsdaten von Amazon MWS abzurufen.",
+Click this button to pull your Sales Order data from Amazon MWS.,"Klicken Sie auf diese Schaltfläche, um Ihre Auftragsdaten von Amazon MWS abzurufen.",
Enable Scheduled Sync,Aktivieren Sie die geplante Synchronisierung,
Check this to enable a scheduled Daily synchronization routine via scheduler,"Aktivieren Sie diese Option, um eine geplante tägliche Synchronisierungsroutine über den Scheduler zu aktivieren",
Max Retry Limit,Max. Wiederholungslimit,
@@ -6060,14 +6060,14 @@
Default Customer,Standardkunde,
Customer Group will set to selected group while syncing customers from Shopify,Die Kundengruppe wird bei der Synchronisierung von Kunden von Shopify auf die ausgewählte Gruppe festgelegt,
For Company,Für Unternehmen,
-Cash Account will used for Sales Invoice creation,Cash Account wird für die Erstellung von Verkaufsrechnungen verwendet,
+Cash Account will used for Sales Invoice creation,Cash Account wird für die Erstellung von Ausgangsrechnungen verwendet,
Update Price from Shopify To ERPNext Price List,Preis von Shopify auf ERPNext Preisliste aktualisieren,
-Default Warehouse to to create Sales Order and Delivery Note,Standard Warehouse zum Erstellen von Kundenauftrag und Lieferschein,
-Sales Order Series,Kundenauftragsreihen,
+Default Warehouse to to create Sales Order and Delivery Note,Standard Lager zum Erstellen von Auftrag und Lieferschein,
+Sales Order Series,Auftragsnummernkreis,
Import Delivery Notes from Shopify on Shipment,Lieferscheine von Shopify bei Versand importieren,
Delivery Note Series,Lieferschein-Serie,
-Import Sales Invoice from Shopify if Payment is marked,"Verkaufsrechnung aus Shopify importieren, wenn Zahlung markiert ist",
-Sales Invoice Series,Verkaufsrechnung Serie,
+Import Sales Invoice from Shopify if Payment is marked,"Ausgangsrechnung aus Shopify importieren, wenn Zahlung markiert ist",
+Sales Invoice Series,Nummernkreis Ausgangsrechnung,
Shopify Tax Account,Steuerkonto erstellen,
Shopify Tax/Shipping Title,Steuern / Versand Titel,
ERPNext Account,ERPNext Konto,
@@ -6106,13 +6106,13 @@
Tax Account,Steuerkonto,
Freight and Forwarding Account,Fracht- und Speditionskonto,
Creation User,Erstellungsbenutzer,
-"The user that will be used to create Customers, Items and Sales Orders. This user should have the relevant permissions.","Der Benutzer, der zum Erstellen von Kunden, Artikeln und Kundenaufträgen verwendet wird. Dieser Benutzer sollte über die entsprechenden Berechtigungen verfügen.",
-"This warehouse will be used to create Sales Orders. The fallback warehouse is ""Stores"".",Dieses Lager wird zum Erstellen von Kundenaufträgen verwendet. Das Fallback-Lager ist "Stores".,
+"The user that will be used to create Customers, Items and Sales Orders. This user should have the relevant permissions.","Der Benutzer, der zum Erstellen von Kunden, Artikeln und Aufträgen verwendet wird. Dieser Benutzer sollte über die entsprechenden Berechtigungen verfügen.",
+"This warehouse will be used to create Sales Orders. The fallback warehouse is ""Stores"".",Dieses Lager wird zum Erstellen von Aufträgen verwendet. Das Fallback-Lager ist "Stores".,
"The fallback series is ""SO-WOO-"".",Die Fallback-Serie heißt "SO-WOO-".,
-This company will be used to create Sales Orders.,Diese Firma wird zum Erstellen von Kundenaufträgen verwendet.,
+This company will be used to create Sales Orders.,Diese Firma wird zum Erstellen von Aufträgen verwendet.,
Delivery After (Days),Lieferung nach (Tage),
-This is the default offset (days) for the Delivery Date in Sales Orders. The fallback offset is 7 days from the order placement date.,Dies ist der Standardversatz (Tage) für das Lieferdatum in Kundenaufträgen. Der Fallback-Offset beträgt 7 Tage ab Bestelldatum.,
-"This is the default UOM used for items and Sales orders. The fallback UOM is ""Nos"".","Dies ist die Standard-ME, die für Artikel und Kundenaufträge verwendet wird. Die Fallback-UOM lautet "Nos".",
+This is the default offset (days) for the Delivery Date in Sales Orders. The fallback offset is 7 days from the order placement date.,Dies ist der Standardversatz (Tage) für das Lieferdatum in Aufträgen. Der Fallback-Offset beträgt 7 Tage ab Bestelldatum.,
+"This is the default UOM used for items and Sales orders. The fallback UOM is ""Nos"".","Dies ist die Standard-ME, die für Artikel und Aufträge verwendet wird. Die Fallback-UOM lautet "Nos".",
Endpoints,Endpunkte,
Endpoint,Endpunkt,
Antibiotic Name,Antibiotika-Name,
@@ -6230,8 +6230,8 @@
Reminder Message,Erinnerungsmeldung,
Remind Before,Vorher erinnern,
Laboratory Settings,Laboreinstellungen,
-Create Lab Test(s) on Sales Invoice Submission,Erstellen Sie Labortests für die Übermittlung von Verkaufsrechnungen,
-Checking this will create Lab Test(s) specified in the Sales Invoice on submission.,"Wenn Sie dies aktivieren, werden Labortests erstellt, die bei der Übermittlung in der Verkaufsrechnung angegeben sind.",
+Create Lab Test(s) on Sales Invoice Submission,Erstellen Sie Labortests für die Übermittlung von Ausgangsrechnungen,
+Checking this will create Lab Test(s) specified in the Sales Invoice on submission.,"Wenn Sie dies aktivieren, werden Labortests erstellt, die bei der Übermittlung in der Ausgangsrechnung angegeben sind.",
Create Sample Collection document for Lab Test,Erstellen Sie ein Probensammeldokument für den Labortest,
Checking this will create a Sample Collection document every time you create a Lab Test,"Wenn Sie dies aktivieren, wird jedes Mal, wenn Sie einen Labortest erstellen, ein Probensammeldokument erstellt",
Employee name and designation in print,Name und Bezeichnung des Mitarbeiters im Druck,
@@ -6315,7 +6315,7 @@
Get Prescribed Therapies,Holen Sie sich verschriebene Therapien,
Appointment Datetime,Termin Datum / Uhrzeit,
Duration (In Minutes),Dauer (in Minuten),
-Reference Sales Invoice,Referenzverkaufsrechnung,
+Reference Sales Invoice,Referenzausgangsrechnung,
More Info,Weitere Informationen,
Referring Practitioner,Überweisender Praktiker,
Reminded,Erinnert,
@@ -7268,7 +7268,7 @@
Default Work In Progress Warehouse,Standard-Fertigungslager,
Default Finished Goods Warehouse,Standard-Fertigwarenlager,
Default Scrap Warehouse,Standard-Schrottlager,
-Overproduction Percentage For Sales Order,Überproduktionsprozentsatz für Kundenauftrag,
+Overproduction Percentage For Sales Order,Überproduktionsprozentsatz für Auftrag,
Overproduction Percentage For Work Order,Überproduktionsprozentsatz für Arbeitsauftrag,
Other Settings,Weitere Einstellungen,
Update BOM Cost Automatically,Stücklisten-Kosten automatisch aktualisieren,
@@ -7281,7 +7281,7 @@
Production Plan,Produktionsplan,
MFG-PP-.YYYY.-,MFG-PP-.YYYY.-,
Get Items From,Holen Sie Elemente aus,
-Get Sales Orders,Kundenaufträge aufrufen,
+Get Sales Orders,Aufträge aufrufen,
Material Request Detail,Materialanforderungsdetail,
Get Material Request,Get-Material anfordern,
Material Requests,Materialwünsche,
@@ -7304,8 +7304,8 @@
material_request_item,material_request_item,
Product Bundle Item,Produkt-Bundle-Artikel,
Production Plan Material Request,Produktionsplan-Material anfordern,
-Production Plan Sales Order,Produktionsplan für Kundenauftrag,
-Sales Order Date,Kundenauftrags-Datum,
+Production Plan Sales Order,Produktionsplan für Auftrag,
+Sales Order Date,Auftragsdatum,
Routing Name,Routing-Name,
MFG-WO-.YYYY.-,MFG-WO-.YYYY.-,
Item To Manufacture,Zu fertigender Artikel,
@@ -7482,12 +7482,12 @@
Start and End Dates,Start- und Enddatum,
Actual Time (in Hours),Tatsächliche Zeit (in Stunden),
Costing and Billing,Kalkulation und Abrechnung,
-Total Costing Amount (via Timesheets),Gesamtkalkulationsbetrag (über Arbeitszeittabellen),
-Total Expense Claim (via Expense Claims),Gesamtbetrag der Aufwandsabrechnung (über Aufwandsabrechnungen),
-Total Purchase Cost (via Purchase Invoice),Summe Einkaufskosten (über Einkaufsrechnung),
-Total Sales Amount (via Sales Order),Gesamtverkaufsbetrag (über Kundenauftrag),
-Total Billable Amount (via Timesheets),Gesamter abrechenbarer Betrag (über Arbeitszeittabellen),
-Total Billed Amount (via Sales Invoices),Gesamtabrechnungsbetrag (über Verkaufsrechnungen),
+Total Costing Amount (via Timesheets),Gesamtkalkulationsbetrag (über Zeiterfassung),
+Total Expense Claim (via Expense Claims),Gesamtbetrag der Auslagenabrechnung (über Auslagenabrechnungen),
+Total Purchase Cost (via Purchase Invoice),Summe Einkaufskosten (über Eingangsrechnung),
+Total Sales Amount (via Sales Order),Auftragssumme (über Auftrag),
+Total Billable Amount (via Timesheets),Abrechenbare Summe (über Zeiterfassung),
+Total Billed Amount (via Sales Invoices),Abgerechnete Summe (über Ausgangsrechnungen),
Total Consumed Material Cost (via Stock Entry),Summe der verbrauchten Materialkosten (über die Bestandsbuchung),
Gross Margin,Handelsspanne,
Gross Margin %,Handelsspanne %,
@@ -7497,9 +7497,9 @@
Twice Daily,Zweimal täglich,
First Email,Erste E-Mail,
Second Email,Zweite E-Mail,
-Time to send,Zeit zu senden,
-Day to Send,Tag zum Senden,
-Message will be sent to the users to get their status on the Project,"Es wird eine Nachricht an die Benutzer gesendet, um deren Status für das Projekt zu erhalten",
+Time to send,Sendezeit,
+Day to Send,Sendetag,
+Message will be sent to the users to get their status on the Project,"Es wird eine Nachricht an die Benutzer gesendet, um über den Projektstatus zu informieren",
Projects Manager,Projektleiter,
Project Template,Projektvorlage,
Project Template Task,Projektvorlagenaufgabe,
@@ -7518,27 +7518,27 @@
Expected Time (in hours),Voraussichtliche Zeit (in Stunden),
% Progress,% Fortschritt,
Is Milestone,Ist Meilenstein,
-Task Description,Aufgabenbeschreibung,
+Task Description,Vorgangsbeschreibung,
Dependencies,Abhängigkeiten,
-Dependent Tasks,Abhängige Aufgaben,
+Dependent Tasks,Abhängige Vorgänge,
Depends on Tasks,Abhängig von Vorgang,
Actual Start Date (via Time Sheet),Das tatsächliche Startdatum (durch Zeiterfassung),
Actual Time (in hours),Tatsächliche Zeit (in Stunden),
Actual End Date (via Time Sheet),Das tatsächliche Enddatum (durch Zeiterfassung),
-Total Costing Amount (via Time Sheet),Gesamtkostenbetrag (über Arbeitszeitblatt),
-Total Expense Claim (via Expense Claim),Gesamtbetrag der Aufwandsabrechnung (über Aufwandsabrechnung),
-Total Billing Amount (via Time Sheet),Gesamtrechnungsbetrag (über Arbeitszeitblatt),
+Total Costing Amount (via Time Sheet),Gesamtkosten (über Zeiterfassung),
+Total Expense Claim (via Expense Claim),Summe der Auslagen (über Auslagenabrechnung),
+Total Billing Amount (via Time Sheet),Gesamtrechnungsbetrag (über Zeiterfassung),
Review Date,Überprüfungsdatum,
Closing Date,Abschlussdatum,
Task Depends On,Vorgang hängt ab von,
-Task Type,Aufgabentyp,
+Task Type,Vorgangstyp,
TS-.YYYY.-,TS-.YYYY.-,
Employee Detail,Mitarbeiterdetails,
Billing Details,Rechnungsdetails,
-Total Billable Hours,Insgesamt abrechenbare Stunden,
-Total Billed Hours,Insgesamt Angekündigt Stunden,
+Total Billable Hours,Summe abrechenbare Stunden,
+Total Billed Hours,Summe abgerechneter Stunden,
Total Costing Amount,Gesamtkalkulation Betrag,
-Total Billable Amount,Insgesamt abrechenbare Betrag,
+Total Billable Amount,Summe abrechenbarer Betrag,
Total Billed Amount,Gesamtrechnungsbetrag,
% Amount Billed,% des Betrages berechnet,
Hrs,Std,
@@ -7555,10 +7555,10 @@
Monitoring Frequency,Überwachungsfrequenz,
Weekday,Wochentag,
Objectives,Ziele,
-Quality Goal Objective,Qualitätsziel Ziel,
+Quality Goal Objective,Qualitätsziel,
Objective,Zielsetzung,
Agenda,Agenda,
-Minutes,Protokoll,
+Minutes,Protokolle,
Quality Meeting Agenda,Qualitätstreffen Agenda,
Quality Meeting Minutes,Qualitätssitzungsprotokoll,
Minute,Minute,
@@ -7627,7 +7627,7 @@
Restaurant Order Entry,Restaurantbestellung,
Restaurant Table,Restaurant-Tisch,
Click Enter To Add,Klicken Sie zum Hinzufügen auf Hinzufügen.,
-Last Sales Invoice,Letzte Verkaufsrechnung,
+Last Sales Invoice,Letzte Ausgangsrechnung,
Current Order,Aktueller Auftrag,
Restaurant Order Entry Item,Restaurantbestellzugangsposten,
Served,Serviert,
@@ -7639,7 +7639,7 @@
Reservation End Time,Reservierungsendzeit,
No of Seats,Anzahl der Sitze,
Minimum Seating,Mindestbestuhlung,
-"Keep Track of Sales Campaigns. Keep track of Leads, Quotations, Sales Order etc from Campaigns to gauge Return on Investment. ","Verkaufskampagne verfolgen: Leads, Angebote, Kundenaufträge usw. von Kampagnen beobachten um die Kapitalverzinsung (RoI) zu messen.",
+"Keep Track of Sales Campaigns. Keep track of Leads, Quotations, Sales Order etc from Campaigns to gauge Return on Investment. ","Verkaufskampagne verfolgen: Leads, Angebote, Aufträge usw. von Kampagnen beobachten um die Kapitalverzinsung (RoI) zu messen.",
SAL-CAM-.YYYY.-,SAL-CAM-.YYYY.-,
Campaign Schedules,Kampagnenpläne,
Buyer of Goods and Services.,Käufer von Waren und Dienstleistungen.,
@@ -7647,8 +7647,8 @@
Default Company Bank Account,Standard-Bankkonto des Unternehmens,
From Lead,Von Lead,
Account Manager,Buchhalter,
-Allow Sales Invoice Creation Without Sales Order,Ermöglichen Sie die Erstellung von Kundenrechnungen ohne Kundenauftrag,
-Allow Sales Invoice Creation Without Delivery Note,Ermöglichen Sie die Erstellung einer Verkaufsrechnung ohne Lieferschein,
+Allow Sales Invoice Creation Without Sales Order,Ermöglichen Sie die Erstellung von Kundenrechnungen ohne Auftrag,
+Allow Sales Invoice Creation Without Delivery Note,Ermöglichen Sie die Erstellung einer Ausgangsrechnung ohne Lieferschein,
Default Price List,Standardpreisliste,
Primary Address and Contact Detail,Primäre Adresse und Kontaktdetails,
"Select, to make the customer searchable with these fields","Wählen Sie, um den Kunden mit diesen Feldern durchsuchbar zu machen",
@@ -7665,7 +7665,7 @@
Sales Team Details,Verkaufsteamdetails,
Customer POS id,Kunden-POS-ID,
Customer Credit Limit,Kundenkreditlimit,
-Bypass Credit Limit Check at Sales Order,Kreditlimitprüfung im Kundenauftrag umgehen,
+Bypass Credit Limit Check at Sales Order,Kreditlimitprüfung im Auftrag umgehen,
Industry Type,Wirtschaftsbranche,
MAT-INS-.YYYY.-,MAT-INS-.YYYY.-,
Installation Date,Datum der Installation,
@@ -7701,16 +7701,16 @@
Additional Notes,Zusätzliche Bemerkungen,
SAL-ORD-.YYYY.-,SAL-ORD-.YYYY.-,
Skip Delivery Note,Lieferschein überspringen,
-In Words will be visible once you save the Sales Order.,"""In Worten"" wird sichtbar, sobald Sie den Kundenauftrag speichern.",
-Track this Sales Order against any Project,Diesen Kundenauftrag in jedem Projekt nachverfolgen,
+In Words will be visible once you save the Sales Order.,"""In Worten"" wird sichtbar, sobald Sie den Auftrag speichern.",
+Track this Sales Order against any Project,Diesen Auftrag in jedem Projekt nachverfolgen,
Billing and Delivery Status,Abrechnungs- und Lieferstatus,
Not Delivered,Nicht geliefert,
Fully Delivered,Komplett geliefert,
Partly Delivered,Teilweise geliefert,
Not Applicable,Nicht andwendbar,
% Delivered,% geliefert,
-% of materials delivered against this Sales Order,% der für diesen Kundenauftrag gelieferten Materialien,
-% of materials billed against this Sales Order,% der Materialien welche zu diesem Kundenauftrag gebucht wurden,
+% of materials delivered against this Sales Order,% der für diesen Auftrag gelieferten Materialien,
+% of materials billed against this Sales Order,% der Materialien welche zu diesem Auftrag gebucht wurden,
Not Billed,Nicht abgerechnet,
Fully Billed,Voll berechnet,
Partly Billed,Teilweise abgerechnet,
@@ -7845,13 +7845,13 @@
Bank Credit Balance,Bankguthaben,
Receivables,Forderungen,
Payables,Verbindlichkeiten,
-Sales Orders to Bill,Kundenaufträge an Rechnung,
+Sales Orders to Bill,Aufträge an Rechnung,
Purchase Orders to Bill,Bestellungen an Rechnung,
-New Sales Orders,Neue Kundenaufträge,
+New Sales Orders,Neue Aufträge,
New Purchase Orders,Neue Bestellungen an Lieferanten,
-Sales Orders to Deliver,Kundenaufträge zu liefern,
-Purchase Orders to Receive,Bestellungen zu empfangen,
-New Purchase Invoice,Neue Kaufrechnung,
+Sales Orders to Deliver,Auszuliefernde Aufträge,
+Purchase Orders to Receive,Anzuliefernde Bestellungen,
+New Purchase Invoice,Neue Eingangsrechnung,
New Quotations,Neue Angebote,
Open Quotations,Angebote öffnen,
Open Issues,Offene Punkte,
@@ -7972,7 +7972,7 @@
Is Return,Ist Rückgabe,
Issue Credit Note,Gutschrift ausgeben,
Return Against Delivery Note,Zurück zum Lieferschein,
-Customer's Purchase Order No,Kundenauftragsnr.,
+Customer's Purchase Order No,Bestellnummer des Kunden,
Billing Address Name,Name der Rechnungsadresse,
Required only for sample item.,Nur erforderlich für Probeartikel.,
"If you have created a standard template in Sales Taxes and Charges Template, select one and click on the button below.","Wenn eine Standardvorlage unter den Vorlagen ""Steuern und Abgaben beim Verkauf"" erstellt wurde, bitte eine Vorlage auswählen und auf die Schaltfläche unten klicken.",
@@ -7989,8 +7989,8 @@
Excise Page Number,Seitenzahl entfernen,
Instructions,Anweisungen,
From Warehouse,Ab Lager,
-Against Sales Order,Zu Kundenauftrag,
-Against Sales Order Item,Zu Kundenauftrags-Position,
+Against Sales Order,Zu Auftrag,
+Against Sales Order Item,Zu Auftragsposition,
Against Sales Invoice,Zu Ausgangsrechnung,
Against Sales Invoice Item,Zu Ausgangsrechnungs-Position,
Available Batch Qty at From Warehouse,Verfügbare Chargenmenge im Ausgangslager,
@@ -8063,7 +8063,7 @@
"Example: ABCD.#####\nIf series is set and Serial No is not mentioned in transactions, then automatic serial number will be created based on this series. If you always want to explicitly mention Serial Nos for this item. leave this blank.","Beispiel: ABCD.##### \n Wenn ""Serie"" eingestellt ist und ""Seriennummer"" in den Transaktionen nicht aufgeführt ist, dann wird eine Seriennummer automatisch auf der Grundlage dieser Serie erstellt. Wenn immer explizit Seriennummern für diesen Artikel aufgeführt werden sollen, muss das Feld leer gelassen werden.",
Variants,Varianten,
Has Variants,Hat Varianten,
-"If this item has variants, then it cannot be selected in sales orders etc.","Wenn dieser Artikel Varianten hat, dann kann er bei den Kundenaufträgen, etc. nicht ausgewählt werden",
+"If this item has variants, then it cannot be selected in sales orders etc.","Wenn dieser Artikel Varianten hat, dann kann er bei den Aufträgen, etc. nicht ausgewählt werden",
Variant Based On,Variante basierend auf,
Item Attribute,Artikelattribut,
"Sales, Purchase, Accounting Defaults","Verkauf, Einkauf, Buchhaltungsvorgaben",
@@ -8529,7 +8529,7 @@
Qty to Deliver,Zu liefernde Menge,
Patient Appointment Analytics,Analyse von Patiententerminen,
Payment Period Based On Invoice Date,Zahlungszeitraum basierend auf Rechnungsdatum,
-Pending SO Items For Purchase Request,Ausstehende Artikel aus Kundenaufträgen für Lieferantenanfrage,
+Pending SO Items For Purchase Request,Ausstehende Artikel aus Aufträgen für Lieferantenanfrage,
Procurement Tracker,Beschaffungs-Tracker,
Product Bundle Balance,Produkt-Bundle-Balance,
Production Analytics,Produktions-Analysen,
@@ -8544,7 +8544,7 @@
Qty to Receive,Anzunehmende Menge,
Received Qty Amount,Erhaltene Menge Menge,
Billed Qty,Rechnungsmenge,
-Purchase Order Trends,Entwicklung Lieferantenaufträge,
+Purchase Order Trends,Entwicklung Bestellungen,
Purchase Receipt Trends,Trendanalyse Kaufbelege,
Purchase Register,Übersicht über Einkäufe,
Quotation Trends,Trendanalyse Angebote,
@@ -8555,7 +8555,7 @@
Salary Register,Gehalt Register,
Sales Analytics,Vertriebsanalyse,
Sales Invoice Trends,Ausgangsrechnung-Trendanalyse,
-Sales Order Trends,Trendanalyse Kundenaufträge,
+Sales Order Trends,Trendanalyse Aufträge,
Sales Partner Commission Summary,Zusammenfassung der Vertriebspartnerprovision,
Sales Partner Target Variance based on Item Group,Zielabweichung des Vertriebspartners basierend auf Artikelgruppe,
Sales Partner Transaction Summary,Sales Partner Transaction Summary,
@@ -8706,7 +8706,7 @@
Reference Detail No,Referenz Detail Nr,
Custom Remarks,Benutzerdefinierte Bemerkungen,
Please select a Company first.,Bitte wählen Sie zuerst eine Firma aus.,
-"Row #{0}: Reference Document Type must be one of Sales Order, Sales Invoice, Journal Entry or Dunning","Zeile # {0}: Der Referenzdokumenttyp muss Kundenauftrag, Verkaufsrechnung, Journaleintrag oder Mahnwesen sein",
+"Row #{0}: Reference Document Type must be one of Sales Order, Sales Invoice, Journal Entry or Dunning","Zeile # {0}: Der Referenzdokumenttyp muss Auftrag, Ausgangsrechnung, Journaleintrag oder Mahnwesen sein",
POS Closing Entry,POS Closing Entry,
POS Opening Entry,POS-Eröffnungseintrag,
POS Transactions,POS-Transaktionen,
@@ -8716,7 +8716,7 @@
POS Closing Entry Taxes,POS Closing Entry Taxes,
POS Invoice,POS-Rechnung,
ACC-PSINV-.YYYY.-,ACC-PSINV-.YYYY.-,
-Consolidated Sales Invoice,Konsolidierte Verkaufsrechnung,
+Consolidated Sales Invoice,Konsolidierte Ausgangsrechnung,
Return Against POS Invoice,Gegen POS-Rechnung zurücksenden,
Consolidated,Konsolidiert,
POS Invoice Item,POS-Rechnungsposition,
@@ -8826,7 +8826,7 @@
"By default, the Supplier Name is set as per the Supplier Name entered. If you want Suppliers to be named by a ","Standardmäßig wird der Lieferantenname gemäß dem eingegebenen Lieferantennamen festgelegt. Wenn Sie möchten, dass Lieferanten von a benannt werden",
choose the 'Naming Series' option.,Wählen Sie die Option "Naming Series".,
Configure the default Price List when creating a new Purchase transaction. Item prices will be fetched from this Price List.,Konfigurieren Sie die Standardpreisliste beim Erstellen einer neuen Kauftransaktion. Artikelpreise werden aus dieser Preisliste abgerufen.,
-"If this option is configured 'Yes', ERPNext will prevent you from creating a Purchase Invoice or Receipt without creating a Purchase Order first. This configuration can be overridden for a particular supplier by enabling the 'Allow Purchase Invoice Creation Without Purchase Order' checkbox in the Supplier master.","Wenn diese Option auf "Ja" konfiguriert ist, verhindert ERPNext, dass Sie eine Kaufrechnung oder einen Beleg erstellen können, ohne zuvor eine Bestellung zu erstellen. Diese Konfiguration kann für einen bestimmten Lieferanten überschrieben werden, indem das Kontrollkästchen "Erstellung von Einkaufsrechnungen ohne Bestellung zulassen" im Lieferantenstamm aktiviert wird.",
+"If this option is configured 'Yes', ERPNext will prevent you from creating a Purchase Invoice or Receipt without creating a Purchase Order first. This configuration can be overridden for a particular supplier by enabling the 'Allow Purchase Invoice Creation Without Purchase Order' checkbox in the Supplier master.","Wenn diese Option auf "Ja" konfiguriert ist, verhindert ERPNext, dass Sie eine Kaufrechnung oder einen Beleg erstellen können, ohne zuvor eine Bestellung zu erstellen. Diese Konfiguration kann für einen bestimmten Lieferanten überschrieben werden, indem das Kontrollkästchen "Erstellung von Eingangsrechnungen ohne Bestellung zulassen" im Lieferantenstamm aktiviert wird.",
"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.","Wenn diese Option auf "Ja" konfiguriert ist, verhindert ERPNext, dass Sie eine Kaufrechnung erstellen können, ohne zuvor einen Kaufbeleg zu erstellen. Diese Konfiguration kann für einen bestimmten Lieferanten überschrieben werden, indem das Kontrollkästchen "Erstellung von Kaufrechnungen ohne Kaufbeleg zulassen" im Lieferantenstamm aktiviert wird.",
Quantity & Stock,Menge & Lager,
Call Details,Anrufdetails,
@@ -8903,7 +8903,7 @@
"If checked, a customer will be created for every Patient. Patient Invoices will be created against this Customer. You can also select existing Customer while creating a Patient. This field is checked by default.","Wenn diese Option aktiviert ist, wird für jeden Patienten ein Kunde erstellt. Für diesen Kunden werden Patientenrechnungen erstellt. Sie können beim Erstellen eines Patienten auch einen vorhandenen Kunden auswählen. Dieses Feld ist standardmäßig aktiviert.",
Collect Registration Fee,Registrierungsgebühr sammeln,
"If your Healthcare facility bills registrations of Patients, you can check this and set the Registration Fee in the field below. Checking this will create new Patients with a Disabled status by default and will only be enabled after invoicing the Registration Fee.","Wenn Ihre Gesundheitseinrichtung die Registrierung von Patienten in Rechnung stellt, können Sie dies überprüfen und die Registrierungsgebühr im Feld unten festlegen. Wenn Sie dies aktivieren, werden standardmäßig neue Patienten mit dem Status "Deaktiviert" erstellt und erst nach Rechnungsstellung der Registrierungsgebühr aktiviert.",
-Checking this will automatically create a Sales Invoice whenever an appointment is booked for a Patient.,"Wenn Sie dies aktivieren, wird automatisch eine Verkaufsrechnung erstellt, wenn ein Termin für einen Patienten gebucht wird.",
+Checking this will automatically create a Sales Invoice whenever an appointment is booked for a Patient.,"Wenn Sie dies aktivieren, wird automatisch eine Ausgangsrechnung erstellt, wenn ein Termin für einen Patienten gebucht wird.",
Healthcare Service Items,Artikel im Gesundheitswesen,
"You can create a service item for Inpatient Visit Charge and set it here. Similarly, you can set up other Healthcare Service Items for billing in this section. Click ",Sie können ein Serviceelement für die Gebühr für stationäre Besuche erstellen und hier festlegen. Ebenso können Sie in diesem Abschnitt andere Gesundheitsposten für die Abrechnung einrichten. Klicken,
Set up default Accounts for the Healthcare Facility,Richten Sie Standardkonten für die Gesundheitseinrichtung ein,
@@ -8958,7 +8958,7 @@
Add New Line,Neue Zeile hinzufügen,
Secondary UOM,Sekundäre UOM,
"<b>Single</b>: Results which require only a single input.\n<br>\n<b>Compound</b>: Results which require multiple event inputs.\n<br>\n<b>Descriptive</b>: Tests which have multiple result components with manual result entry.\n<br>\n<b>Grouped</b>: Test templates which are a group of other test templates.\n<br>\n<b>No Result</b>: Tests with no results, can be ordered and billed but no Lab Test will be created. e.g.. Sub Tests for Grouped results","<b>Single</b> : Ergebnisse, die nur eine einzige Eingabe erfordern.<br> <b>Verbindung</b> : Ergebnisse, die mehrere Ereigniseingaben erfordern.<br> <b>Beschreibend</b> : Tests mit mehreren Ergebniskomponenten mit manueller Ergebniseingabe.<br> <b>Gruppiert</b> : <b>Testvorlagen,</b> die eine Gruppe anderer <b>Testvorlagen</b> sind.<br> <b>Kein Ergebnis</b> : Tests ohne Ergebnisse können bestellt und in Rechnung gestellt werden, es wird jedoch kein Labortest erstellt. z.B. Untertests für gruppierte Ergebnisse",
-"If unchecked, the item will not be available in Sales Invoices for billing but can be used in group test creation. ","Wenn diese Option deaktiviert ist, ist der Artikel in den Verkaufsrechnungen nicht zur Abrechnung verfügbar, kann jedoch für die Erstellung von Gruppentests verwendet werden.",
+"If unchecked, the item will not be available in Sales Invoices for billing but can be used in group test creation. ","Wenn diese Option deaktiviert ist, ist der Artikel in den Ausgangsrechnungen nicht zur Abrechnung verfügbar, kann jedoch für die Erstellung von Gruppentests verwendet werden.",
Description ,Beschreibung,
Descriptive Test,Beschreibender Test,
Group Tests,Gruppentests,
@@ -9084,8 +9084,8 @@
Manufacturing Section,Fertigungsabteilung,
"By default, the Customer Name is set as per the Full Name entered. If you want Customers to be named by a ","Standardmäßig wird der Kundenname gemäß dem eingegebenen vollständigen Namen festgelegt. Wenn Sie möchten, dass Kunden von a benannt werden",
Configure the default Price List when creating a new Sales transaction. Item prices will be fetched from this Price List.,Konfigurieren Sie die Standardpreisliste beim Erstellen einer neuen Verkaufstransaktion. Artikelpreise werden aus dieser Preisliste abgerufen.,
-"If this option is configured 'Yes', ERPNext will prevent you from creating a Sales Invoice or Delivery Note without creating a Sales Order first. This configuration can be overridden for a particular Customer by enabling the 'Allow Sales Invoice Creation Without Sales Order' checkbox in the Customer master.","Wenn diese Option auf "Ja" konfiguriert ist, verhindert ERPNext, dass Sie eine Verkaufsrechnung oder einen Lieferschein erstellen, ohne zuvor einen Kundenauftrag zu erstellen. Diese Konfiguration kann für einen bestimmten Kunden überschrieben werden, indem das Kontrollkästchen "Erstellung von Verkaufsrechnungen ohne Kundenauftrag zulassen" im Kundenstamm aktiviert wird.",
-"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.","Wenn diese Option auf "Ja" konfiguriert ist, verhindert ERPNext, dass Sie eine Verkaufsrechnung erstellen, ohne zuvor einen Lieferschein zu erstellen. Diese Konfiguration kann für einen bestimmten Kunden überschrieben werden, indem das Kontrollkästchen "Erstellung von Verkaufsrechnungen ohne Lieferschein zulassen" im Kundenstamm aktiviert wird.",
+"If this option is configured 'Yes', ERPNext will prevent you from creating a Sales Invoice or Delivery Note without creating a Sales Order first. This configuration can be overridden for a particular Customer by enabling the 'Allow Sales Invoice Creation Without Sales Order' checkbox in the Customer master.","Wenn diese Option auf "Ja" konfiguriert ist, verhindert ERPNext, dass Sie eine Ausgangsrechnung oder einen Lieferschein erstellen, ohne zuvor einen Auftrag zu erstellen. Diese Konfiguration kann für einen bestimmten Kunden überschrieben werden, indem das Kontrollkästchen "Erstellung von Ausgangsrechnung ohne Auftrag zulassen" im Kundenstamm aktiviert wird.",
+"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.","Wenn diese Option auf "Ja" konfiguriert ist, verhindert ERPNext, dass Sie eine Ausgangsrechnung erstellen, ohne zuvor einen Lieferschein zu erstellen. Diese Konfiguration kann für einen bestimmten Kunden überschrieben werden, indem das Kontrollkästchen "Erstellung von Ausgangsrechnungen ohne Lieferschein zulassen" im Kundenstamm aktiviert wird.",
Default Warehouse for Sales Return,Standardlager für Retouren,
Default In Transit Warehouse,Standard im Transit Warehouse,
Enable Perpetual Inventory For Non Stock Items,Aktivieren Sie das ewige Inventar für nicht vorrätige Artikel,
@@ -9114,7 +9114,7 @@
Choose between FIFO and Moving Average Valuation Methods. Click ,Wählen Sie zwischen FIFO- und Moving Average-Bewertungsmethoden. Klicken,
to know more about them.,um mehr über sie zu erfahren.,
Show 'Scan Barcode' field above every child table to insert Items with ease.,"Zeigen Sie das Feld "Barcode scannen" über jeder untergeordneten Tabelle an, um Elemente problemlos einzufügen.",
-"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.","Seriennummern für Lagerbestände werden automatisch basierend auf den Artikeln festgelegt, die basierend auf First-In-First-Out in Transaktionen wie Kauf- / Verkaufsrechnungen, Lieferscheinen usw. eingegeben wurden.",
+"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.","Seriennummern für Lagerbestände werden automatisch basierend auf den Artikeln festgelegt, die basierend auf First-In-First-Out in Transaktionen wie Ein- und Ausgangsrechnungen, Lieferscheinen usw. eingegeben wurden.",
"If blank, parent Warehouse Account or company default will be considered in transactions","Wenn leer, wird das übergeordnete Lagerkonto oder der Firmenstandard bei Transaktionen berücksichtigt",
Service Level Agreement Details,Details zum Service Level Agreement,
Service Level Agreement Status,Status des Service Level Agreements,
@@ -9263,10 +9263,10 @@
Account No,Konto Nr,
IFSC,IFSC,
MICR,MICR,
-Sales Order Analysis,Kundenauftragsanalyse,
+Sales Order Analysis,Auftragsanalyse,
Amount Delivered,Gelieferter Betrag,
Delay (in Days),Verzögerung (in Tagen),
-Group by Sales Order,Nach Kundenauftrag gruppieren,
+Group by Sales Order,Nach Auftrag gruppieren,
Sales Value,Verkaufswert,
Stock Qty vs Serial No Count,Lagermenge vs Seriennummer,
Serial No Count,Seriennummer nicht gezählt,
@@ -9456,8 +9456,8 @@
Based On Document,Basierend auf Dokument,
Based On Data ( in years ),Basierend auf Daten (in Jahren),
Smoothing Constant,Glättungskonstante,
-Please fill the Sales Orders table,Bitte füllen Sie die Tabelle Kundenaufträge aus,
-Sales Orders Required,Kundenaufträge erforderlich,
+Please fill the Sales Orders table,Bitte füllen Sie die Tabelle Aufträge aus,
+Sales Orders Required,Aufträge erforderlich,
Please fill the Material Requests table,Bitte füllen Sie die Materialanforderungstabelle aus,
Material Requests Required,Materialanforderungen erforderlich,
Items to Manufacture are required to pull the Raw Materials associated with it.,"Zu fertigende Gegenstände sind erforderlich, um die damit verbundenen Rohstoffe zu ziehen.",
@@ -9486,7 +9486,7 @@
Payroll date can not be greater than employee's relieving date.,Das Abrechnungsdatum darf nicht größer sein als das Entlastungsdatum des Mitarbeiters.,
Row #{0}: Please enter the result value for {1},Zeile # {0}: Bitte geben Sie den Ergebniswert für {1} ein,
Mandatory Results,Obligatorische Ergebnisse,
-Sales Invoice or Patient Encounter is required to create Lab Tests,Für die Erstellung von Labortests ist eine Verkaufsrechnung oder eine Patientenbegegnung erforderlich,
+Sales Invoice or Patient Encounter is required to create Lab Tests,Für die Erstellung von Labortests ist eine Ausgangsrechnung oder eine Patientenbegegnung erforderlich,
Insufficient Data,Unzureichende Daten,
Lab Test(s) {0} created successfully,Labortest (e) {0} erfolgreich erstellt,
Test :,Prüfung :,
@@ -9634,16 +9634,16 @@
Default: 10 mins,Standard: 10 Minuten,
Overproduction for Sales and Work Order,Überproduktion für Kunden- und Arbeitsauftrag,
"Update BOM cost automatically via scheduler, based on the latest Valuation Rate/Price List Rate/Last Purchase Rate of raw materials","Aktualisieren Sie die Stücklistenkosten automatisch über den Planer, basierend auf der neuesten Bewertungsrate / Preislistenrate / letzten Kaufrate der Rohstoffe",
-Purchase Order already created for all Sales Order items,Bestellung bereits für alle Kundenauftragspositionen angelegt,
+Purchase Order already created for all Sales Order items,Bestellung bereits für alle Auftragspositionen angelegt,
Select Items,Gegenstände auswählen,
Against Default Supplier,Gegen Standardlieferanten,
Auto close Opportunity after the no. of days mentioned above,Gelegenheit zum automatischen Schließen nach der Nr. der oben genannten Tage,
-Is Sales Order Required for Sales Invoice & Delivery Note Creation?,Ist ein Kundenauftrag für die Erstellung von Kundenrechnungen und Lieferscheinen erforderlich?,
-Is Delivery Note Required for Sales Invoice Creation?,Ist für die Erstellung der Verkaufsrechnung ein Lieferschein erforderlich?,
+Is Sales Order Required for Sales Invoice & Delivery Note Creation?,Ist ein Auftrag für die Erstellung von Kundenrechnungen und Lieferscheinen erforderlich?,
+Is Delivery Note Required for Sales Invoice Creation?,Ist für die Erstellung der Ausgangsrechnung ein Lieferschein erforderlich?,
How often should Project and Company be updated based on Sales Transactions?,Wie oft sollten Projekt und Unternehmen basierend auf Verkaufstransaktionen aktualisiert werden?,
Allow User to Edit Price List Rate in Transactions,Benutzer darf Preisliste in Transaktionen bearbeiten,
Allow Item to Be Added Multiple Times in a Transaction,"Zulassen, dass ein Element in einer Transaktion mehrmals hinzugefügt wird",
-Allow Multiple Sales Orders Against a Customer's Purchase Order,Erlauben Sie mehrere Kundenaufträge für die Bestellung eines Kunden,
+Allow Multiple Sales Orders Against a Customer's Purchase Order,Erlauben Sie mehrere Aufträge für die Bestellung eines Kunden,
Validate Selling Price for Item Against Purchase Rate or Valuation Rate,Überprüfen Sie den Verkaufspreis für den Artikel anhand der Kauf- oder Bewertungsrate,
Hide Customer's Tax ID from Sales Transactions,Steuer-ID des Kunden vor Verkaufstransaktionen ausblenden,
"The percentage you are allowed to receive or deliver more against the quantity ordered. For example, if you have ordered 100 units, and your Allowance is 10%, then you are allowed to receive 110 units.","Der Prozentsatz, den Sie mehr gegen die bestellte Menge erhalten oder liefern dürfen. Wenn Sie beispielsweise 100 Einheiten bestellt haben und Ihre Zulage 10% beträgt, können Sie 110 Einheiten erhalten.",
@@ -9653,8 +9653,8 @@
Set Qty in Transactions Based on Serial No Input,Stellen Sie die Menge in Transaktionen basierend auf Seriennummer ohne Eingabe ein,
Raise Material Request When Stock Reaches Re-order Level,"Erhöhen Sie die Materialanforderung, wenn der Lagerbestand die Nachbestellmenge erreicht",
Notify by Email on Creation of Automatic Material Request,Benachrichtigen Sie per E-Mail über die Erstellung einer automatischen Materialanforderung,
-Allow Material Transfer from Delivery Note to Sales Invoice,Materialübertragung vom Lieferschein zur Verkaufsrechnung zulassen,
-Allow Material Transfer from Purchase Receipt to Purchase Invoice,Materialübertragung vom Kaufbeleg zur Kaufrechnung zulassen,
+Allow Material Transfer from Delivery Note to Sales Invoice,Materialübertragung vom Lieferschein zur Ausgangsrechnung zulassen,
+Allow Material Transfer from Purchase Receipt to Purchase Invoice,Materialübertragung vom Kaufbeleg zur Eingangsrechnung zulassen,
Freeze Stocks Older Than (Days),Aktien einfrieren älter als (Tage),
Role Allowed to Edit Frozen Stock,Rolle darf eingefrorenes Material bearbeiten,
The unallocated amount of Payment Entry {0} is greater than the Bank Transaction's unallocated amount,Der nicht zugewiesene Betrag der Zahlungseingabe {0} ist größer als der nicht zugewiesene Betrag der Banküberweisung,
@@ -9694,7 +9694,7 @@
Error Occured,Fehler aufgetreten,
Opening Invoice Creation In Progress,Öffnen der Rechnungserstellung läuft,
Creating {} out of {} {},{} Aus {} {} erstellen,
-(Serial No: {0}) cannot be consumed as it's reserverd to fullfill Sales Order {1}.,"(Seriennummer: {0}) kann nicht verwendet werden, da es zum Ausfüllen des Kundenauftrags {1} reserviert ist.",
+(Serial No: {0}) cannot be consumed as it's reserverd to fullfill Sales Order {1}.,"(Seriennummer: {0}) kann nicht verwendet werden, da es zum Ausfüllen des Auftrags {1} reserviert ist.",
Item {0} {1},Gegenstand {0} {1},
Last Stock Transaction for item {0} under warehouse {1} was on {2}.,Die letzte Lagertransaktion für Artikel {0} unter Lager {1} war am {2}.,
Stock Transactions for Item {0} under warehouse {1} cannot be posted before this time.,Lagertransaktionen für Artikel {0} unter Lager {1} können nicht vor diesem Zeitpunkt gebucht werden.,
@@ -9822,8 +9822,8 @@
"If you {0} {1} worth item {2}, the scheme {3} will be applied on the item.","Wenn Sie {0} {1} Gegenstand {2} wert sind, wird das Schema {3} auf den Gegenstand angewendet.",
"As the field {0} is enabled, the field {1} is mandatory.","Da das Feld {0} aktiviert ist, ist das Feld {1} obligatorisch.",
"As the field {0} is enabled, the value of the field {1} should be more than 1.","Wenn das Feld {0} aktiviert ist, sollte der Wert des Feldes {1} größer als 1 sein.",
-Cannot deliver Serial No {0} of item {1} as it is reserved to fullfill Sales Order {2},"Die Seriennummer {0} von Artikel {1} kann nicht geliefert werden, da sie für die Erfüllung des Kundenauftrags {2} reserviert ist.",
-"Sales Order {0} has reservation for the item {1}, you can only deliver reserved {1} against {0}.","Kundenauftrag {0} hat eine Reservierung für den Artikel {1}, Sie können reservierte {1} nur gegen {0} liefern.",
+Cannot deliver Serial No {0} of item {1} as it is reserved to fullfill Sales Order {2},"Die Seriennummer {0} von Artikel {1} kann nicht geliefert werden, da sie für die Erfüllung des Auftrags {2} reserviert ist.",
+"Sales Order {0} has reservation for the item {1}, you can only deliver reserved {1} against {0}.","Auftrag {0} hat eine Reservierung für den Artikel {1}, Sie können reservierte {1} nur gegen {0} liefern.",
{0} Serial No {1} cannot be delivered,{0} Seriennummer {1} kann nicht zugestellt werden,
Row {0}: Subcontracted Item is mandatory for the raw material {1},Zeile {0}: Unterauftragsartikel sind für den Rohstoff {1} obligatorisch.,
"As there are sufficient raw materials, Material Request is not required for Warehouse {0}.","Da genügend Rohstoffe vorhanden sind, ist für Warehouse {0} keine Materialanforderung erforderlich.",
diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py
index 14b3afa..1d8b3a8 100644
--- a/erpnext/utilities/transaction_base.py
+++ b/erpnext/utilities/transaction_base.py
@@ -162,6 +162,28 @@
return ret
+ def reset_default_field_value(self, default_field: str, child_table: str, child_table_field: str):
+ """ Reset "Set default X" fields on forms to avoid confusion.
+
+ example:
+ doc = {
+ "set_from_warehouse": "Warehouse A",
+ "items": [{"from_warehouse": "warehouse B"}, {"from_warehouse": "warehouse A"}],
+ }
+ Since this has dissimilar values in child table, the default field will be erased.
+
+ doc.reset_default_field_value("set_from_warehouse", "items", "from_warehouse")
+ """
+ child_table_values = set()
+
+ for row in self.get(child_table):
+ child_table_values.add(row.get(child_table_field))
+
+ if len(child_table_values) > 1:
+ self.set(default_field, None)
+ else:
+ self.set(default_field, list(child_table_values)[0])
+
def delete_events(ref_type, ref_name):
events = frappe.db.sql_list(""" SELECT
distinct `tabEvent`.name
diff --git a/requirements.txt b/requirements.txt
index 4f7ff7b..39591ca 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,6 +1,6 @@
# frappe # https://github.com/frappe/frappe is installed during bench-init
gocardless-pro~=1.22.0
-googlemaps # used in ERPNext, but dependency is defined in Frappe
+googlemaps
pandas~=1.1.5
plaid-python~=7.2.1
pycountry~=20.7.3