diff --git a/.github/helper/.flake8_strict b/.github/helper/.flake8_strict index 198ec7bfe5..3e8f7dd11a 100644 --- a/.github/helper/.flake8_strict +++ b/.github/helper/.flake8_strict @@ -66,7 +66,8 @@ ignore = F841, E713, E712, - B023 + B023, + B028 max-line-length = 200 diff --git a/.github/helper/documentation.py b/.github/helper/documentation.py index 378983e95f..83346045f8 100644 --- a/.github/helper/documentation.py +++ b/.github/helper/documentation.py @@ -3,52 +3,71 @@ import requests from urllib.parse import urlparse -docs_repos = [ - "frappe_docs", - "erpnext_documentation", +WEBSITE_REPOS = [ "erpnext_com", "frappe_io", ] +DOCUMENTATION_DOMAINS = [ + "docs.erpnext.com", + "frappeframework.com", +] -def uri_validator(x): - result = urlparse(x) - return all([result.scheme, result.netloc, result.path]) -def docs_link_exists(body): - for line in body.splitlines(): - for word in line.split(): - if word.startswith('http') and uri_validator(word): - parsed_url = urlparse(word) - if parsed_url.netloc == "github.com": - parts = parsed_url.path.split('/') - if len(parts) == 5 and parts[1] == "frappe" and parts[2] in docs_repos: - return True - elif parsed_url.netloc == "docs.erpnext.com": - return True +def is_valid_url(url: str) -> bool: + parts = urlparse(url) + return all((parts.scheme, parts.netloc, parts.path)) + + +def is_documentation_link(word: str) -> bool: + if not word.startswith("http") or not is_valid_url(word): + return False + + parsed_url = urlparse(word) + if parsed_url.netloc in DOCUMENTATION_DOMAINS: + return True + + if parsed_url.netloc == "github.com": + parts = parsed_url.path.split("/") + if len(parts) == 5 and parts[1] == "frappe" and parts[2] in WEBSITE_REPOS: + return True + + return False + + +def contains_documentation_link(body: str) -> bool: + return any( + is_documentation_link(word) + for line in body.splitlines() + for word in line.split() + ) + + +def check_pull_request(number: str) -> "tuple[int, str]": + response = requests.get(f"https://api.github.com/repos/frappe/erpnext/pulls/{number}") + if not response.ok: + return 1, "Pull Request Not Found! ⚠️" + + payload = response.json() + title = (payload.get("title") or "").lower().strip() + head_sha = (payload.get("head") or {}).get("sha") + body = (payload.get("body") or "").lower() + + if ( + not title.startswith("feat") + or not head_sha + or "no-docs" in body + or "backport" in body + ): + return 0, "Skipping documentation checks... 🏃" + + if contains_documentation_link(body): + return 0, "Documentation Link Found. You're Awesome! 🎉" + + return 1, "Documentation Link Not Found! ⚠️" if __name__ == "__main__": - pr = sys.argv[1] - response = requests.get("https://api.github.com/repos/frappe/erpnext/pulls/{}".format(pr)) - - if response.ok: - payload = response.json() - title = (payload.get("title") or "").lower().strip() - head_sha = (payload.get("head") or {}).get("sha") - body = (payload.get("body") or "").lower() - - if (title.startswith("feat") - and head_sha - and "no-docs" not in body - and "backport" not in body - ): - if docs_link_exists(body): - print("Documentation Link Found. You're Awesome! 🎉") - - else: - print("Documentation Link Not Found! ⚠️") - sys.exit(1) - - else: - print("Skipping documentation checks... 🏃") + exit_code, message = check_pull_request(sys.argv[1]) + print(message) + sys.exit(exit_code) diff --git a/.github/helper/install.sh b/.github/helper/install.sh index 2bb950fcfc..0c71b41a7c 100644 --- a/.github/helper/install.sh +++ b/.github/helper/install.sh @@ -24,15 +24,14 @@ fi 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 -proot -e "SET GLOBAL character_set_server = 'utf8mb4'" + mysql --host 127.0.0.1 --port 3306 -u root -proot -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 -proot -e "CREATE USER 'test_frappe'@'localhost' IDENTIFIED BY 'test_frappe'" + mysql --host 127.0.0.1 --port 3306 -u root -proot -e "CREATE DATABASE test_frappe" + mysql --host 127.0.0.1 --port 3306 -u root -proot -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" + mysql --host 127.0.0.1 --port 3306 -u root -proot -e "FLUSH PRIVILEGES" fi if [ "$DB" == "postgres" ];then @@ -42,12 +41,17 @@ fi install_whktml() { - 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 - sudo mv /tmp/wkhtmltox/bin/wkhtmltopdf /usr/local/bin/wkhtmltopdf - sudo chmod o+x /usr/local/bin/wkhtmltopdf + if [ "$(lsb_release -rs)" = "22.04" ]; then + wget -O /tmp/wkhtmltox.deb https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-2/wkhtmltox_0.12.6.1-2.jammy_amd64.deb + sudo apt install /tmp/wkhtmltox.deb + else + echo "Please update this script to support wkhtmltopdf for $(lsb_release -ds)" + exit 1 + fi } install_whktml & +wkpid=$! + cd ~/frappe-bench || exit @@ -61,6 +65,8 @@ bench get-app erpnext "${GITHUB_WORKSPACE}" if [ "$TYPE" == "server" ]; then bench setup requirements --dev; fi +wait $wkpid + bench start &> bench_run_logs.txt & CI=Yes bench build --app frappe & bench --site test_site reinstall --yes diff --git a/.github/helper/site_config_mariadb.json b/.github/helper/site_config_mariadb.json index 948ad08bab..8c86f73729 100644 --- a/.github/helper/site_config_mariadb.json +++ b/.github/helper/site_config_mariadb.json @@ -9,8 +9,8 @@ "mail_password": "test", "admin_password": "admin", "root_login": "root", - "root_password": "travis", + "root_password": "root", "host_name": "http://test_site:8000", - "install_apps": ["erpnext"], + "install_apps": ["payments", "erpnext"], "throttle_user_limit": 100 } diff --git a/.github/workflows/initiate_release.yml b/.github/workflows/initiate_release.yml new file mode 100644 index 0000000000..ef38974ae2 --- /dev/null +++ b/.github/workflows/initiate_release.yml @@ -0,0 +1,32 @@ +# This workflow is agnostic to branches. Only maintain on develop branch. +# To add/remove versions just modify the matrix. + +name: Create weekly release pull requests +on: + schedule: + # 9:30 UTC => 3 PM IST Tuesday + - cron: "30 9 * * 2" + workflow_dispatch: + +jobs: + release: + name: Release + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + version: ["13", "14"] + + steps: + - uses: octokit/request-action@v2.x + with: + route: POST /repos/{owner}/{repo}/pulls + owner: frappe + repo: erpnext + title: |- + "chore: release v${{ matrix.version }}" + body: "Automated weekly release." + base: version-${{ matrix.version }} + head: version-${{ matrix.version }}-hotfix + env: + GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} diff --git a/.github/workflows/patch.yml b/.github/workflows/patch.yml index 4d2dc58677..d5f0052744 100644 --- a/.github/workflows/patch.yml +++ b/.github/workflows/patch.yml @@ -11,7 +11,7 @@ on: workflow_dispatch: concurrency: - group: patch-develop-${{ github.event.number }} + group: patch-develop-${{ github.event_name }}-${{ github.event.number || github.event_name == 'workflow_dispatch' && github.run_id || '' }} cancel-in-progress: true jobs: @@ -25,7 +25,7 @@ jobs: mysql: image: mariadb:10.3 env: - MYSQL_ALLOW_EMPTY_PASSWORD: YES + MARIADB_ROOT_PASSWORD: 'root' ports: - 3306:3306 options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5a46002820..37bb37e1d2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,10 +13,10 @@ jobs: with: fetch-depth: 0 persist-credentials: false - - name: Setup Node.js v14 + - name: Setup Node.js uses: actions/setup-node@v2 with: - node-version: 14 + node-version: 18 - name: Setup dependencies run: | npm install @semantic-release/git @semantic-release/exec --no-save @@ -28,4 +28,4 @@ jobs: GIT_AUTHOR_EMAIL: "developers@frappe.io" GIT_COMMITTER_NAME: "Frappe PR Bot" GIT_COMMITTER_EMAIL: "developers@frappe.io" - run: npx semantic-release \ No newline at end of file + run: npx semantic-release diff --git a/.github/workflows/server-tests-mariadb.yml b/.github/workflows/server-tests-mariadb.yml index 64134bc539..c70c76f65f 100644 --- a/.github/workflows/server-tests-mariadb.yml +++ b/.github/workflows/server-tests-mariadb.yml @@ -16,18 +16,18 @@ on: workflow_dispatch: inputs: user: - description: 'user' + description: 'Frappe Framework repository user (add your username for forks)' required: true default: 'frappe' type: string branch: - description: 'Branch name' + description: 'Frappe Framework branch' default: 'develop' required: false type: string concurrency: - group: server-mariadb-develop-${{ github.event.number }} + group: server-mariadb-develop-${{ github.event_name }}-${{ github.event.number || github.event_name == 'workflow_dispatch' && github.run_id || '' }} cancel-in-progress: true jobs: @@ -45,9 +45,9 @@ jobs: services: mysql: - image: mariadb:10.3 + image: mariadb:10.6 env: - MYSQL_ALLOW_EMPTY_PASSWORD: YES + MARIADB_ROOT_PASSWORD: 'root' ports: - 3306:3306 options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 @@ -59,7 +59,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v2 with: - python-version: '3.10' + python-version: '3.11' - name: Check for valid Python & Merge Conflicts run: | @@ -120,7 +120,7 @@ jobs: FRAPPE_BRANCH: ${{ github.event.inputs.branch }} - 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 --with-coverage --total-builds 4 --build-number ${{ matrix.container }}' env: TYPE: server CI_BUILD_ID: ${{ github.run_id }} diff --git a/.github/workflows/server-tests-postgres.yml b/.github/workflows/server-tests-postgres.yml index 651c935c15..df43801478 100644 --- a/.github/workflows/server-tests-postgres.yml +++ b/.github/workflows/server-tests-postgres.yml @@ -9,7 +9,7 @@ on: types: [opened, labelled, synchronize, reopened] concurrency: - group: server-postgres-develop-${{ github.event.number }} + group: server-postgres-develop-${{ github.event_name }}-${{ github.event.number || github.event_name == 'workflow_dispatch' && github.run_id || '' }} cancel-in-progress: true jobs: diff --git a/.mergify.yml b/.mergify.yml index d7f82e696b..c5f3d83ce6 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -9,6 +9,7 @@ pull_request_rules: - author!=nabinhait - author!=ankush - author!=deepeshgarg007 + - author!=frappe-pr-bot - author!=mergify[bot] - or: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index dc3011f050..d70977c07e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,8 +16,8 @@ repos: - id: check-merge-conflict - id: check-ast - - repo: https://gitlab.com/pycqa/flake8 - rev: 3.9.2 + - repo: https://github.com/PyCQA/flake8 + rev: 5.0.4 hooks: - id: flake8 additional_dependencies: [ @@ -32,8 +32,8 @@ repos: - id: black additional_dependencies: ['click==8.0.4'] - - repo: https://github.com/timothycrosley/isort - rev: 5.9.1 + - repo: https://github.com/PyCQA/isort + rev: 5.12.0 hooks: - id: isort exclude: ".*setup.py$" diff --git a/CODEOWNERS b/CODEOWNERS index ecbae86d96..c4ea16328e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -4,29 +4,26 @@ # the repo. Unless a later match takes precedence, erpnext/accounts/ @nextchamp-saqib @deepeshgarg007 @ruthra-kumar -erpnext/assets/ @nextchamp-saqib @deepeshgarg007 @ruthra-kumar +erpnext/assets/ @anandbaburajan @deepeshgarg007 erpnext/loan_management/ @nextchamp-saqib @deepeshgarg007 erpnext/regional @nextchamp-saqib @deepeshgarg007 @ruthra-kumar erpnext/selling @nextchamp-saqib @deepeshgarg007 @ruthra-kumar erpnext/support/ @nextchamp-saqib @deepeshgarg007 pos* @nextchamp-saqib -erpnext/buying/ @marination @rohitwaghchaure @s-aga-r -erpnext/e_commerce/ @marination -erpnext/maintenance/ @marination @rohitwaghchaure @s-aga-r -erpnext/manufacturing/ @marination @rohitwaghchaure @s-aga-r -erpnext/portal/ @marination -erpnext/quality_management/ @marination @rohitwaghchaure @s-aga-r -erpnext/shopping_cart/ @marination -erpnext/stock/ @marination @rohitwaghchaure @s-aga-r +erpnext/buying/ @rohitwaghchaure @s-aga-r +erpnext/maintenance/ @rohitwaghchaure @s-aga-r +erpnext/manufacturing/ @rohitwaghchaure @s-aga-r +erpnext/quality_management/ @rohitwaghchaure @s-aga-r +erpnext/stock/ @rohitwaghchaure @s-aga-r +erpnext/subcontracting @rohitwaghchaure @s-aga-r -erpnext/crm/ @NagariaHussain -erpnext/education/ @rutwikhdev +erpnext/crm/ @NagariaHussain +erpnext/education/ @rutwikhdev erpnext/projects/ @ruchamahabal -erpnext/controllers/ @deepeshgarg007 @nextchamp-saqib @rohitwaghchaure @marination -erpnext/patches/ @deepeshgarg007 @nextchamp-saqib @marination -erpnext/public/ @nextchamp-saqib @marination +erpnext/controllers/ @deepeshgarg007 @nextchamp-saqib @rohitwaghchaure +erpnext/patches/ @deepeshgarg007 @nextchamp-saqib .github/ @ankush -pyproject.toml @gavindsouza @ankush +pyproject.toml @ankush diff --git a/README.md b/README.md index cea3472447..44bd729688 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ New passwords will be created for the ERPNext "Administrator" user, the MariaDB 1. [Frappe School](https://frappe.school) - Learn Frappe Framework and ERPNext from the various courses by the maintainers or from the community. 2. [Official documentation](https://docs.erpnext.com/) - Extensive documentation for ERPNext. 3. [Discussion Forum](https://discuss.erpnext.com/) - Engage with community of ERPNext users and service providers. -4. [Telegram Group](https://t.me/erpnexthelp) - Get instant help from huge community of users. +4. [Telegram Group](https://erpnext_public.t.me) - Get instant help from huge community of users. ## Contributing @@ -82,6 +82,8 @@ GNU/General Public License (see [license.txt](license.txt)) The ERPNext code is licensed as GNU General Public License (v3) and the Documentation is licensed as Creative Commons (CC-BY-SA-3.0) and the copyright is owned by Frappe Technologies Pvt Ltd (Frappe) and Contributors. +By contributing to ERPNext, you agree that your contributions will be licensed under its GNU General Public License (v3). + ## Logo and Trademark Policy Please read our [Logo and Trademark Policy](TRADEMARK_POLICY.md). diff --git a/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py b/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py index fefec0ee7b..f091a4ff5a 100644 --- a/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py +++ b/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py @@ -76,7 +76,7 @@ def get( def build_result(account, dates, gl_entries): result = [[getdate(date), 0.0] for date in dates] - root_type = frappe.db.get_value("Account", account, "root_type") + root_type = frappe.get_cached_value("Account", account, "root_type") # start with the first date date_index = 0 diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py index f319003876..45e04ee6b0 100644 --- a/erpnext/accounts/deferred_revenue.py +++ b/erpnext/accounts/deferred_revenue.py @@ -378,7 +378,7 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None): return # check if books nor frozen till endate: - if accounts_frozen_upto and (end_date) <= getdate(accounts_frozen_upto): + if accounts_frozen_upto and getdate(end_date) <= getdate(accounts_frozen_upto): end_date = get_last_day(add_days(accounts_frozen_upto, 1)) if via_journal_entry: diff --git a/erpnext/accounts/doctype/account/account.py b/erpnext/accounts/doctype/account/account.py index 2610c8655e..ec0ba081c8 100644 --- a/erpnext/accounts/doctype/account/account.py +++ b/erpnext/accounts/doctype/account/account.py @@ -37,7 +37,7 @@ class Account(NestedSet): def autoname(self): from erpnext.accounts.utils import get_autoname_with_number - self.name = get_autoname_with_number(self.account_number, self.account_name, None, self.company) + self.name = get_autoname_with_number(self.account_number, self.account_name, self.company) def validate(self): from erpnext.accounts.utils import validate_field_number @@ -58,7 +58,7 @@ class Account(NestedSet): def validate_parent(self): """Fetch Parent Details and validate parent account""" if self.parent_account: - par = frappe.db.get_value( + par = frappe.get_cached_value( "Account", self.parent_account, ["name", "is_group", "company"], as_dict=1 ) if not par: @@ -82,7 +82,7 @@ class Account(NestedSet): def set_root_and_report_type(self): if self.parent_account: - par = frappe.db.get_value( + par = frappe.get_cached_value( "Account", self.parent_account, ["report_type", "root_type"], as_dict=1 ) @@ -92,7 +92,7 @@ class Account(NestedSet): self.root_type = par.root_type if self.is_group: - db_value = frappe.db.get_value("Account", self.name, ["report_type", "root_type"], as_dict=1) + db_value = self.get_doc_before_save() if db_value: if self.report_type != db_value.report_type: frappe.db.sql( @@ -111,13 +111,13 @@ class Account(NestedSet): ) def validate_root_details(self): - # does not exists parent - if frappe.db.exists("Account", self.name): - if not frappe.db.get_value("Account", self.name, "parent_account"): - throw(_("Root cannot be edited."), RootNotEditable) + doc_before_save = self.get_doc_before_save() + + if doc_before_save and not doc_before_save.parent_account: + throw(_("Root cannot be edited."), RootNotEditable) if not self.parent_account and not self.is_group: - frappe.throw(_("The root account {0} must be a group").format(frappe.bold(self.name))) + throw(_("The root account {0} must be a group").format(frappe.bold(self.name))) def validate_root_company_and_sync_account_to_children(self): # ignore validation while creating new compnay or while syncing to child companies @@ -127,7 +127,9 @@ class Account(NestedSet): return ancestors = get_root_company(self.company) if ancestors: - if frappe.get_value("Company", self.company, "allow_account_creation_against_child_company"): + if frappe.get_cached_value( + "Company", self.company, "allow_account_creation_against_child_company" + ): return if not frappe.db.get_value( "Account", {"account_name": self.account_name, "company": ancestors[0]}, "name" @@ -138,7 +140,7 @@ class Account(NestedSet): if not descendants: return parent_acc_name_map = {} - parent_acc_name, parent_acc_number = frappe.db.get_value( + parent_acc_name, parent_acc_number = frappe.get_cached_value( "Account", self.parent_account, ["account_name", "account_number"] ) filters = { @@ -159,27 +161,28 @@ class Account(NestedSet): self.create_account_for_child_company(parent_acc_name_map, descendants, parent_acc_name) def validate_group_or_ledger(self): - if self.get("__islocal"): + doc_before_save = self.get_doc_before_save() + if not doc_before_save or cint(doc_before_save.is_group) == cint(self.is_group): return - existing_is_group = frappe.db.get_value("Account", self.name, "is_group") - if cint(self.is_group) != cint(existing_is_group): - if self.check_gle_exists(): - throw(_("Account with existing transaction cannot be converted to ledger")) - elif self.is_group: - if self.account_type and not self.flags.exclude_account_type_check: - throw(_("Cannot covert to Group because Account Type is selected.")) - elif self.check_if_child_exists(): - throw(_("Account with child nodes cannot be set as ledger")) + if self.check_gle_exists(): + throw(_("Account with existing transaction cannot be converted to ledger")) + elif self.is_group: + if self.account_type and not self.flags.exclude_account_type_check: + throw(_("Cannot covert to Group because Account Type is selected.")) + elif self.check_if_child_exists(): + throw(_("Account with child nodes cannot be set as ledger")) def validate_frozen_accounts_modifier(self): - old_value = frappe.db.get_value("Account", self.name, "freeze_account") - if old_value and old_value != self.freeze_account: - frozen_accounts_modifier = frappe.db.get_value( - "Accounts Settings", None, "frozen_accounts_modifier" - ) - if not frozen_accounts_modifier or frozen_accounts_modifier not in frappe.get_roles(): - throw(_("You are not authorized to set Frozen value")) + doc_before_save = self.get_doc_before_save() + if not doc_before_save or doc_before_save.freeze_account == self.freeze_account: + return + + frozen_accounts_modifier = frappe.get_cached_value( + "Accounts Settings", "Accounts Settings", "frozen_accounts_modifier" + ) + if not frozen_accounts_modifier or frozen_accounts_modifier not in frappe.get_roles(): + throw(_("You are not authorized to set Frozen value")) def validate_balance_must_be_debit_or_credit(self): from erpnext.accounts.utils import get_balance_on @@ -223,9 +226,9 @@ class Account(NestedSet): ) # validate if parent of child company account to be added is a group - if frappe.db.get_value("Account", self.parent_account, "is_group") and not frappe.db.get_value( - "Account", parent_acc_name_map[company], "is_group" - ): + if frappe.get_cached_value( + "Account", self.parent_account, "is_group" + ) and not frappe.get_cached_value("Account", parent_acc_name_map[company], "is_group"): msg = _( "While creating account for Child Company {0}, parent account {1} found as a ledger account." ).format(company_bold, parent_acc_name_bold) @@ -377,17 +380,15 @@ def validate_account_number(name, account_number, company): @frappe.whitelist() def update_account_number(name, account_name, account_number=None, from_descendant=False): - account = frappe.db.get_value("Account", name, "company", as_dict=True) + account = frappe.get_cached_doc("Account", name) if not account: return - old_acc_name, old_acc_number = frappe.db.get_value( - "Account", name, ["account_name", "account_number"] - ) + old_acc_name, old_acc_number = account.account_name, account.account_number # check if account exists in parent company ancestors = get_ancestors_of("Company", account.company) - allow_independent_account_creation = frappe.get_value( + allow_independent_account_creation = frappe.get_cached_value( "Company", account.company, "allow_account_creation_against_child_company" ) @@ -435,22 +436,24 @@ def update_account_number(name, account_name, account_number=None, from_descenda @frappe.whitelist() def merge_account(old, new, is_group, root_type, company): # Validate properties before merging - if not frappe.db.exists("Account", new): + new_account = frappe.get_cached_doc("Account", new) + + if not new_account: throw(_("Account {0} does not exist").format(new)) - val = list(frappe.db.get_value("Account", new, ["is_group", "root_type", "company"])) - - if val != [cint(is_group), root_type, company]: + if (new_account.is_group, new_account.root_type, new_account.company) != ( + cint(is_group), + root_type, + company, + ): throw( _( """Merging is only possible if following properties are same in both records. Is Group, Root Type, Company""" ) ) - if is_group and frappe.db.get_value("Account", new, "parent_account") == old: - frappe.db.set_value( - "Account", new, "parent_account", frappe.db.get_value("Account", old, "parent_account") - ) + if is_group and new_account.parent_account == old: + new_account.db_set("parent_account", frappe.get_cached_value("Account", old, "parent_account")) frappe.rename_doc("Account", old, new, merge=1, force=1) diff --git a/erpnext/accounts/doctype/account/account_tree.js b/erpnext/accounts/doctype/account/account_tree.js index 8ae90ceb38..d537adfcbf 100644 --- a/erpnext/accounts/doctype/account/account_tree.js +++ b/erpnext/accounts/doctype/account/account_tree.js @@ -56,36 +56,41 @@ frappe.treeview_settings["Account"] = { accounts = nodes; } - const get_balances = frappe.call({ - method: 'erpnext.accounts.utils.get_account_balances', - args: { - accounts: accounts, - company: cur_tree.args.company - }, - }); + frappe.db.get_single_value("Accounts Settings", "show_balance_in_coa").then((value) => { + if(value) { - get_balances.then(r => { - if (!r.message || r.message.length == 0) return; + const get_balances = frappe.call({ + method: 'erpnext.accounts.utils.get_account_balances', + args: { + accounts: accounts, + company: cur_tree.args.company + }, + }); - for (let account of r.message) { + get_balances.then(r => { + if (!r.message || r.message.length == 0) return; - const node = cur_tree.nodes && cur_tree.nodes[account.value]; - if (!node || node.is_root) continue; + for (let account of r.message) { - // show Dr if positive since balance is calculated as debit - credit else show Cr - const balance = account.balance_in_account_currency || account.balance; - const dr_or_cr = balance > 0 ? "Dr": "Cr"; - const format = (value, currency) => format_currency(Math.abs(value), currency); + const node = cur_tree.nodes && cur_tree.nodes[account.value]; + if (!node || node.is_root) continue; - if (account.balance!==undefined) { - node.parent && node.parent.find('.balance-area').remove(); - $('' - + (account.balance_in_account_currency ? - (format(account.balance_in_account_currency, account.account_currency) + " / ") : "") - + format(account.balance, account.company_currency) - + " " + dr_or_cr - + '').insertBefore(node.$ul); - } + // show Dr if positive since balance is calculated as debit - credit else show Cr + const balance = account.balance_in_account_currency || account.balance; + const dr_or_cr = balance > 0 ? "Dr": "Cr"; + const format = (value, currency) => format_currency(Math.abs(value), currency); + + if (account.balance!==undefined) { + node.parent && node.parent.find('.balance-area').remove(); + $('' + + (account.balance_in_account_currency ? + (format(account.balance_in_account_currency, account.account_currency) + " / ") : "") + + format(account.balance, account.company_currency) + + " " + dr_or_cr + + '').insertBefore(node.$ul); + } + } + }); } }); }, diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py b/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py index 947b4853e8..75f8f0645c 100644 --- a/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py +++ b/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py @@ -53,7 +53,7 @@ def create_charts( "account_number": account_number, "account_type": child.get("account_type"), "account_currency": child.get("account_currency") - or frappe.db.get_value("Company", company, "default_currency"), + or frappe.get_cached_value("Company", company, "default_currency"), "tax_rate": child.get("tax_rate"), } ) @@ -148,7 +148,7 @@ def get_charts_for_country(country, with_standard=False): ) or frappe.local.flags.allow_unverified_charts: charts.append(content["name"]) - country_code = frappe.db.get_value("Country", country, "code") + country_code = frappe.get_cached_value("Country", country, "code") if country_code: folders = ("verified",) if frappe.local.flags.allow_unverified_charts: diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/unverified/at_austria_chart_template.json b/erpnext/accounts/doctype/account/chart_of_accounts/unverified/at_austria_chart_template.json index 3b07d5181e..58d67beb67 100644 --- a/erpnext/accounts/doctype/account/chart_of_accounts/unverified/at_austria_chart_template.json +++ b/erpnext/accounts/doctype/account/chart_of_accounts/unverified/at_austria_chart_template.json @@ -2,397 +2,438 @@ "country_code": "at", "name": "Austria - Chart of Accounts", "tree": { - "Summe Abschreibungen und Aufwendungen": { - "7010 bis 7080 Abschreibungen auf das Anlageverm\u00f6gen (ausgenommen Finanzanlagen)": {}, - "7100 bis 7190 Sonstige Steuern": { - "account_type": "Tax" - }, - "7200 bis 7290 Instandhaltung u. Reinigung durh Dritte, Entsorgung, Beleuchtung": {}, - "7300 bis 7310 Transporte durch Dritte": {}, - "7320 bis 7330 Kfz - Aufwand": {}, - "7340 bis 7350 Reise- und Fahraufwand": {}, - "7360 bis 7370 Tag- und N\u00e4chtigungsgelder": {}, - "7380 bis 7390 Nachrichtenaufwand": {}, - "7400 bis 7430 Miet- und Pachtaufwand": {}, - "7440 bis 7470 Leasingaufwand": {}, - "7480 bis 7490 Lizenzaufwand": {}, - "7500 bis 7530 Aufwand f\u00fcr beigestelltes Personal": {}, - "7540 bis 7570 Provisionen an Dritte": {}, - "7580 bis 7590 Aufsichtsratsverg\u00fctungen": {}, - "7610 bis 7620 Druckerzeugnisse und Vervielf\u00e4ltigungen": {}, - "7650 bis 7680 Werbung und Repr\u00e4sentationen": {}, - "7700 bis 7740 Versicherungen": {}, - "7750 bis 7760 Beratungs- und Pr\u00fcfungsaufwand": {}, - "7800 bis 7810 Schadensf\u00e4lle": {}, - "7840 bis 7880 Verschiedene betriebliche Aufwendungen": {}, - "7910 bis 7950 Aufwandsstellenrechung der Hersteller": {}, - "Abschreibungen auf aktivierte Aufwendungen f\u00fcr das Ingangs. u. Erweitern des Betriebes": {}, - "Abschreibungen vom Umlaufverm\u00f6gen, soweit diese die im Unternehmen \u00fcblichen Abschreibungen \u00fcbersteigen": {}, - "Aufwandsstellenrechnung": {}, - "Aus- und Fortbildung": {}, - "Buchwert abgegangener Anlagen, ausgenommen Finanzanlagen": {}, - "B\u00fcromaterial und Drucksorten": {}, - "Fachliteratur und Zeitungen ": {}, - "Herstellungskosten der zur Erzielung der Umsatzerl\u00f6se erbrachten Leistungen": {}, - "Mitgliedsbeitr\u00e4ge": {}, - "Skontoertr\u00e4ge auf sonstige betriebliche Aufwendungen": {}, - "Sonstige betrieblichen Aufwendungen": {}, - "Spenden und Trinkgelder": {}, - "Spesen des Geldverkehrs": {}, - "Verluste aus dem Abgang vom Anlageverm\u00f6gen, ausgenommen Finanzanlagen": {}, - "Vertriebskosten": {}, - "Verwaltungskosten": {}, - "root_type": "Expense" - }, - "Summe Betriebliche Ertr\u00e4ge": { - "4400 bis 4490 Erl\u00f6sschm\u00e4lerungen": {}, - "4500 bis 4570 Ver\u00e4nderungen des Bestandes an fertigen und unfertigen Erzeugn. sowie an noch nicht abrechenbaren Leistungen": {}, - "4580 bis 4590 andere aktivierte Eigenleistungen": {}, - "4600 bis 4620 Erl\u00f6se aus dem Abgang vom Anlageverm\u00f6gen, ausgen. Finanzanlagen": {}, - "4630 bis 4650 Ertr\u00e4ge aus dem Abgang vom Anlageverm\u00f6gen, ausgen. Finanzanlagen": {}, - "4660 bis 4670 Ertr\u00e4ge aus der Zuschreibung zum Anlageverm\u00f6gen, ausgen. Finanzanlagen": {}, - "4700 bis 4790 Ertr\u00e4ge aus der Aufl\u00f6sung von R\u00fcckstellungen": {}, - "4800 bis 4990 \u00dcbrige betriebliche Ertr\u00e4ge": {}, - "Erl\u00f6se 0 % Ausfuhrlieferungen/Drittl\u00e4nder": {}, - "Erl\u00f6se 10 %": {}, - "Erl\u00f6se 20 %": {}, - "Erl\u00f6se aus im Inland stpfl. EG Lieferungen 10 % USt": {}, - "Erl\u00f6se aus im Inland stpfl. EG Lieferungen 20 % USt": {}, - "Erl\u00f6se i.g. Lieferungen (stfr)": {}, - "root_type": "Income" - }, - "Summe Eigenkapital R\u00fccklagen Abschlusskonten": { - "9000 bis 9180 Gezeichnetes bzw. gewidmetes Kapital": { - "account_type": "Equity" - }, - "9200 bis 9290 Kapitalr\u00fccklagen": { - "account_type": "Equity" - }, - "9300 bis 9380 Gewinnr\u00fccklagen": { - "account_type": "Equity" - }, - "9400 bis 9590 Bewertungsreserven uns sonst. unversteuerte R\u00fccklagen": { - "account_type": "Equity" - }, - "9600 bis 9690 Privat und Verrechnungskonten bei Einzelunternehmen und Personengesellschaften": {}, - "9700 bis 9790 Einlagen stiller Gesellschafter ": {}, - "9900 bis 9999 Evidenzkonten": {}, - "Bilanzgewinn (-verlust )": { - "account_type": "Equity" - }, - "Er\u00f6ffnungsbilanz": {}, - "Gewinn- und Verlustrechnung": {}, - "Schlussbilanz": {}, - "nicht eingeforderte ausstehende Einlagen": { - "account_type": "Equity" - }, - "root_type": "Equity" - }, - "Summe Finanzertr\u00e4ge und Aufwendungen": { - "8000 bis 8040 Ertr\u00e4ge aus Beteiligungen": {}, - "8050 bis 8090 Ertr\u00e4ge aus anderen Wertpapieren und Ausleihungen des Finanzanlageverm\u00f6gens": {}, - "8100 bis 8130 Sonstige Zinsen und \u00e4hnliche Ertr\u00e4ge": {}, - "8220 bis 8250 Aufwendungen aus Beteiligungen": {}, - "8260 bis 8270 Aufwendungen aus sonst. Fiananzanlagen und aus Wertpapieren des Umlaufverm\u00f6gens": {}, - "8280 bis 8340 Zinsen und \u00e4hnliche Aufwendungem": {}, - "8400 bis 8440 Au\u00dferordentliche Ertr\u00e4ge": {}, - "8450 bis 8490 Au\u00dferordentliche Aufwendungen": {}, - "8500 bis 8590 Steuern vom Einkommen und vom Ertrag": { - "account_type": "Tax" - }, - "8600 bis 8690 Aufl\u00f6sung unversteuerten R\u00fccklagen": {}, - "8700 bis 8740 Aufl\u00f6sung von Kapitalr\u00fccklagen": {}, - "8750 bis 8790 Aufl\u00f6sung von Gewinnr\u00fccklagen": {}, - "8800 bis 8890 Zuweisung von unversteuerten R\u00fccklagen": {}, - "Buchwert abgegangener Beteiligungen": {}, - "Buchwert abgegangener Wertpapiere des Umlaufverm\u00f6gens": {}, - "Buchwert abgegangener sonstiger Finanzanlagen": {}, - "Erl\u00f6se aus dem Abgang von Beteiligungen": {}, - "Erl\u00f6se aus dem Abgang von Wertpapieren des Umlaufverm\u00f6gens": {}, - "Erl\u00f6se aus dem Abgang von sonstigen Finanzanlagen": {}, - "Ertr\u00e4ge aus dem Abgang von und der Zuschreibung zu Finanzanlagen": {}, - "Ertr\u00e4ge aus dem Abgang von und der Zuschreibung zu Wertpapieren des Umlaufverm\u00f6gens": {}, - "Gewinabfuhr bzw. Verlust\u00fcberrechnung aus Ergebnisabf\u00fchrungsvertr\u00e4gen": {}, - "nicht ausgenutzte Lieferantenskonti": {}, - "root_type": "Income" - }, - "Summe Fremdkapital": { - "3020 bis 3030 Steuerr\u00fcckstellungen": {}, - "3040 bis 3090 Sonstige R\u00fcckstellungen": {}, - "3110 bis 3170 Verbindlichkeiten gegen\u00fcber Kredidinstituten": {}, - "3180 bis 3190 Verbindlichkeiten gegen\u00fcber Finanzinstituten": {}, - "3380 bis 3390 Verbindlichkeiten aus der Annahme gezogener Wechsel u. d. Ausstellungen eigener Wechsel": { + "Klasse 0 Aktiva: Anlageverm\u00f6gen": { + "0100 Konzessionen ": {"account_type": "Fixed Asset"}, + "0110 Patentrechte und Lizenzen ": {"account_type": "Fixed Asset"}, + "0120 Datenverarbeitungsprogramme ": {"account_type": "Fixed Asset"}, + "0130 Marken, Warenzeichen und Musterschutzrechte, sonstige Urheberrechte ": {"account_type": "Fixed Asset"}, + "0140 Pacht- und Mietrechte ": {"account_type": "Fixed Asset"}, + "0150 Bezugs- und ähnliche Rechte ": {"account_type": "Fixed Asset"}, + "0160 Geschäfts-/Firmenwert ": {"account_type": "Fixed Asset"}, + "0170 Umgründungsmehrwert ": {"account_type": "Fixed Asset"}, + "0180 Geleistete Anzahlungen auf immaterielle Vermögensgegenstände": {"account_type": "Fixed Asset"}, + "0190 Kumulierte Abschreibungen zu immateriellen Vermögensgegenständen ": {"account_type": "Fixed Asset"}, + "0200 Unbebaute Grundstücke, soweit nicht landwirtschaftlich genutzt ": {"account_type": "Fixed Asset"}, + "0210 Bebaute Grundstücke (Grundwert) ": {"account_type": "Fixed Asset"}, + "0220 Landwirtschaftlich genutzte Grundstücke ": {"account_type": "Fixed Asset"}, + "0230 Grundstücksgleiche Rechte ": {"account_type": "Fixed Asset"}, + "0300 Betriebs- und Geschäftsgebäude auf eigenem Grund ": {"account_type": "Fixed Asset"}, + "0310 Wohn- und Sozialgebäude auf eigenem Grund ": {"account_type": "Fixed Asset"}, + "0320 Betriebs- und Geschäftsgebäude auf fremdem Grund ": {"account_type": "Fixed Asset"}, + "0330 Wohn- und Sozialgebäude auf fremdem Grund ": {"account_type": "Fixed Asset"}, + "0340 Grundstückseinrichtungen auf eigenem Grund ": {"account_type": "Fixed Asset"}, + "0350 Grundstückseinrichtungen auf fremdem Grund ": {"account_type": "Fixed Asset"}, + "0360 Bauliche Investitionen in fremden (gepachteten) Betriebs- und Geschäftsgebäuden": {"account_type": "Fixed Asset"}, + "0370 Bauliche Investitionen in fremden (gepachteten) Wohn- und Sozialgebäuden": {"account_type": "Fixed Asset"}, + "0390 Kumulierte Abschreibungen zu Grundstücken ": {"account_type": "Fixed Asset"}, + "0400 Maschinen und Geräte ": {"account_type": "Fixed Asset"}, + "0500 Maschinenwerkzeuge ": {"account_type": "Fixed Asset"}, + "0510 Allgemeine Werkzeuge und Handwerkzeuge ": {"account_type": "Fixed Asset"}, + "0520 Prototypen, Formen, Modelle ": {"account_type": "Fixed Asset"}, + "0530 Andere Erzeugungshilfsmittel (auch Softwarewerkzeuge)": {"account_type": "Fixed Asset"}, + "0540 Hebezeuge und Montageanlagen ": {"account_type": "Fixed Asset"}, + "0550 Geringwertige Vermögensgegenstände, soweit im Erzeugungsprozess ": {"account_type": "Fixed Asset"}, + "0560 Festwerte technische Anlagen und Maschinen ": {"account_type": "Fixed Asset"}, + "0590 Kumulierte Abschreibungen zu technischen Anlagen und Maschinen ": {"account_type": "Fixed Asset"}, + "0600 Betriebs- und Geschäftsausstattung, soweit nicht gesondert angeführt ": {"account_type": "Fixed Asset"}, + "0610 Andere Anlagen, soweit nicht gesondert angeführt ": {"account_type": "Fixed Asset"}, + "0620 Büromaschinen, EDV-Anlagen ": {"account_type": "Fixed Asset"}, + "0630 PKW und Kombis ": {"account_type": "Fixed Asset"}, + "0640 LKW ": {"account_type": "Fixed Asset"}, + "0650 Andere Beförderungsmittel ": {"account_type": "Fixed Asset"}, + "0660 Gebinde ": {"account_type": "Fixed Asset"}, + "0670 Geringwertige Vermögensgegenstände, soweit nicht im Erzeugungssprozess verwendet": {"account_type": "Fixed Asset"}, + "0680 Festwerte außer technische Anlagen und Maschinen ": {"account_type": "Fixed Asset"}, + "0690 Kumulierte Abschreibungen zu anderen Anlagen, Betriebs- und Geschäftsausstattung": {"account_type": "Fixed Asset"}, + "0700 Geleistete Anzahlungen auf Sachanlagen ": {"account_type": "Fixed Asset"}, + "0710 Anlagen in Bau ": {"account_type": "Fixed Asset"}, + "0790 Kumulierte Abschreibungen zu geleisteten Anzahlungen auf Sachanlagen ": {"account_type": "Fixed Asset"}, + "0800 Anteile an verbundenen Unternehmen ": {"account_type": "Fixed Asset"}, + "0810 Beteiligungen an Gemeinschaftsunternehmen ": {"account_type": "Fixed Asset"}, + "0820 Beteiligungen an angeschlossenen (assoziierten) Unternehmen ": {"account_type": "Fixed Asset"}, + "0830 Eigene Anteile, Anteile an herrschenden oder mit Mehrheit beteiligten ": {"account_type": "Fixed Asset"}, + "0840 Sonstige Beteiligungen ": {"account_type": "Fixed Asset"}, + "0850 Ausleihungen an verbundene Unternehmen ": {"account_type": "Fixed Asset"}, + "0860 Ausleihungen an Unternehmen mit Beteiligungsverhältnis": {"account_type": "Fixed Asset"}, + "0870 Ausleihungen an Gesellschafter ": {"account_type": "Fixed Asset"}, + "0880 Sonstige Ausleihungen ": {"account_type": "Fixed Asset"}, + "0890 Anteile an Kapitalgesellschaften ohne Beteiligungscharakter ": {"account_type": "Fixed Asset"}, + "0900 Anteile an Personengesellschaften ohne Beteiligungscharakter ": {"account_type": "Fixed Asset"}, + "0910 Genossenschaftsanteile ohne Beteiligungscharakter ": {"account_type": "Fixed Asset"}, + "0920 Anteile an Investmentfonds ": {"account_type": "Fixed Asset"}, + "0930 Festverzinsliche Wertpapiere des Anlagevermögens ": {"account_type": "Fixed Asset"}, + "0980 Geleistete Anzahlungen auf Finanzanlagen ": {"account_type": "Fixed Asset"}, + "0990 Kumulierte Abschreibungen zu Finanzanlagen ": {"account_type": "Fixed Asset"}, + "root_type": "Asset" + }, + "Klasse 1 Aktiva: Vorr\u00e4te": { + "1000 Bezugsverrechnung": {"account_type": "Stock"}, + "1100 Rohstoffe": {"account_type": "Stock"}, + "1200 Bezogene Teile": {"account_type": "Stock"}, + "1300 Hilfsstoffe": {"account_type": "Stock"}, + "1350 Betriebsstoffe": {"account_type": "Stock"}, + "1360 Vorrat Energietraeger": {"account_type": "Stock"}, + "1400 Unfertige Erzeugnisse": {"account_type": "Stock"}, + "1500 Fertige Erzeugnisse": {"account_type": "Stock"}, + "1600 Handelswarenvorrat": {"account_type": "Stock Received But Not Billed"}, + "1700 Noch nicht abrechenbare Leistungen": {"account_type": "Stock"}, + "1900 Wertberichtigungen": {"account_type": "Stock"}, + "1800 Geleistete Anzahlungen": {"account_type": "Stock"}, + "1900 Wertberichtigungen": {"account_type": "Stock"}, + "root_type": "Asset" + }, + "Klasse 3 Passiva: Verbindlichkeiten": { + "3000 Allgemeine Verbindlichkeiten (Schuld)": {"account_type": "Payable"}, + "3010 R\u00fcckstellungen f\u00fcr Pensionen": {"account_type": "Payable"}, + "3020 Steuerr\u00fcckstellungen": {"account_type": "Tax"}, + "3041 Sonstige R\u00fcckstellungen": {"account_type": "Payable"}, + "3110 Verbindlichkeiten gegen\u00fcber Bank": {"account_type": "Payable"}, + "3150 Verbindlichkeiten Darlehen": {"account_type": "Payable"}, + "3185 Verbindlichkeiten Kreditkarte": {"account_type": "Payable"}, + "3380 Verbindlichkeiten aus der Annahme gezogener Wechsel u. d. Ausstellungen eigener Wechsel": { "account_type": "Payable" }, - "3400 bis 3470 Verbindlichkeiten gegen\u00fc. verb. Untern., Verbindl. gegen\u00fc. Untern., mit denen eine Beteiligungsverh\u00e4lnis besteht": {}, - "3600 bis 3690 Verbindlichkeiten im Rahmen der sozialen Sicherheit": {}, - "3700 bis 3890 \u00dcbrige sonstige Verbindlichkeiten": {}, - "3900 bis 3990 Passive Rechnungsabgrenzungsposten": {}, - "Anleihen (einschlie\u00dflich konvertibler)": {}, - "Erhaltene Anzahlungenauf Bestellungen": {}, - "R\u00fcckstellungen f\u00fcr Abfertigung": {}, - "R\u00fcckstellungen f\u00fcr Pensionen": {}, - "USt. \u00a719 /art (reverse charge)": { + "3400 Verbindlichkeiten gegen\u00fc. verb. Untern., Verbindl. gegen\u00fc. Untern., mit denen eine Beteiligungsverh\u00e4lnis besteht": {}, + "3460 Verbindlichkeiten gegenueber Gesellschaftern": {"account_type": "Payable"}, + "3470 Einlagen stiller Gesellschafter": {"account_type": "Payable"}, + "3585 Verbindlichkeiten Lohnsteuer": {"account_type": "Tax"}, + "3590 Verbindlichkeiten Kommunalabgaben": {"account_type": "Tax"}, + "3595 Verbindlichkeiten Dienstgeberbeitrag": {"account_type": "Tax"}, + "3600 Verbindlichkeiten Sozialversicherung": {"account_type": "Payable"}, + "3640 Verbindlichkeiten Loehne und Gehaelter": {"account_type": "Payable"}, + "3700 Sonstige Verbindlichkeiten": {"account_type": "Payable"}, + "3900 Passive Rechnungsabgrenzungsposten": {"account_type": "Payable"}, + "3100 Anleihen (einschlie\u00dflich konvertibler)": {"account_type": "Payable"}, + "3200 Erhaltene Anzahlungen auf Bestellungen": {"account_type": "Payable"}, + "3040 R\u00fcckstellungen f\u00fcr Abfertigung": {"account_type": "Payable"}, + + "3530 USt. \u00a719 (reverse charge)": { "account_type": "Tax" }, - "Umsatzsteuer": {}, - "Umsatzsteuer Zahllast": { + "3500 Verbindlichkeiten aus Umsatzsteuer": {"account_type": "Tax"}, + "3580 Umsatzsteuer Zahllast": { "account_type": "Tax" }, - "Umsatzsteuer aus i.g. Erwerb 10%": { + "3510 Umsatzsteuer Inland 20%": { "account_type": "Tax" }, - "Umsatzsteuer aus i.g. Erwerb 20%": { + "3515 Umsatzsteuer Inland 10%": { + "account_type": "Tax" + }, + "3520 Umsatzsteuer aus i.g. Erwerb 20%": { "account_type": "Tax" }, - "Umsatzsteuer aus i.g. Lieferungen 10%": { + "3525 Umsatzsteuer aus i.g. Erwerb 10%": { "account_type": "Tax" - }, - "Umsatzsteuer aus i.g. Lieferungen 20%": { - "account_type": "Tax" - }, - "Umsatzsteuer-Evidenzkonto f\u00fcr erhaltene Anzahlungen auf Bestellungen": {}, - "Verbindlichkeiten aus Lieferungen u. Leistungen EU": { + }, + "3560 Umsatzsteuer-Evidenzkonto f\u00fcr erhaltene Anzahlungen auf Bestellungen": {}, + "3360 Verbindlichkeiten aus Lieferungen u. Leistungen EU": { "account_type": "Payable" }, - "Verbindlichkeiten aus Lieferungen u. Leistungen Inland": { + "3000 Verbindlichkeiten aus Lieferungen u. Leistungen Inland": { "account_type": "Payable" }, - "Verbindlichkeiten aus Lieferungen u. Leistungen sonst. Ausland": { + "3370 Verbindlichkeiten aus Lieferungen u. Leistungen sonst. Ausland": { "account_type": "Payable" }, - "Verbindlichkeiten gegen\u00fcber Gesellschaften": {}, - "Verrechnung Finanzamt": { + "3400 Verbindlichkeiten gegen\u00fcber verbundenen Unternehmen": {}, + "3570 Verrechnung Finanzamt": { "account_type": "Tax" }, "root_type": "Liability" - }, - "Summe Kontoklasse 0 Anlageverm\u00f6gen": { - "44 bis 49 Sonstige Maschinen und maschinelle Anlagen": {}, - "920 bis 930 Festverzinsliche Wertpapiere des Anlageverm\u00f6gens": {}, - "940 bis 970 Sonstige Finanzanlagen, Wertrechte": {}, - "Allgemeine Werkzeuge und Handwerkzeuge": {}, - "Andere Bef\u00f6rderungsmittel": {}, - "Andere Betriebs- und Gesch\u00e4ftsausstattung": {}, - "Andere Erzeugungshilfsmittel": {}, - "Anlagen im Bau": {}, - "Anteile an Investmentfonds": {}, - "Anteile an Kapitalgesellschaften ohne Beteiligungscharakter": {}, - "Anteile an Personengesellschaften ohne Beteiligungscharakter": {}, - "Anteile an verbundenen Unternehmen": {}, - "Antriebsmaschinen": {}, - "Aufwendungen f\u00fcs das Ingangssetzen u. Erweitern eines Betriebes": {}, - "Ausleihungen an verbundene Unternehmen": {}, - "Ausleihungen an verbundene Unternehmen, mit denen ein Beteiligungsverh\u00e4lnis besteht": {}, - "Bauliche Investitionen in fremden (gepachteten) Betriebs- und Gesch\u00e4ftsgeb\u00e4uden": {}, - "Bauliche Investitionen in fremden (gepachteten) Wohn- und Sozialgeb\u00e4uden": {}, - "Bebaute Grundst\u00fccke (Grundwert)": {}, - "Beheizungs- und Beleuchtungsanlagen": {}, - "Beteiligungen an Gemeinschaftunternehmen": {}, - "Beteiligungen an angeschlossenen (assoziierten) Unternehmen": {}, - "Betriebs- und Gesch\u00e4ftsgeb\u00e4ude auf eigenem Grund": {}, - "Betriebs- und Gesch\u00e4ftsgeb\u00e4ude auf fremdem Grund": {}, - "B\u00fcromaschinen, EDV - Anlagen": {}, - "Datenverarbeitungsprogramme": {}, - "Energieversorgungsanlagen": {}, - "Fertigungsmaschinen": {}, - "Gebinde": {}, - "Geleistete Anzahlungen": {}, - "Genossenschaften ohne Beteiligungscharakter": {}, - "Geringwertige Verm\u00f6gensgegenst\u00e4nde, soweit im Erzeugerprozess verwendet": {}, - "Geringwertige Verm\u00f6gensgegenst\u00e4nde, soweit nicht im Erzeugungsprozess verwendet": {}, - "Gesch\u00e4fts(Firmen)wert": {}, - "Grundst\u00fcckseinrichtunten auf eigenem Grund": {}, - "Grundst\u00fcckseinrichtunten auf fremdem Grund": {}, - "Grundst\u00fccksgleiche Rechte": {}, - "Hebezeuge und Montageanlagen": {}, - "Konzessionen": {}, - "Kumulierte Abschreibungen": {}, - "LKW": {}, - "Marken, Warenzeichen und Musterschutzrechte": {}, - "Maschinenwerkzeuge": {}, - "Nachrichten- und Kontrollanlagen": {}, - "PKW": {}, - "Pacht- und Mietrechte": {}, - "Patentrechte und Lizenzen": {}, - "Sonstige Ausleihungen": {}, - "Sonstige Beteiligungen": {}, - "Transportanlagen": {}, - "Unbebaute Grundst\u00fccke": {}, - "Vorrichtungen, Formen und Modelle": {}, - "Wohn- und Sozialgeb\u00e4ude auf eigenem Grund": {}, - "Wohn- und Sozialgeb\u00e4ude auf fremdem Grund": {}, + }, + "Klasse 2 Aktiva: Umlaufverm\u00f6gen, Rechnungsabgrenzungen": { + "2030 Forderungen aus Lieferungen und Leistungen Inland (0% USt, umsatzsteuerfrei)": { + "account_type": "Receivable" + }, + "2010 Forderungen aus Lieferungen und Leistungen Inland (10% USt, umsatzsteuerfrei)": { + "account_type": "Receivable" + }, + "2000 Forderungen aus Lieferungen und Leistungen Inland (20% USt, umsatzsteuerfrei)": { + "account_type": "Receivable" + }, + "2040 Forderungen aus Lieferungen und Leistungen Inland (sonstiger USt-Satz)": { + "account_type": "Receivable" + }, + "2100 Forderungen aus Lieferungen und Leistungen EU": { + "account_type": "Receivable" + }, + "2150 Forderungen aus Lieferungen und Leistungen Ausland (Nicht-EU)": { + "account_type": "Receivable" + }, + "2200 Forderungen gegen\u00fcber verbundenen Unternehmen": { + "account_type": "Receivable" + }, + "2250 Forderungen gegen\u00fcber Unternehmen, mit denen ein Beteiligungsverh\u00e4ltnis besteht": { + "account_type": "Receivable" + }, + "2300 Sonstige Forderungen und Verm\u00f6gensgegenst\u00e4nde": { + "account_type": "Receivable" + }, + "2630 Sonstige Wertpapiere": { + "account_type": "Stock" + }, + "2750 Kassenbest\u00e4nde in Fremdw\u00e4hrung": { + "account_type": "Cash" + }, + "2900 Aktive Rechnungsabrenzungsposten": { + "account_type": "Receivable" + }, + "2600 Anteile an verbundenen Unternehmen": { + "account_type": "Equity" + }, + "2680 Besitzwechsel ohne Forderungen": { + "account_type": "Receivable" + }, + "2950 Aktiviertes Disagio": { + "account_type": "Receivable" + }, + "2610 Eigene Anteile und Wertpapiere an mit Mehrheit beteiligten Unternehmen": { + "account_type": "Receivable" + }, + "2570 Einfuhrumsatzsteuer (bezahlt)": {"account_type": "Tax"}, + + "2460 Eingeforderte aber noch nicht eingezahlte Einlagen": { + "account_type": "Receivable" + }, + "2180 Einzelwertberichtigungen zu Forderungen aus Lief. und Leist. Ausland": { + "account_type": "Receivable" + }, + "2130 Einzelwertberichtigungen zu Forderungen aus Lief. und Leist. EU": { + "account_type": "Receivable" + }, + "2080 Einzelwertberichtigungen zu Forderungen aus Lief. und Leist. Inland ": { + "account_type": "Receivable" + }, + "2270 Einzelwertberichtigungen zu Forderungen gegen\u00fcber Unternehmen mit denen ein Beteiligungsverh\u00e4ltnis besteht": { + "account_type": "Receivable" + }, + "2230 Einzelwertberichtigungen zu Forderungen gegen\u00fcber verbundenen Unternehmen": { + "account_type": "Receivable" + }, + "2470 Einzelwertberichtigungen zu sonstigen Forderungen und Verm\u00f6gensgegenst\u00e4nden": { + "account_type": "Receivable" + }, + "2700 Kassenbestand": { + "account_type": "Cash" + }, + "2190 Pauschalwertberichtigungen zu Forderungen aus Lief. und Leist. sonstiges Ausland": { + "account_type": "Receivable" + }, + "2130 Pauschalwertberichtigungen zu Forderungen aus Lief. und Leist. EU": { + "account_type": "Receivable" + }, + "2100 Pauschalwertberichtigungen zu Forderungen aus Lief. und Leist. Inland ": { + "account_type": "Receivable" + }, + "2280 Pauschalwertberichtigungen zu Forderungen gegen\u00fcber Unternehmen mit denen ein Beteiligungsverh\u00e4ltnis besteht": { + "account_type": "Receivable" + }, + "2240 Pauschalwertberichtigungen zu Forderungen gegen\u00fcber verbundenen Unternehmen": { + "account_type": "Receivable" + }, + "2480 Pauschalwertberichtigungen zu sonstigen Forderungen und Verm\u00f6gensgegenst\u00e4nden": { + "account_type": "Receivable" + }, + "2740 Postwertzeichen": { + "account_type": "Cash" + }, + "2780 Schecks in Euro": { + "account_type": "Cash" + }, + "2800 Guthaben bei Bank": { + "account_type": "Bank" + }, + "2801 Guthaben bei Bank - Sparkonto": { + "account_type": "Bank" + }, + "2810 Guthaben bei Paypal": { + "account_type": "Bank" + }, + "2930 Mietvorauszahlungen": { + "account_type": "Receivable" + }, + "2980 Abgrenzung latenter Steuern": { + "account_type": "Receivable" + }, + "2500 Vorsteuer": { + "account_type": "Receivable" + }, + "2510 Vorsteuer Inland 10%": { + "account_type": "Tax" + }, + "2895 Schwebende Geldbewegugen": { + "account_type": "Bank" + }, + "2513 Vorsteuer Inland 5%": { + "account_type": "Tax" + }, + "2515 Vorsteuer Inland 20%": { + "account_type": "Tax" + }, + "2520 Vorsteuer aus innergemeinschaftlichem Erwerb 10%": { + "account_type": "Tax" + }, + "2525 Vorsteuer aus innergemeinschaftlichem Erwerb 20%": { + "account_type": "Tax" + }, + "2530 Vorsteuer \u00a719/Art 19 ( reverse charge ) ": { + "account_type": "Tax" + }, + "2690 Wertberichtigungen zu Wertpapieren und Anteilen": { + "account_type": "Receivable" + }, "root_type": "Asset" }, - "Summe Personalaufwand": { - "6000 bis 6190 L\u00f6hne": {}, - "6200 bis 6390 Geh\u00e4lter": {}, - "6400 bis 6440 Aufwendungen f\u00fcr Abfertigungen": {}, - "6450 bis 6490 Aufwendungen f\u00fcr Altersversorgung": {}, - "6500 bis 6550 Gesetzlicher Sozialaufwand Arbeiter": {}, - "6560 bis 6590 Gesetzlicher Sozialaufwand Angestellte": {}, - "6600 bis 6650 Lohnabh\u00e4ngige Abgaben und Pflichtbeitr\u00e4gte": {}, - "6660 bis 6690 Gehaltsabh\u00e4ngige Abgaben und Pflichtbeitr\u00e4gte": {}, - "6700 bis 6890 Sonstige Sozialaufwendungen": {}, - "Aufwandsstellenrechnung": {}, + "Klasse 4: Betriebliche Erträge": { + "4000 Erlöse 20 %": {"account_type": "Income Account"}, + "4020 Erl\u00f6se 0 % steuerbefreit": {"account_type": "Income Account"}, + "4010 Erl\u00f6se 10 %": {"account_type": "Income Account"}, + "4030 Erl\u00f6se 13 %": {"account_type": "Income Account"}, + "4040 Erl\u00f6se 0 % innergemeinschaftliche Lieferungen": {"account_type": "Income Account"}, + "4400 Erl\u00f6sreduktion 0 % steuerbefreit": {"account_type": "Expense Account"}, + "4410 Erl\u00f6sreduktion 10 %": {"account_type": "Expense Account"}, + "4420 Erl\u00f6sreduktion 20 %": {"account_type": "Expense Account"}, + "4430 Erl\u00f6sreduktion 13 %": {"account_type": "Expense Account"}, + "4440 Erl\u00f6sreduktion 0 % innergemeinschaftliche Lieferungen": {"account_type": "Expense Account"}, + "4500 Ver\u00e4nderungen des Bestandes an fertigen und unfertigen Erzeugn. sowie an noch nicht abrechenbaren Leistungen": {"account_type": "Income Account"}, + "4580 Aktivierte Eigenleistungen": {"account_type": "Income Account"}, + "4600 Erl\u00f6se aus dem Abgang vom Anlageverm\u00f6gen, ausgen. Finanzanlagen": {"account_type": "Income Account"}, + "4630 Ertr\u00e4ge aus dem Abgang vom Anlageverm\u00f6gen, ausgen. Finanzanlagen": {"account_type": "Income Account"}, + "4660 Ertr\u00e4ge aus der Zuschreibung zum Anlageverm\u00f6gen, ausgen. Finanzanlagen": {"account_type": "Income Account"}, + "4700 Ertr\u00e4ge aus der Aufl\u00f6sung von R\u00fcckstellungen": {"account_type": "Income Account"}, + "4800 \u00dcbrige betriebliche Ertr\u00e4ge": {"account_type": "Income Account"}, + "root_type": "Income" + }, + "Klasse 5: Aufwand f\u00fcr Material und Leistungen": { + "5000 Einkauf Partnerleistungen": {"account_type": "Cost of Goods Sold"}, + "5100 Verbrauch an Rohstoffen": {"account_type": "Cost of Goods Sold"}, + "5200 Verbrauch von bezogenen Fertig- und Einzelteilen": {"account_type": "Cost of Goods Sold"}, + "5300 Verbrauch von Hilfsstoffen": {"account_type": "Cost of Goods Sold"}, + "5340 Verbrauch Verpackungsmaterial": {"account_type": "Cost of Goods Sold"}, + "5470 Verbrauch von Kleinmaterial": {"account_type": "Cost of Goods Sold"}, + "5450 Verbrauch von Reinigungsmaterial": {"account_type": "Cost of Goods Sold"}, + "5400 Verbrauch von Betriebsstoffen": {"account_type": "Cost of Goods Sold"}, + "5500 Verbrauch von Werkzeugen und anderen Erzeugungshilfsmittel": {"account_type": "Cost of Goods Sold"}, + "5600 Verbrauch von Brenn- und Treibstoffen, Energie und Wasser": {"account_type": "Cost of Goods Sold"}, + "5700 Bearbeitung durch Dritte": {"account_type": "Cost of Goods Sold"}, + "5900 Aufwandsstellenrechnung Material": {"account_type": "Cost of Goods Sold"}, + "5820 Skontoertr\u00e4ge (20% USt.)": {"account_type": "Income Account"}, + "5810 Skontoertr\u00e4ge (10% USt.)": {"account_type": "Income Account"}, + "5010 Handelswareneinkauf 10 %": {"account_type": "Cost of Goods Sold"}, + "5020 Handelswareneinkauf 20 %": {"account_type": "Cost of Goods Sold"}, + "5040 Handelswareneinkauf innergemeinschaftlicher Erwerb 10 % VSt/10 % USt": {"account_type": "Cost of Goods Sold"}, + "5050 Handelswareneinkauf innergemeinschaftlicher Erwerb 20 % VSt/20 % USt": {"account_type": "Cost of Goods Sold"}, + "5070 Handelswareneinkauf innergemeinschaftlicher Erwerb ohne Vorsteuerabzug und 10 % USt": {"account_type": "Cost of Goods Sold"}, + "5080 Handelswareneinkauf innergemeinschaftlicher Erwerb ohne Vorsteuerabzug und 20 % USt": {"account_type": "Cost of Goods Sold"}, "root_type": "Expense" }, - "Summe Umlaufverm\u00f6gen": { - "2000 bis 2007 Forderungen aus Lief. und Leist. Inland": { - "account_type": "Receivable" - }, - "2100 bis 2120 Forderungen aus Lief. und Leist. EU": { - "account_type": "Receivable" - }, - "2150 bis 2170 Forderungen aus Lief. und Leist. Ausland": { - "account_type": "Receivable" - }, - "2200 bis 2220 Forderungen gegen\u00fcber verbundenen Unternehmen": { - "account_type": "Receivable" - }, - "2250 bis 2270 Forderungen gegen\u00fcber Unternehmen, mit denen ein Beteiligungsverh\u00e4ltnis besteht": { - "account_type": "Receivable" - }, - "2300 bis 2460 Sonstige Forderungen und Verm\u00f6gensgegenst\u00e4nde": { - "account_type": "Receivable" - }, - "2630 bis 2670 Sonstige Wertpapiere": { - "account_type": "Receivable" - }, - "2750 bis 2770 Kassenbest\u00e4nde in Fremdw\u00e4hrung": { - "account_type": "Receivable" - }, - "Aktive Rechnungsabrenzungsposten": { - "account_type": "Receivable" - }, - "Anteile an verbundenen Unternehmen": { - "account_type": "Receivable" - }, - "Bank / Guthaben bei Kreditinstituten": { - "account_type": "Receivable" - }, - "Besitzwechsel ...": { - "account_type": "Receivable" - }, - "Disagio": { - "account_type": "Receivable" - }, - "Eigene Anteile (Wertpapiere)": { - "account_type": "Receivable" - }, - "Einfuhrumsatzsteuer (bezahlt)": {}, - "Eingeforderte aber noch nicht eingezahlte Einlagen": { - "account_type": "Receivable" - }, - "Einzelwertberichtigungen zu Forderungen aus Lief. und Leist. Ausland": { - "account_type": "Receivable" - }, - "Einzelwertberichtigungen zu Forderungen aus Lief. und Leist. EU": { - "account_type": "Receivable" - }, - "Einzelwertberichtigungen zu Forderungen aus Lief. und Leist. Inland ": { - "account_type": "Receivable" - }, - "Einzelwertberichtigungen zu Forderungen gegen\u00fcber Unternehmen mit denen ein Beteiligungsverh\u00e4ltnis besteht": { - "account_type": "Receivable" - }, - "Einzelwertberichtigungen zu Forderungen gegen\u00fcber verbundenen Unternehmen": { - "account_type": "Receivable" - }, - "Einzelwertberichtigungen zu sonstigen Forderungen und Verm\u00f6gensgegenst\u00e4nden": { - "account_type": "Receivable" - }, - "Kassenbestand": { - "account_type": "Receivable" - }, - "Pauschalwertberichtigungen zu Forderungen aus Lief. und Leist. Ausland": { - "account_type": "Receivable" - }, - "Pauschalwertberichtigungen zu Forderungen aus Lief. und Leist. EU": { - "account_type": "Receivable" - }, - "Pauschalwertberichtigungen zu Forderungen aus Lief. und Leist. Inland ": { - "account_type": "Receivable" - }, - "Pauschalwertberichtigungen zu Forderungen gegen\u00fcber Unternehmen mit denen ein Beteiligungsverh\u00e4ltnis besteht": { - "account_type": "Receivable" - }, - "Pauschalwertberichtigungen zu Forderungen gegen\u00fcber verbundenen Unternehmen": { - "account_type": "Receivable" - }, - "Pauschalwertberichtigungen zu sonstigen Forderungen und Verm\u00f6gensgegenst\u00e4nden": { - "account_type": "Receivable" - }, - "Postwertzeichen": { - "account_type": "Receivable" - }, - "Schecks in Inlandsw\u00e4hrung": { - "account_type": "Receivable" - }, - "Sonstige Anteile": { - "account_type": "Receivable" - }, - "Stempelmarken": { - "account_type": "Receivable" - }, - "Steuerabgrenzung": { - "account_type": "Receivable" - }, - "Unterschiedsbetrag gem. Abschnitt XII Pensionskassengesetz": { - "account_type": "Receivable" - }, - "Unterschiedsbetrag zur gebotenen Pensionsr\u00fcckstellung": { - "account_type": "Receivable" - }, - "Vorsteuer": { - "account_type": "Receivable" - }, - "Vorsteuer aus ig. Erwerb 10%": { - "account_type": "Tax" - }, - "Vorsteuer aus ig. Erwerb 20%": { - "account_type": "Tax" - }, - "Vorsteuer \u00a719/Art 19 ( reverse charge ) ": { - "account_type": "Tax" - }, - "Wertberichtigungen": { - "account_type": "Receivable" - }, - "root_type": "Asset" - }, - "Summe Vorr\u00e4te": { - "1000 bis 1090 Bezugsverrechnung": {}, - "1100 bis 1190 Rohstoffe": {}, - "1200 bis 1290 Bezogene Teile": {}, - "1300 bis 1340 Hilfsstoffe": {}, - "1350 bis 1390 Betriebsstoffe": {}, - "1400 bis 1490 Unfertige Erzeugniss": {}, - "1500 bis 1590 Fertige Erzeugniss": {}, - "1600 bis 1690 Waren": {}, - "1700 bis 1790 Noch nicht abgerechenbare Leistungen": {}, - "1900 bis 1990 Wertberichtigungen": {}, - "geleistete Anzahlungen": {}, - "root_type": "Asset" - }, - "Summe Wareneinsatz": { - "5100 bis 5190 Verbrauch an Rohstoffen": {}, - "5200 bis 5290 Verbrauch von bezogenen Fertig- und Einzelteilen": {}, - "5300 bis 5390 Verbrauch von Hilfsstoffen": {}, - "5400 bis 5490 Verbrauch von Betriebsstoffen": {}, - "5500 bis 5590 Verbrauch von Werkzeugen und anderen Erzeugungshilfsmittel": {}, - "5600 bis 5690 Verbrauch von Brenn- und Treibstoffen, Energie und Wasser": {}, - "5700 bis 5790 Sonstige bezogene Herstellungsleistungen": {}, - "Aufwandsstellenrechnung": {}, - "Skontoertr\u00e4ge auf Materialaufwand": {}, - "Skontoertr\u00e4ge auf sonstige bezogene Herstellungsleistungen": {}, - "Wareneinkauf 10 %": {}, - "Wareneinkauf 20 %": {}, - "Wareneinkauf igErwerb 10 % VSt/10 % USt": {}, - "Wareneinkauf igErwerb 20 % VSt/20 % USt": {}, - "Wareneinkauf igErwerb ohne Vorsteuerabzug und 10 % USt": {}, - "Wareneinkauf igErwerb ohne Vorsteuerabzug und 20 % USt": {}, + "Klasse 6: Personalaufwand": { + "6000 L\u00f6hne": {"account_type": "Payable"}, + "6200 Geh\u00e4lter": {"account_type": "Payable"}, + "6400 Aufwendungen f\u00fcr Abfertigungen": {"account_type": "Payable"}, + "6450 Aufwendungen f\u00fcr Altersversorgung": {"account_type": "Payable"}, + "6500 Gesetzlicher Sozialaufwand Arbeiter": {"account_type": "Payable"}, + "6560 Gesetzlicher Sozialaufwand Angestellte": {"account_type": "Payable"}, + "6600 Lohnabh\u00e4ngige Abgaben und Pflichtbeitr\u00e4gte": {"account_type": "Payable"}, + "6660 Gehaltsabh\u00e4ngige Abgaben und Pflichtbeitr\u00e4gte": {"account_type": "Payable"}, + "6700 Sonstige Sozialaufwendungen": {"account_type": "Payable"}, + "6900 Aufwandsstellenrechnung Personal": {"account_type": "Payable"}, "root_type": "Expense" + }, + "Klasse 7: Abschreibungen und sonstige betriebliche Aufwendungen": { + "7010 Abschreibungen auf das Anlageverm\u00f6gen (ausgenommen Finanzanlagen)": {"account_type": "Depreciation"}, + "7100 Sonstige Steuern und Geb\u00fchren": {"account_type": "Tax"}, + "7200 Instandhaltung u. Reinigung durch Dritte, Entsorgung, Energie": {"account_type": "Expense Account"}, + "7300 Transporte durch Dritte": {"account_type": "Expense Account"}, + "7310 Fahrrad - Aufwand": {"account_type": "Expense Account"}, + "7320 Kfz - Aufwand": {"account_type": "Expense Account"}, + "7330 LKW - Aufwand": {"account_type": "Expense Account"}, + "7340 Lastenrad - Aufwand": {"account_type": "Expense Account"}, + "7350 Reise- und Fahraufwand": {"account_type": "Expense Account"}, + "7360 Tag- und N\u00e4chtigungsgelder": {"account_type": "Expense Account"}, + "7380 Nachrichtenaufwand": {"account_type": "Expense Account"}, + "7400 Miet- und Pachtaufwand": {"account_type": "Expense Account"}, + "7440 Leasingaufwand": {"account_type": "Expense Account"}, + "7480 Lizenzaufwand": {"account_type": "Expense Account"}, + "7500 Aufwand f\u00fcr beigestelltes Personal": {"account_type": "Expense Account"}, + "7540 Provisionen an Dritte": {"account_type": "Expense Account"}, + "7580 Aufsichtsratsverg\u00fctungen": {"account_type": "Expense Account"}, + "7610 Druckerzeugnisse und Vervielf\u00e4ltigungen": {"account_type": "Expense Account"}, + "7650 Werbung und Repr\u00e4sentationen": {"account_type": "Expense Account"}, + "7700 Versicherungen": {"account_type": "Expense Account"}, + "7750 Beratungs- und Pr\u00fcfungsaufwand": {"account_type": "Expense Account"}, + "7800 Forderungsverluste und Schadensf\u00e4lle": {"account_type": "Expense Account"}, + "7840 Verschiedene betriebliche Aufwendungen": {"account_type": "Expense Account"}, + "7910 Aufwandsstellenrechung der Hersteller": {"account_type": "Expense Account"}, + "7060 Sofortabschreibungen geringwertig": {"account_type": "Expense Account"}, + "7070 Abschreibungen vom Umlaufverm\u00f6gen, soweit diese die im Unternehmen \u00fcblichen Abschreibungen \u00fcbersteigen": {"account_type": "Depreciation"}, + "7900 Aufwandsstellenrechnung": {"account_type": "Expense Account"}, + "7770 Aus- und Fortbildung": {"account_type": "Expense Account"}, + "7820 Buchwert abgegangener Anlagen, ausgenommen Finanzanlagen": {"account_type": "Expense Account"}, + "7600 B\u00fcromaterial und Drucksorten": {"account_type": "Expense Account"}, + "7630 Fachliteratur und Zeitungen ": {"account_type": "Expense Account"}, + "7960 Herstellungskosten der zur Erzielung der Umsatzerl\u00f6se erbrachten Leistungen": {"account_type": "Expense Account"}, + "7780 Mitgliedsbeitr\u00e4ge": {"account_type": "Expense Account"}, + "7880 Skontoertr\u00e4ge auf sonstige betriebliche Aufwendungen": {"account_type": "Expense Account"}, + "7990 Sonstige betrieblichen Aufwendungen": {"account_type": "Expense Account"}, + "7680 Spenden und Trinkgelder": {"account_type": "Expense Account"}, + "7790 Spesen des Geldverkehrs": {"account_type": "Expense Account"}, + "7830 Verluste aus dem Abgang vom Anlageverm\u00f6gen, ausgenommen Finanzanlagen": {"account_type": "Expense Account"}, + "7970 Vertriebskosten": {"account_type": "Expense Account"}, + "7980 Verwaltungskosten": {"account_type": "Expense Account"}, + "root_type": "Expense" + }, + "Klasse 8: Finanz- und ausserordentliche Ertr\u00e4ge und Aufwendungen": { + "8000 Ertr\u00e4ge aus Beteiligungen": {"account_type": "Income Account"}, + "8050 Ertr\u00e4ge aus anderen Wertpapieren und Ausleihungen des Finanzanlageverm\u00f6gens": {"account_type": "Income Account"}, + "8100 Zinsen aus Bankguthaben": {"account_type": "Income Account"}, + "8110 Zinsen aus gewaehrten Darlehen": {"account_type": "Income Account"}, + "8130 Verzugszinsenertraege": {"account_type": "Income Account"}, + "8220 Aufwendungen aus Beteiligungen": {"account_type": "Expense Account"}, + "8260 Aufwendungen aus sonst. Fiananzanlagen und aus Wertpapieren des Umlaufverm\u00f6gens": {}, + "8280 Zinsen und \u00e4hnliche Aufwendungem": {"account_type": "Expense Account"}, + "8400 Au\u00dferordentliche Ertr\u00e4ge": {"account_type": "Income Account"}, + "8450 Au\u00dferordentliche Aufwendungen": {"account_type": "Expense Account"}, + "8500 Steuern vom Einkommen und vom Ertrag": { + "account_type": "Tax" + }, + "8600 Aufl\u00f6sung unversteuerten R\u00fccklagen": {"account_type": "Income Account"}, + "8700 Aufl\u00f6sung von Kapitalr\u00fccklagen": {"account_type": "Income Account"}, + "8750 Aufl\u00f6sung von Gewinnr\u00fccklagen": {"account_type": "Income Account"}, + "8800 Zuweisung zu unversteuerten R\u00fccklagen": {"account_type": "Expense Account"}, + "8900 Zuweisung zu Gewinnr\u00fccklagen": {"account_type": "Expense Account"}, + "8100 Buchwert abgegangener Beteiligungen": {"account_type": "Expense Account"}, + "8130 Buchwert abgegangener Wertpapiere des Umlaufverm\u00f6gens": {"account_type": "Expense Account"}, + "8120 Buchwert abgegangener sonstiger Finanzanlagen": {"account_type": "Expense Account"}, + "8990 Gewinnabfuhr bzw. Verlust\u00fcberrechnung aus Ergebnisabf\u00fchrungsvertr\u00e4gen": {"account_type": "Expense Account"}, + "8350 nicht ausgenutzte Lieferantenskonti": {"account_type": "Expense Account"}, + "root_type": "Income" + }, + "Klasse 9 Passiva: Eigenkapital, R\u00fccklagen, stille Einlagen, Abschlusskonten": { + "9000 Gezeichnetes bzw. gewidmetes Kapital": { + "account_type": "Equity" + }, + "9200 Kapitalr\u00fccklagen": { + "account_type": "Equity" + }, + "9300 Gewinnr\u00fccklagen": { + "account_type": "Equity" + }, + "9400 Bewertungsreserven uns sonst. unversteuerte R\u00fccklagen": { + "account_type": "Equity" + }, + "9600 Private Entnahmen": {"account_type": "Equity"}, + "9610 Privatsteuern": {"account_type": "Equity"}, + "9700 Einlagen stiller Gesellschafter ": {"account_type": "Equity"}, + "9900 Evidenzkonto": {"account_type": "Equity"}, + "9800 Er\u00f6ffnungsbilanzkonto (EBK)": {"account_type": "Equity"}, + "9880 Jahresergebnis laut Gewinn- und Verlustrechnung (G+V)": {"account_type": "Equity"}, + "9850 Schlussbilanzkonto (SBK)": {"account_type": "Round Off"}, + "9190 nicht eingeforderte ausstehende Einlagen und berechtigte Entnahmen von Gesellschaftern": { + "account_type": "Equity" + }, + "root_type": "Equity" } - } -} + } + } diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR03_gnucash.json b/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR03_gnucash.json index ee501f664b..741d4283e2 100644 --- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR03_gnucash.json +++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR03_gnucash.json @@ -1,38 +1,38 @@ { - "country_code": "de", - "name": "SKR03 mit Kontonummern", - "tree": { - "Aktiva": { - "is_group": 1, + "country_code": "de", + "name": "SKR03 mit Kontonummern", + "tree": { + "Aktiva": { + "is_group": 1, "root_type": "Asset", - "A - Anlagevermögen": { - "is_group": 1, - "EDV-Software": { - "account_number": "0027", - "account_type": "Fixed Asset" - }, - "Gesch\u00e4ftsausstattung": { - "account_number": "0410", - "account_type": "Fixed Asset" - }, - "B\u00fcroeinrichtung": { - "account_number": "0420", - "account_type": "Fixed Asset" - }, - "Darlehen": { - "account_number": "0565" - }, - "Maschinen": { - "account_number": "0210", - "account_type": "Fixed Asset" - }, - "Betriebsausstattung": { - "account_number": "0400", - "account_type": "Fixed Asset" - }, - "Ladeneinrichtung": { - "account_number": "0430", - "account_type": "Fixed Asset" + "A - Anlagevermögen": { + "is_group": 1, + "EDV-Software": { + "account_number": "0027", + "account_type": "Fixed Asset" + }, + "Geschäftsausstattung": { + "account_number": "0410", + "account_type": "Fixed Asset" + }, + "Büroeinrichtung": { + "account_number": "0420", + "account_type": "Fixed Asset" + }, + "Darlehen": { + "account_number": "0565" + }, + "Maschinen": { + "account_number": "0210", + "account_type": "Fixed Asset" + }, + "Betriebsausstattung": { + "account_number": "0400", + "account_type": "Fixed Asset" + }, + "Ladeneinrichtung": { + "account_number": "0430", + "account_type": "Fixed Asset" }, "Accumulated Depreciation": { "account_type": "Accumulated Depreciation" @@ -60,36 +60,46 @@ "Durchlaufende Posten": { "account_number": "1590" }, - "Gewinnermittlung \u00a74/3 nicht Ergebniswirksam": { + "Verrechnungskonto Gewinnermittlung § 4 Abs. 3 EStG, nicht ergebniswirksam": { "account_number": "1371" }, "Abziehbare Vorsteuer": { - "account_type": "Tax", "is_group": 1, - "Abziehbare Vorsteuer 7%": { - "account_number": "1571" + "Abziehbare Vorsteuer 7 %": { + "account_number": "1571", + "account_type": "Tax", + "tax_rate": 7.0 }, - "Abziehbare Vorsteuer 19%": { - "account_number": "1576" + "Abziehbare Vorsteuer 19 %": { + "account_number": "1576", + "account_type": "Tax", + "tax_rate": 19.0 }, - "Abziehbare Vorsteuer nach \u00a713b UStG 19%": { - "account_number": "1577" - }, - "Leistungen \u00a713b UStG 19% Vorsteuer, 19% Umsatzsteuer": { - "account_number": "3120" + "Abziehbare Vorsteuer nach § 13b UStG 19 %": { + "account_number": "1577", + "account_type": "Tax", + "tax_rate": 19.0 } } }, "III. Wertpapiere": { - "is_group": 1 + "is_group": 1, + "Anteile an verbundenen Unternehmen (Umlaufvermögen)": { + "account_number": "1340" + }, + "Anteile an herrschender oder mit Mehrheit beteiligter Gesellschaft": { + "account_number": "1344" + }, + "Sonstige Wertpapiere": { + "account_number": "1348" + } }, "IV. Kassenbestand, Bundesbankguthaben, Guthaben bei Kreditinstituten und Schecks.": { "is_group": 1, "Kasse": { - "account_type": "Cash", "is_group": 1, + "account_type": "Cash", "Kasse": { - "is_group": 1, "account_number": "1000", "account_type": "Cash" } @@ -111,21 +121,21 @@ "C - Rechnungsabgrenzungsposten": { "is_group": 1, "Aktive Rechnungsabgrenzung": { - "account_number": "0980" + "account_number": "0980" } }, "D - Aktive latente Steuern": { "is_group": 1, "Aktive latente Steuern": { - "account_number": "0983" + "account_number": "0983" } }, "E - Aktiver Unterschiedsbetrag aus der Vermögensverrechnung": { "is_group": 1 } - }, - "Passiva": { - "is_group": 1, + }, + "Passiva": { + "is_group": 1, "root_type": "Liability", "A. Eigenkapital": { "is_group": 1, @@ -200,26 +210,32 @@ }, "Umsatzsteuer": { "is_group": 1, - "account_type": "Tax", - "Umsatzsteuer 7%": { - "account_number": "1771" + "Umsatzsteuer 7 %": { + "account_number": "1771", + "account_type": "Tax", + "tax_rate": 7.0 }, - "Umsatzsteuer 19%": { - "account_number": "1776" + "Umsatzsteuer 19 %": { + "account_number": "1776", + "account_type": "Tax", + "tax_rate": 19.0 }, "Umsatzsteuer-Vorauszahlung": { - "account_number": "1780" + "account_number": "1780", + "account_type": "Tax" }, "Umsatzsteuer-Vorauszahlung 1/11": { "account_number": "1781" }, - "Umsatzsteuer \u00a7 13b UStG 19%": { - "account_number": "1787" + "Umsatzsteuer nach § 13b UStG 19 %": { + "account_number": "1787", + "account_type": "Tax", + "tax_rate": 19.0 }, "Umsatzsteuer Vorjahr": { "account_number": "1790" }, - "Umsatzsteuer fr\u00fchere Jahre": { + "Umsatzsteuer frühere Jahre": { "account_number": "1791" } } @@ -234,44 +250,56 @@ "E. Passive latente Steuern": { "is_group": 1 } - }, - "Erl\u00f6se u. Ertr\u00e4ge 2/8": { - "is_group": 1, - "root_type": "Income", - "Erl\u00f6skonten 8": { + }, + "Erlöse u. Erträge 2/8": { + "is_group": 1, + "root_type": "Income", + "Erlöskonten 8": { "is_group": 1, - "Erl\u00f6se": { - "account_number": "8200", - "account_type": "Income Account" - }, - "Erl\u00f6se USt. 19%": { - "account_number": "8400", - "account_type": "Income Account" - }, - "Erl\u00f6se USt. 7%": { - "account_number": "8300", - "account_type": "Income Account" - } - }, - "Ertragskonten 2": { - "is_group": 1, - "sonstige Zinsen und \u00e4hnliche Ertr\u00e4ge": { - "account_number": "2650", - "account_type": "Income Account" - }, - "Au\u00dferordentliche Ertr\u00e4ge": { - "account_number": "2500", - "account_type": "Income Account" - }, - "Sonstige Ertr\u00e4ge": { - "account_number": "2700", - "account_type": "Income Account" - } - } - }, - "Aufwendungen 2/4": { - "is_group": 1, + "Erlöse": { + "account_number": "8200", + "account_type": "Income Account" + }, + "Erlöse USt. 19 %": { + "account_number": "8400", + "account_type": "Income Account" + }, + "Erlöse USt. 7 %": { + "account_number": "8300", + "account_type": "Income Account" + } + }, + "Ertragskonten 2": { + "is_group": 1, + "sonstige Zinsen und ähnliche Erträge": { + "account_number": "2650", + "account_type": "Income Account" + }, + "Außerordentliche Erträge": { + "account_number": "2500", + "account_type": "Income Account" + }, + "Sonstige Erträge": { + "account_number": "2700", + "account_type": "Income Account" + } + } + }, + "Aufwendungen 2/4": { + "is_group": 1, "root_type": "Expense", + "Fremdleistungen": { + "account_number": "3100", + "account_type": "Expense Account" + }, + "Fremdleistungen ohne Vorsteuer": { + "account_number": "3109", + "account_type": "Expense Account" + }, + "Bauleistungen eines im Inland ansässigen Unternehmers 19 % Vorsteuer und 19 % Umsatzsteuer": { + "account_number": "3120", + "account_type": "Expense Account" + }, "Wareneingang": { "account_number": "3200" }, @@ -298,234 +326,234 @@ "Gegenkonto 4996-4998": { "account_number": "4999" }, - "Abschreibungen": { - "is_group": 1, + "Abschreibungen": { + "is_group": 1, "Abschreibungen auf Sachanlagen (ohne AfA auf Kfz und Gebäude)": { - "account_number": "4830", - "account_type": "Accumulated Depreciation" + "account_number": "4830", + "account_type": "Accumulated Depreciation" }, "Abschreibungen auf Gebäude": { - "account_number": "4831", - "account_type": "Depreciation" + "account_number": "4831", + "account_type": "Depreciation" }, "Abschreibungen auf Kfz": { - "account_number": "4832", - "account_type": "Depreciation" + "account_number": "4832", + "account_type": "Depreciation" }, "Sofortabschreibung GWG": { - "account_number": "4855", - "account_type": "Expense Account" + "account_number": "4855", + "account_type": "Expense Account" } - }, - "Kfz-Kosten": { - "is_group": 1, - "Kfz-Steuer": { - "account_number": "4510", - "account_type": "Expense Account" - }, - "Kfz-Versicherungen": { - "account_number": "4520", - "account_type": "Expense Account" - }, - "laufende Kfz-Betriebskosten": { - "account_number": "4530", - "account_type": "Expense Account" - }, - "Kfz-Reparaturen": { - "account_number": "4540", - "account_type": "Expense Account" - }, - "Fremdfahrzeuge": { - "account_number": "4570", - "account_type": "Expense Account" - }, - "sonstige Kfz-Kosten": { - "account_number": "4580", - "account_type": "Expense Account" - } - }, - "Personalkosten": { - "is_group": 1, - "Geh\u00e4lter": { - "account_number": "4120", - "account_type": "Expense Account" - }, - "gesetzliche soziale Aufwendungen": { - "account_number": "4130", - "account_type": "Expense Account" - }, - "Aufwendungen f\u00fcr Altersvorsorge": { - "account_number": "4165", - "account_type": "Expense Account" - }, - "Verm\u00f6genswirksame Leistungen": { - "account_number": "4170", - "account_type": "Expense Account" - }, - "Aushilfsl\u00f6hne": { - "account_number": "4190", - "account_type": "Expense Account" - } - }, - "Raumkosten": { - "is_group": 1, - "Miete und Nebenkosten": { - "account_number": "4210", - "account_type": "Expense Account" - }, - "Gas, Wasser, Strom (Verwaltung, Vertrieb)": { - "account_number": "4240", - "account_type": "Expense Account" - }, - "Reinigung": { - "account_number": "4250", - "account_type": "Expense Account" - } - }, - "Reparatur/Instandhaltung": { - "is_group": 1, - "Reparatur u. Instandh. von Anlagen/Maschinen u. Betriebs- u. Gesch\u00e4ftsausst.": { - "account_number": "4805", - "account_type": "Expense Account" - } - }, - "Versicherungsbeitr\u00e4ge": { - "is_group": 1, - "Versicherungen": { - "account_number": "4360", - "account_type": "Expense Account" - }, - "Beitr\u00e4ge": { - "account_number": "4380", - "account_type": "Expense Account" - }, - "sonstige Ausgaben": { - "account_number": "4390", - "account_type": "Expense Account" - }, - "steuerlich abzugsf\u00e4hige Versp\u00e4tungszuschl\u00e4ge und Zwangsgelder": { - "account_number": "4396", - "account_type": "Expense Account" - } - }, - "Werbe-/Reisekosten": { - "is_group": 1, - "Werbekosten": { - "account_number": "4610", - "account_type": "Expense Account" - }, - "Aufmerksamkeiten": { - "account_number": "4653", - "account_type": "Expense Account" - }, - "nicht abzugsf\u00e4hige Betriebsausg. aus Werbe-, Repr\u00e4s.- u. Reisekosten": { - "account_number": "4665", - "account_type": "Expense Account" - }, - "Reisekosten Unternehmer": { - "account_number": "4670", - "account_type": "Expense Account" - } - }, - "verschiedene Kosten": { - "is_group": 1, - "Porto": { - "account_number": "4910", - "account_type": "Expense Account" - }, - "Telekom": { - "account_number": "4920", - "account_type": "Expense Account" - }, - "Mobilfunk D2": { - "account_number": "4921", - "account_type": "Expense Account" - }, - "Internet": { - "account_number": "4922", - "account_type": "Expense Account" - }, - "B\u00fcrobedarf": { - "account_number": "4930", - "account_type": "Expense Account" - }, - "Zeitschriften, B\u00fccher": { - "account_number": "4940", - "account_type": "Expense Account" - }, - "Fortbildungskosten": { - "account_number": "4945", - "account_type": "Expense Account" - }, - "Buchf\u00fchrungskosten": { - "account_number": "4955", - "account_type": "Expense Account" - }, - "Abschlu\u00df- u. Pr\u00fcfungskosten": { - "account_number": "4957", - "account_type": "Expense Account" - }, - "Nebenkosten des Geldverkehrs": { - "account_number": "4970", - "account_type": "Expense Account" - }, - "Werkzeuge und Kleinger\u00e4te": { - "account_number": "4985", - "account_type": "Expense Account" - } - }, - "Zinsaufwendungen": { - "is_group": 1, - "Zinsaufwendungen f\u00fcr kurzfristige Verbindlichkeiten": { - "account_number": "2110", - "account_type": "Expense Account" - }, - "Zinsaufwendungen f\u00fcr KFZ Finanzierung": { - "account_number": "2121", - "account_type": "Expense Account" - } - } - }, - "Anfangsbestand 9": { - "is_group": 1, - "root_type": "Equity", - "Saldenvortragskonten": { - "is_group": 1, - "Saldenvortrag Sachkonten": { - "account_number": "9000" - }, - "Saldenvortr\u00e4ge Debitoren": { - "account_number": "9008" - }, - "Saldenvortr\u00e4ge Kreditoren": { - "account_number": "9009" - } - } - }, - "Privatkonten 1": { - "is_group": 1, - "root_type": "Equity", - "Privatentnahmen/-einlagen": { - "is_group": 1, - "Privatentnahme allgemein": { - "account_number": "1800" - }, - "Privatsteuern": { - "account_number": "1810" - }, - "Sonderausgaben beschr\u00e4nkt abzugsf\u00e4hig": { - "account_number": "1820" - }, - "Sonderausgaben unbeschr\u00e4nkt abzugsf\u00e4hig": { - "account_number": "1830" - }, - "Au\u00dfergew\u00f6hnliche Belastungen": { - "account_number": "1850" - }, - "Privateinlagen": { - "account_number": "1890" - } - } - } - } + }, + "Kfz-Kosten": { + "is_group": 1, + "Kfz-Steuer": { + "account_number": "4510", + "account_type": "Expense Account" + }, + "Kfz-Versicherungen": { + "account_number": "4520", + "account_type": "Expense Account" + }, + "laufende Kfz-Betriebskosten": { + "account_number": "4530", + "account_type": "Expense Account" + }, + "Kfz-Reparaturen": { + "account_number": "4540", + "account_type": "Expense Account" + }, + "Fremdfahrzeuge": { + "account_number": "4570", + "account_type": "Expense Account" + }, + "sonstige Kfz-Kosten": { + "account_number": "4580", + "account_type": "Expense Account" + } + }, + "Personalkosten": { + "is_group": 1, + "Gehälter": { + "account_number": "4120", + "account_type": "Expense Account" + }, + "gesetzliche soziale Aufwendungen": { + "account_number": "4130", + "account_type": "Expense Account" + }, + "Aufwendungen für Altersvorsorge": { + "account_number": "4165", + "account_type": "Expense Account" + }, + "Vermögenswirksame Leistungen": { + "account_number": "4170", + "account_type": "Expense Account" + }, + "Aushilfslöhne": { + "account_number": "4190", + "account_type": "Expense Account" + } + }, + "Raumkosten": { + "is_group": 1, + "Miete und Nebenkosten": { + "account_number": "4210", + "account_type": "Expense Account" + }, + "Gas, Wasser, Strom (Verwaltung, Vertrieb)": { + "account_number": "4240", + "account_type": "Expense Account" + }, + "Reinigung": { + "account_number": "4250", + "account_type": "Expense Account" + } + }, + "Reparatur/Instandhaltung": { + "is_group": 1, + "Reparaturen und Instandhaltungen von anderen Anlagen und Betriebs- und Geschäftsausstattung": { + "account_number": "4805", + "account_type": "Expense Account" + } + }, + "Versicherungsbeiträge": { + "is_group": 1, + "Versicherungen": { + "account_number": "4360", + "account_type": "Expense Account" + }, + "Beiträge": { + "account_number": "4380", + "account_type": "Expense Account" + }, + "sonstige Ausgaben": { + "account_number": "4390", + "account_type": "Expense Account" + }, + "steuerlich abzugsfähige Verspätungszuschläge und Zwangsgelder": { + "account_number": "4396", + "account_type": "Expense Account" + } + }, + "Werbe-/Reisekosten": { + "is_group": 1, + "Werbekosten": { + "account_number": "4610", + "account_type": "Expense Account" + }, + "Aufmerksamkeiten": { + "account_number": "4653", + "account_type": "Expense Account" + }, + "nicht abzugsfähige Betriebsausg. aus Werbe-, Repräs.- u. Reisekosten": { + "account_number": "4665", + "account_type": "Expense Account" + }, + "Reisekosten Unternehmer": { + "account_number": "4670", + "account_type": "Expense Account" + } + }, + "verschiedene Kosten": { + "is_group": 1, + "Porto": { + "account_number": "4910", + "account_type": "Expense Account" + }, + "Telekom": { + "account_number": "4920", + "account_type": "Expense Account" + }, + "Mobilfunk D2": { + "account_number": "4921", + "account_type": "Expense Account" + }, + "Internet": { + "account_number": "4922", + "account_type": "Expense Account" + }, + "Bürobedarf": { + "account_number": "4930", + "account_type": "Expense Account" + }, + "Zeitschriften, Bücher": { + "account_number": "4940", + "account_type": "Expense Account" + }, + "Fortbildungskosten": { + "account_number": "4945", + "account_type": "Expense Account" + }, + "Buchführungskosten": { + "account_number": "4955", + "account_type": "Expense Account" + }, + "Abschluß- u. Prüfungskosten": { + "account_number": "4957", + "account_type": "Expense Account" + }, + "Nebenkosten des Geldverkehrs": { + "account_number": "4970", + "account_type": "Expense Account" + }, + "Werkzeuge und Kleingeräte": { + "account_number": "4985", + "account_type": "Expense Account" + } + }, + "Zinsaufwendungen": { + "is_group": 1, + "Zinsaufwendungen für kurzfristige Verbindlichkeiten": { + "account_number": "2110", + "account_type": "Expense Account" + }, + "Zinsaufwendungen für KFZ Finanzierung": { + "account_number": "2121", + "account_type": "Expense Account" + } + } + }, + "Anfangsbestand 9": { + "is_group": 1, + "root_type": "Equity", + "Saldenvortragskonten": { + "is_group": 1, + "Saldenvortrag Sachkonten": { + "account_number": "9000" + }, + "Saldenvorträge Debitoren": { + "account_number": "9008" + }, + "Saldenvorträge Kreditoren": { + "account_number": "9009" + } + } + }, + "Privatkonten 1": { + "is_group": 1, + "root_type": "Equity", + "Privatentnahmen/-einlagen": { + "is_group": 1, + "Privatentnahme allgemein": { + "account_number": "1800" + }, + "Privatsteuern": { + "account_number": "1810" + }, + "Sonderausgaben beschränkt abzugsfähig": { + "account_number": "1820" + }, + "Sonderausgaben unbeschränkt abzugsfähig": { + "account_number": "1830" + }, + "Außergewöhnliche Belastungen": { + "account_number": "1850" + }, + "Privateinlagen": { + "account_number": "1890" + } + } + } + } } diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/hu_chart_of_accounts_for_microenterprises_with_account_number.json b/erpnext/accounts/doctype/account/chart_of_accounts/verified/hu_chart_of_accounts_for_microenterprises_with_account_number.json new file mode 100644 index 0000000000..2cd6c0fc61 --- /dev/null +++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/hu_chart_of_accounts_for_microenterprises_with_account_number.json @@ -0,0 +1,1654 @@ +{ + "tree": { + "SZ\u00c1MLAOSZT\u00c1LY BEFEKTETETT ESZK\u00d6Z\u00d6K": { + "account_number": 1, + "root_type": "Asset", + "is_group": 1, + "IMMATERI\u00c1LIS JAVAK": { + "account_number": 11, + "root_type": "Asset", + "is_group": 1, + "Vagyoni \u00e9rt\u00e9k\u0171 jogok": { + "account_number": 113, + "root_type": "Asset", + "is_group": 1, + "Vagyoni \u00e9rt\u00e9k\u0171 jogok brutt\u00f3 \u00e9rt\u00e9ke": { + "account_number": 1131, + "root_type": "Asset" + }, + "Vagyoni \u00e9rt\u00e9k\u0171 jogok terven fel\u00fcli \u00e9rt\u00e9kcs\u00f6kken\u00e9se \u00e9s annak vissza\u00edr\u00e1sa": { + "account_number": 1138, + "root_type": "Asset" + }, + "Vagyoni \u00e9rt\u00e9k\u0171 jogok terv szerinti \u00e9rt\u00e9kcs\u00f6kken\u00e9se": { + "account_number": 1139, + "root_type": "Asset" + } + }, + "Szellemi term\u00e9kek": { + "account_number": 114, + "root_type": "Asset", + "is_group": 1, + "Szellemi term\u00e9kek brutt\u00f3 \u00e9rt\u00e9ke": { + "account_number": 1141, + "root_type": "Asset" + }, + "Szellemi term\u00e9kek terven fel\u00fcli \u00e9rt\u00e9kcs\u00f6kken\u00e9se \u00e9s annak vissza\u00edr\u00e1sa": { + "account_number": 1148, + "root_type": "Asset" + }, + "Szellemi term\u00e9kek terv szerinti \u00e9rt\u00e9kcs\u00f6kken\u00e9se": { + "account_number": 1149, + "root_type": "Asset" + } + }, + "Kis \u00e9rt\u00e9k\u0171 immateri\u00e1lis javak": { + "account_number": 119, + "root_type": "Asset", + "is_group": 1, + "Kis \u00e9rt\u00e9k\u0171 immateri\u00e1lis javak brutt\u00f3 \u00e9rt\u00e9ke": { + "account_number": 1191, + "root_type": "Asset" + }, + "Kis \u00e9rt\u00e9k\u0171 immateri\u00e1lis javak terv szerinti \u00e9rt\u00e9kcs\u00f6kken\u00e9se": { + "account_number": 1199, + "root_type": "Asset" + } + } + }, + "INGATLANOK \u00c9S KAPCSOL\u00d3D\u00d3 VAGYONI \u00c9RT\u00c9K\u0170 JOGOK": { + "account_number": 12, + "root_type": "Asset", + "is_group": 1, + "Telkek, f\u00f6ldter\u00fcletek": { + "account_number": 121, + "root_type": "Asset", + "is_group": 1, + "Telkek, f\u00f6ldter\u00fcletek brutt\u00f3 \u00e9rt\u00e9ke": { + "account_number": 1211, + "root_type": "Asset" + }, + "Telkek, f\u00f6ldter\u00fcletek terven fel\u00fcli \u00e9rt\u00e9kcs\u00f6kken\u00e9se \u00e9s annak vissza\u00edr\u00e1sa": { + "account_number": 1218, + "root_type": "Asset" + }, + "Telkek, f\u00f6ldter\u00fcletek terv szerinti \u00e9rt\u00e9kcs\u00f6kken\u00e9se": { + "account_number": 1219, + "root_type": "Asset" + } + }, + "Ingatlanokhoz kapcsol\u00f3d\u00f3 vagyoni \u00e9rt\u00e9k\u0171 jogok": { + "account_number": 122, + "root_type": "Asset", + "is_group": 1, + "Ingatlanokhoz kapcsol\u00f3d\u00f3 vagyoni \u00e9rt\u00e9k\u0171 jogok brutt\u00f3 \u00e9rt\u00e9ke": { + "account_number": 1221, + "root_type": "Asset" + }, + "Ingatlanokhoz kapcsol\u00f3d\u00f3 vagyoni \u00e9rt\u00e9k\u0171 jogok terven fel\u00fcli \u00e9rt\u00e9kcs\u00f6kken\u00e9se \u00e9s annak vissza\u00edr\u00e1sa": { + "account_number": 1228, + "root_type": "Asset" + }, + "Ingatlanokhoz kapcsol\u00f3d\u00f3 vagyoni \u00e9rt\u00e9k\u0171 jogok terv szerinti \u00e9rt\u00e9kcs\u00f6kken\u00e9se": { + "account_number": 1229, + "root_type": "Asset" + } + }, + "\u00c9p\u00fcletek, \u00e9p\u00fcletr\u00e9szek, tulajdoni h\u00e1nyadok": { + "account_number": 123, + "root_type": "Asset", + "is_group": 1, + "\u00c9p\u00fcletek, \u00e9p\u00fcletr\u00e9szek, tulajdoni h\u00e1nyadok brutt\u00f3 \u00e9rt\u00e9ke": { + "account_number": 1231, + "root_type": "Asset" + }, + "\u00c9p\u00fcletek, \u00e9p\u00fcletr\u00e9szek, tulajdoni h\u00e1nyadok terven fel\u00fcli \u00e9rt\u00e9kcs\u00f6kken\u00e9se \u00e9s annak vissza\u00edr\u00e1sa": { + "account_number": 1238, + "root_type": "Asset" + }, + "\u00c9p\u00fcletek, \u00e9p\u00fcletr\u00e9szek, tulajdoni h\u00e1nyadok terv szerinti \u00e9rt\u00e9kcs\u00f6kken\u00e9se": { + "account_number": 1239, + "root_type": "Asset" + } + }, + "Egy\u00e9b ingatlanok": { + "account_number": 124, + "root_type": "Asset", + "is_group": 1, + "Egy\u00e9b ingatlanok brutt\u00f3 \u00e9rt\u00e9ke": { + "account_number": 1241, + "root_type": "Asset" + }, + "Egy\u00e9b ingatlanok terven fel\u00fcli \u00e9rt\u00e9kcs\u00f6kken\u00e9se \u00e9s annak vissza\u00edr\u00e1sa": { + "account_number": 1248, + "root_type": "Asset" + }, + "Egy\u00e9b ingatlanok terv szerinti \u00e9rt\u00e9kcs\u00f6kken\u00e9se": { + "account_number": 1249, + "root_type": "Asset" + } + }, + "Kis \u00e9rt\u00e9k\u0171 ingatlanok": { + "account_number": 129, + "root_type": "Asset", + "is_group": 1, + "Kis \u00e9rt\u00e9k\u0171 ingatlanok brutt\u00f3 \u00e9rt\u00e9ke": { + "account_number": 1291, + "root_type": "Asset" + }, + "Kis \u00e9rt\u00e9k\u0171 ingatlanok terv szerinti \u00e9rt\u00e9kcs\u00f6kken\u00e9se": { + "account_number": 1299, + "root_type": "Asset" + } + } + }, + "M\u0170SZAKI BERENDEZ\u00c9SEK, G\u00c9PEK, J\u00c1RM\u0170VEK": { + "account_number": 13, + "root_type": "Asset", + "is_group": 1, + "Termel\u0151 g\u00e9pek, berendez\u00e9sek": { + "account_number": 131, + "root_type": "Asset", + "is_group": 1, + "Termel\u0151 g\u00e9pek, berendez\u00e9sek brutt\u00f3 \u00e9rt\u00e9ke": { + "account_number": 1311, + "root_type": "Asset" + }, + "Termel\u0151 g\u00e9pek, berendez\u00e9sek terven fel\u00fcli \u00e9rt\u00e9kcs\u00f6kken\u00e9se \u00e9s annak vissza\u00edr\u00e1sa": { + "account_number": 1318, + "root_type": "Asset" + }, + "Termel\u0151 g\u00e9pek, berendez\u00e9sek terv szerinti \u00e9rt\u00e9kcs\u00f6kken\u00e9se": { + "account_number": 1319, + "root_type": "Asset" + } + }, + "M\u0171szaki j\u00e1rm\u0171vek": { + "account_number": 132, + "root_type": "Asset", + "is_group": 1, + "M\u0171szaki j\u00e1rm\u0171vek brutt\u00f3 \u00e9rt\u00e9ke": { + "account_number": 1321, + "root_type": "Asset" + }, + "M\u0171szaki j\u00e1rm\u0171vek terven fel\u00fcli \u00e9rt\u00e9kcs\u00f6kken\u00e9se \u00e9s annak vissza\u00edr\u00e1sa": { + "account_number": 1328, + "root_type": "Asset" + }, + "M\u0171szaki j\u00e1rm\u0171vek terv szerinti \u00e9rt\u00e9kcs\u00f6kken\u00e9se": { + "account_number": 1329, + "root_type": "Asset" + } + }, + "Ki nem emelt m\u0171szaki berendez\u00e9sek, g\u00e9pek, j\u00e1rm\u0171vek": { + "account_number": 133, + "root_type": "Asset", + "is_group": 1, + "Ki nem emelt m\u0171szaki berendez\u00e9sek, g\u00e9pek, j\u00e1rm\u0171vek brutt\u00f3 \u00e9rt\u00e9ke": { + "account_number": 1331, + "root_type": "Asset" + }, + "Ki nem emelt m\u0171szaki berendez\u00e9sek, g\u00e9pek, j\u00e1rm\u0171vek terven fel\u00fcli \u00e9rt\u00e9kcs\u00f6kken\u00e9se \u00e9s annak vissza\u00edr\u00e1sa": { + "account_number": 1338, + "root_type": "Asset" + }, + "Ki nem emelt m\u0171szaki berendez\u00e9sek, g\u00e9pek, j\u00e1rm\u0171vek terv szerinti \u00e9rt\u00e9kcs\u00f6kken\u00e9se": { + "account_number": 1339, + "root_type": "Asset" + } + }, + "Kis \u00e9rt\u00e9k\u0171 m\u0171szaki berendez\u00e9sek, g\u00e9pek, j\u00e1rm\u0171vek": { + "account_number": 139, + "root_type": "Asset", + "is_group": 1, + "Kis \u00e9rt\u00e9k\u0171 m\u0171szaki berendez\u00e9sek, g\u00e9pek, j\u00e1rm\u0171vek brutt\u00f3 \u00e9rt\u00e9ke": { + "account_number": 1391, + "root_type": "Asset" + }, + "Kis \u00e9rt\u00e9k\u0171 m\u0171szaki berendez\u00e9sek, g\u00e9pek, j\u00e1rm\u0171vek terv szerinti \u00e9rt\u00e9kcs\u00f6kken\u00e9se": { + "account_number": 1399, + "root_type": "Asset" + } + } + }, + "EGY\u00c9B BERENDEZ\u00c9SEK, FELSZEREL\u00c9SEK, J\u00c1RM\u0170VEK": { + "account_number": 14, + "root_type": "Asset", + "is_group": 1, + "Egy\u00e9b (\u00fczemi \u00fczleti), berendez\u00e9sek, felszerel\u00e9sek": { + "account_number": 141, + "root_type": "Asset", + "is_group": 1, + "Egy\u00e9b (\u00fczemi \u00fczleti), berendez\u00e9sek, felszerel\u00e9sek brutt\u00f3 \u00e9rt\u00e9ke": { + "account_number": 1411, + "root_type": "Asset" + }, + "Egy\u00e9b (\u00fczemi \u00fczleti), berendez\u00e9sek, felszerel\u00e9sek terven fel\u00fcli \u00e9rt\u00e9kcs\u00f6kken\u00e9se \u00e9s annak vissza\u00edr\u00e1sa": { + "account_number": 1418, + "root_type": "Asset" + }, + "Egy\u00e9b (\u00fczemi \u00fczleti), berendez\u00e9sek, felszerel\u00e9sek terv szerinti \u00e9rt\u00e9kcs\u00f6kken\u00e9se": { + "account_number": 1419, + "root_type": "Asset" + } + }, + "Egy\u00e9b j\u00e1rm\u0171vek": { + "account_number": 142, + "root_type": "Asset", + "is_group": 1, + "Egy\u00e9b j\u00e1rm\u0171vek brutt\u00f3 \u00e9rt\u00e9ke": { + "account_number": 1421, + "root_type": "Asset" + }, + "Egy\u00e9b j\u00e1rm\u0171vek terven fel\u00fcli \u00e9rt\u00e9kcs\u00f6kken\u00e9se \u00e9s annak vissza\u00edr\u00e1sa": { + "account_number": 1428, + "root_type": "Asset" + }, + "Egy\u00e9b j\u00e1rm\u0171vek terv szerinti \u00e9rt\u00e9kcs\u00f6kken\u00e9se": { + "account_number": 1429, + "root_type": "Asset" + } + }, + "Irodai, igazgat\u00e1si berendez\u00e9sek \u00e9s felszerel\u00e9sek": { + "account_number": 143, + "root_type": "Asset", + "is_group": 1, + "Irodai, igazgat\u00e1si berendez\u00e9sek \u00e9s felszerel\u00e9sek brutt\u00f3 \u00e9rt\u00e9ke": { + "account_number": 1431, + "root_type": "Asset" + }, + "Irodai, igazgat\u00e1si berendez\u00e9sek \u00e9s felszerel\u00e9sek terven fel\u00fcli \u00e9rt\u00e9kcs\u00f6kken\u00e9se \u00e9s annak vissza\u00edr\u00e1sa": { + "account_number": 1438, + "root_type": "Asset" + }, + "Irodai, igazgat\u00e1si berendez\u00e9sek \u00e9s felszerel\u00e9sek terv szerinti \u00e9rt\u00e9kcs\u00f6kken\u00e9se": { + "account_number": 1439, + "root_type": "Asset" + } + }, + "Ki nem emelt egy\u00e9b berendez\u00e9sek, felszerel\u00e9sek": { + "account_number": 144, + "root_type": "Asset", + "is_group": 1, + "Ki nem emelt egy\u00e9b berendez\u00e9sek, felszerel\u00e9sek brutt\u00f3 \u00e9rt\u00e9ke": { + "account_number": 1441, + "root_type": "Asset" + }, + "Ki nem emelt egy\u00e9b berendez\u00e9sek, felszerel\u00e9sek terven fel\u00fcli \u00e9rt\u00e9kcs\u00f6kken\u00e9se \u00e9s annak vissza\u00edr\u00e1sa": { + "account_number": 1448, + "root_type": "Asset" + }, + "Ki nem emelt egy\u00e9b berendez\u00e9sek, felszerel\u00e9sek terv szerinti \u00e9rt\u00e9kcs\u00f6kken\u00e9se": { + "account_number": 1449, + "root_type": "Asset" + } + }, + "Kis \u00e9rt\u00e9k\u0171 egy\u00e9b berendez\u00e9sek, felszerel\u00e9sek, j\u00e1rm\u0171vek": { + "account_number": 149, + "root_type": "Asset", + "is_group": 1, + "Kis \u00e9rt\u00e9k\u0171 egy\u00e9b berendez\u00e9sek, felszerel\u00e9sek, j\u00e1rm\u0171vek brutt\u00f3 \u00e9rt\u00e9ke": { + "account_number": 1491, + "root_type": "Asset" + }, + "Kis \u00e9rt\u00e9k\u0171 egy\u00e9b berendez\u00e9sek, felszerel\u00e9sek, j\u00e1rm\u0171vek terv szerinti \u00e9rt\u00e9kcs\u00f6kken\u00e9se": { + "account_number": 1499, + "root_type": "Asset" + } + } + }, + "TENY\u00c9SZ\u00c1LLATOK": { + "account_number": 15, + "root_type": "Asset", + "is_group": 1, + "Teny\u00e9szt\u00e9sben hasznos\u00edtott \u00e1llatok": { + "account_number": 151, + "root_type": "Asset", + "is_group": 1, + "Teny\u00e9szt\u00e9sben hasznos\u00edtott \u00e1llatok brutt\u00f3 \u00e9rt\u00e9ke": { + "account_number": 1511, + "root_type": "Asset" + }, + "Teny\u00e9szt\u00e9sben hasznos\u00edtott \u00e1llatok terven fel\u00fcli \u00e9rt\u00e9kcs\u00f6kken\u00e9se \u00e9s annak vissza\u00edr\u00e1sa": { + "account_number": 1518, + "root_type": "Asset" + }, + "Teny\u00e9szt\u00e9sben hasznos\u00edtott \u00e1llatok terv szerinti \u00e9rt\u00e9kcs\u00f6kken\u00e9se": { + "account_number": 1519, + "root_type": "Asset" + } + }, + "Ig\u00e1s\u00e1llatok": { + "account_number": 152, + "root_type": "Asset", + "is_group": 1, + "Ig\u00e1s\u00e1llatok brutt\u00f3 \u00e9rt\u00e9ke": { + "account_number": 1521, + "root_type": "Asset" + }, + "Ig\u00e1s\u00e1llatok terven fel\u00fcli \u00e9rt\u00e9kcs\u00f6kken\u00e9se \u00e9s annak vissza\u00edr\u00e1sa": { + "account_number": 1528, + "root_type": "Asset" + }, + "Ig\u00e1s\u00e1llatok terv szerinti \u00e9rt\u00e9kcs\u00f6kken\u00e9se": { + "account_number": 1529, + "root_type": "Asset" + } + }, + "Egy\u00e9b teny\u00e9sz\u00e1llatok": { + "account_number": 153, + "root_type": "Asset", + "is_group": 1, + "Egy\u00e9b teny\u00e9sz\u00e1llatok brutt\u00f3 \u00e9rt\u00e9ke": { + "account_number": 1531, + "root_type": "Asset" + }, + "Egy\u00e9b teny\u00e9sz\u00e1llatok terven fel\u00fcli \u00e9rt\u00e9kcs\u00f6kken\u00e9se \u00e9s annak vissza\u00edr\u00e1sa": { + "account_number": 1538, + "root_type": "Asset" + }, + "Egy\u00e9b teny\u00e9sz\u00e1llatok terv szerinti \u00e9rt\u00e9kcs\u00f6kken\u00e9se": { + "account_number": 1539, + "root_type": "Asset" + } + }, + "Kis \u00e9rt\u00e9k\u0171 teny\u00e9sz\u00e1llatok": { + "account_number": 159, + "root_type": "Asset", + "is_group": 1, + "Kis \u00e9rt\u00e9k\u0171 teny\u00e9sz\u00e1llatok brutt\u00f3 \u00e9rt\u00e9ke": { + "account_number": 1591, + "root_type": "Asset" + }, + "Kis \u00e9rt\u00e9k\u0171 teny\u00e9sz\u00e1llatok terv szerinti \u00e9rt\u00e9kcs\u00f6kken\u00e9se": { + "account_number": 1599, + "root_type": "Asset" + } + } + }, + "BERUH\u00c1Z\u00c1SOK, FEL\u00daJ\u00cdT\u00c1SOK": { + "account_number": 16, + "root_type": "Asset", + "is_group": 1, + "Beruh\u00e1z\u00e1sok": { + "account_number": 161, + "root_type": "Asset" + }, + "Fel\u00faj\u00edt\u00e1sok": { + "account_number": 162, + "root_type": "Asset" + }, + "Beruh\u00e1z\u00e1sok, fel\u00faj\u00edt\u00e1sok terven fel\u00fcli \u00e9rt\u00e9kcs\u00f6kken\u00e9se \u00e9s annak vissza\u00edr\u00e1sa": { + "account_number": 168, + "root_type": "Asset" + } + }, + "TULAJDONI R\u00c9SZESED\u00c9ST JELENT\u0150 BEFEKTET\u00c9SEK (R\u00c9SZESED\u00c9SEK)": { + "account_number": 17, + "root_type": "Asset", + "is_group": 1, + "Tart\u00f3s r\u00e9szesed\u00e9sek": { + "account_number": 171, + "root_type": "Asset" + }, + "R\u00e9szesed\u00e9sek \u00e9rt\u00e9kveszt\u00e9se \u00e9s annak vissza\u00edr\u00e1sa": { + "account_number": 179, + "root_type": "Asset" + } + }, + "HITELVISZONYT MEGTESTES\u00cdT\u0150 \u00c9RT\u00c9KPAP\u00cdROK": { + "account_number": 18, + "root_type": "Asset", + "is_group": 1, + "\u00c1llamk\u00f6tv\u00e9nyek": { + "account_number": 181, + "root_type": "Asset" + }, + "Tart\u00f3s diszkont \u00e9rt\u00e9kpap\u00edrok": { + "account_number": 182, + "root_type": "Asset" + }, + "V\u00e1llalkoz\u00e1sok \u00e1ltal kibocs\u00e1tott tart\u00f3s \u00e9rt\u00e9kpap\u00edrok": { + "account_number": 183, + "root_type": "Asset" + }, + "Egy\u00e9b hitelviszonyt megtestes\u00edt\u0151 \u00e9rt\u00e9kpap\u00edrok": { + "account_number": 184, + "root_type": "Asset" + }, + "Tart\u00f3s \u00e9rt\u00e9kpap\u00edrok \u00e9rt\u00e9kveszt\u00e9se \u00e9s annak vissza\u00edr\u00e1sa": { + "account_number": 189, + "root_type": "Asset" + } + }, + "TART\u00d3SAN ADOTT K\u00d6LCS\u00d6N\u00d6K (tart\u00f3s bankbet\u00e9tek)": { + "account_number": 19, + "root_type": "Asset", + "is_group": 1, + "Tart\u00f3san adott k\u00f6lcs\u00f6n\u00f6k": { + "account_number": 191, + "root_type": "Asset" + }, + "Tart\u00f3s bankbet\u00e9tek": { + "account_number": 192, + "root_type": "Asset" + }, + "Tart\u00f3san adott k\u00f6lcs\u00f6n\u00f6k (\u00e9s bankbet\u00e9tek) \u00e9rt\u00e9kveszt\u00e9se \u00e9s annak vissza\u00edr\u00e1sa": { + "account_number": 199, + "root_type": "Asset" + } + } + }, + "SZ\u00c1MLAOSZT\u00c1LY K\u00c9SZLETEK": { + "account_number": 2, + "root_type": "Asset", + "is_group": 1, + "ANYAGOK": { + "account_number": "21-22", + "root_type": "Asset", + "is_group": 1, + "Nyers \u00e9s alapanyagok": { + "account_number": 211, + "root_type": "Asset" + }, + "Seg\u00e9danyagok": { + "account_number": 221, + "root_type": "Asset" + }, + "\u00dczem \u00e9s f\u0171t\u0151anyagok": { + "account_number": 222, + "root_type": "Asset" + }, + "Fenntart\u00e1si anyagok": { + "account_number": 223, + "root_type": "Asset" + }, + "\u00c9p\u00edt\u00e9si anyagok": { + "account_number": 224, + "root_type": "Asset" + }, + "Egy \u00e9ven bel\u00fcl elhaszn\u00e1l\u00f3d\u00f3 anyagi eszk\u00f6z\u00f6k": { + "account_number": 225, + "root_type": "Asset" + }, + "T\u00e1rgyi eszk\u00f6z\u00f6k k\u00f6z\u00fcl \u00e1tsorolt anyagok": { + "account_number": 226, + "root_type": "Asset" + }, + "Egy\u00e9b anyagok": { + "account_number": 227, + "root_type": "Asset" + }, + "Anyagok \u00e9rt\u00e9kveszt\u00e9se \u00e9s annak vissza\u00edr\u00e1sa": { + "account_number": 229, + "root_type": "Asset" + } + }, + "BEFEJEZETLEN TERMEL\u00c9S \u00c9S F\u00c9LK\u00c9SZ TERM\u00c9KEK": { + "account_number": 23, + "root_type": "Asset", + "is_group": 1, + "Befejezetlen termel\u00e9s": { + "account_number": 231, + "root_type": "Asset" + }, + "F\u00e9lk\u00e9sz term\u00e9kek": { + "account_number": 232, + "root_type": "Asset" + }, + "Befejezetlen termel\u00e9s \u00e9s f\u00e9lk\u00e9sz term\u00e9kek \u00e9rt\u00e9kveszt\u00e9se \u00e9s annak vissza\u00edr\u00e1sa": { + "account_number": 239, + "root_type": "Asset" + } + }, + "N\u00d6VEND\u00c9K, H\u00cdZ\u00d3 \u00c9S EGY\u00c9B \u00c1LLATOK": { + "account_number": 24, + "root_type": "Asset", + "is_group": 1, + "N\u00f6vend\u00e9k\u00e1llatok": { + "account_number": 241, + "root_type": "Asset" + }, + "H\u00edz\u00f3\u00e1llatok": { + "account_number": 242, + "root_type": "Asset" + }, + "Egy\u00e9b \u00e1llatok": { + "account_number": 243, + "root_type": "Asset" + }, + "B\u00e9rbevett \u00e1llatok": { + "account_number": 244, + "root_type": "Asset" + }, + "\u00c1llatok \u00e9rt\u00e9kveszt\u00e9se \u00e9s annak vissza\u00edr\u00e1sa": { + "account_number": 249, + "root_type": "Asset" + } + }, + "K\u00c9SZTERM\u00c9KEK": { + "account_number": 25, + "root_type": "Asset", + "is_group": 1, + "K\u00e9szterm\u00e9kek": { + "account_number": 251, + "root_type": "Asset" + }, + "K\u00e9szterm\u00e9kek \u00e9rt\u00e9kveszt\u00e9se \u00e9s annak vissza\u00edr\u00e1sa": { + "account_number": 259, + "root_type": "Asset" + } + }, + "KERESKEDELMI \u00c1RUK": { + "account_number": 26, + "root_type": "Asset", + "is_group": 1, + "Kereskedelmi \u00e1ruk": { + "account_number": 261, + "root_type": "Asset" + }, + "Idegen helyen t\u00e1rolt, bizom\u00e1nyba \u00e1tadott \u00e1ruk": { + "account_number": 262, + "root_type": "Asset" + }, + "T\u00e1rgyi eszk\u00f6z\u00f6k k\u00f6z\u00fcl \u00e1tsorolt \u00e1ruk": { + "account_number": 263, + "root_type": "Asset" + }, + "Bels\u0151 (egys\u00e9gek, tev\u00e9kenys\u00e9gek k\u00f6z\u00f6tti) \u00e1tad\u00e1s\u00e1tv\u00e9tel \u00fctk\u00f6z\u0151sz\u00e1mla": { + "account_number": 264, + "root_type": "Asset" + }, + "Kereskedelmi \u00e1ruk \u00e9rt\u00e9kveszt\u00e9se \u00e9s annak vissza\u00edr\u00e1sa": { + "account_number": 269, + "root_type": "Asset" + } + }, + "K\u00d6ZVET\u00cdTETT SZOLG\u00c1LTAT\u00c1SOK": { + "account_number": 27, + "root_type": "Asset", + "is_group": 1, + "K\u00f6zvet\u00edtett szolg\u00e1ltat\u00e1sok": { + "account_number": 271, + "root_type": "Asset" + }, + "K\u00f6zvet\u00edtett szolg\u00e1ltat\u00e1sok \u00e9rt\u00e9kveszt\u00e9se \u00e9s annak vissza\u00edr\u00e1sa": { + "account_number": 279, + "root_type": "Asset" + } + }, + "BET\u00c9TD\u00cdJAS G\u00d6NGY\u00d6LEGEK": { + "account_number": 28, + "root_type": "Asset", + "is_group": 1, + "Bet\u00e9td\u00edjas g\u00f6ngy\u00f6legek": { + "account_number": 281, + "root_type": "Asset" + }, + "Bet\u00e9td\u00edjas g\u00f6ngy\u00f6legek \u00e9rt\u00e9kveszt\u00e9se \u00e9s annak vissza\u00edr\u00e1sa": { + "account_number": 289, + "root_type": "Asset" + } + } + }, + "SZ\u00c1MLAOSZT\u00c1LY K\u00d6VETEL\u00c9SEK, P\u00c9NZ\u00dcGYI ESZK\u00d6Z\u00d6K \u00c9S AKT\u00cdV ID\u0150BELI ELHAT\u00c1ROL\u00c1SOK": { + "account_number": 3, + "root_type": "Asset", + "is_group": 1, + "K\u00d6VETEL\u00c9SEK \u00c1RUSZ\u00c1LL\u00cdT\u00c1SB\u00d3L \u00c9S SZOLG\u00c1LTAT\u00c1SB\u00d3L (VEV\u0150K)": { + "account_number": 31, + "root_type": "Asset", + "is_group": 1, + "Vev\u0151k\u00f6vetel\u00e9sek": { + "account_number": 311, + "root_type": "Asset" + }, + "Vev\u0151k\u00f6vetel\u00e9sek \u00e9rt\u00e9kveszt\u00e9se": { + "account_number": 319, + "root_type": "Asset" + } + }, + "K\u00d6VETEL\u00c9SEK TULAJDONOSSAL SZEMBEN": { + "account_number": 33, + "root_type": "Asset", + "is_group": 1, + "Jegyzett, de m\u00e9g be nem fizetett t\u0151ke": { + "account_number": 331, + "root_type": "Asset" + } + }, + "V\u00c1LT\u00d3K\u00d6VETEL\u00c9SEK": { + "account_number": 34, + "root_type": "Asset", + "is_group": 1, + "V\u00e1lt\u00f3k\u00f6vetel\u00e9sek": { + "account_number": 341, + "root_type": "Asset" + }, + "V\u00e1lt\u00f3k\u00f6vetel\u00e9sek \u00e9rt\u00e9kveszt\u00e9se": { + "account_number": 349, + "root_type": "Asset" + } + }, + "ADOTT EL\u0150LEGEK": { + "account_number": 35, + "root_type": "Asset", + "is_group": 1, + "Immateri\u00e1lis javakra adott el\u0151legek": { + "account_number": 351, + "root_type": "Asset" + }, + "Beruh\u00e1z\u00e1sokra adott el\u0151legek": { + "account_number": 352, + "root_type": "Asset" + }, + "K\u00e9szletekre adott el\u0151legek": { + "account_number": 353, + "root_type": "Asset" + }, + "Egy\u00e9b c\u00e9lra adott el\u0151legek": { + "account_number": 354, + "root_type": "Asset" + }, + "Adott el\u0151legek \u00e9rt\u00e9kveszt\u00e9se": { + "account_number": 359, + "root_type": "Asset" + } + }, + "EGY\u00c9B K\u00d6VETEL\u00c9SEK": { + "account_number": 36, + "root_type": "Asset", + "is_group": 1, + "Munkav\u00e1llal\u00f3kkal szembeni k\u00f6vetel\u00e9sek": { + "account_number": 361, + "root_type": "Asset" + }, + "K\u00f6lts\u00e9gvet\u00e9ssel szembeni k\u00f6vetel\u00e9sek": { + "account_number": 362, + "root_type": "Asset" + }, + "R\u00f6vid lej\u00e1ratra k\u00f6lcs\u00f6nadott p\u00e9nzeszk\u00f6z\u00f6k": { + "account_number": 363, + "root_type": "Asset" + }, + "R\u00e9szesed\u00e9sekkel, \u00e9rt\u00e9kpap\u00edrokkal kapcsolatos k\u00f6vetel\u00e9sek": { + "account_number": 364, + "root_type": "Asset" + }, + "K\u00fcl\u00f6nf\u00e9le egy\u00e9b k\u00f6vetel\u00e9sek": { + "account_number": 365, + "root_type": "Asset" + }, + "Egy\u00e9b k\u00f6vetel\u00e9sek \u00e9rt\u00e9kveszt\u00e9se": { + "account_number": 369, + "root_type": "Asset" + } + }, + "\u00c9RT\u00c9KPAP\u00cdROK": { + "account_number": 37, + "root_type": "Asset", + "is_group": 1, + "R\u00e9szesed\u00e9sek": { + "account_number": 371, + "root_type": "Asset" + }, + "Saj\u00e1t r\u00e9szv\u00e9nyek, saj\u00e1t \u00fczletr\u00e9szek": { + "account_number": 372, + "root_type": "Asset" + }, + "Forgat\u00e1si c\u00e9l\u00fa hitelviszonyt megtestes\u00edt\u0151 \u00e9rt\u00e9kpap\u00edrok": { + "account_number": 373, + "root_type": "Asset" + }, + "\u00c9rt\u00e9kpap\u00edr elsz\u00e1mol\u00e1si sz\u00e1mla": { + "account_number": 378, + "root_type": "Asset" + }, + "\u00c9rt\u00e9kpap\u00edrok \u00e9rt\u00e9kveszt\u00e9se": { + "account_number": 379, + "root_type": "Asset" + } + }, + "P\u00c9NZESZK\u00d6Z\u00d6K": { + "account_number": 38, + "root_type": "Asset", + "is_group": 1, + "P\u00e9nzt\u00e1r": { + "account_number": 381, + "root_type": "Asset" + }, + "Valutap\u00e9nzt\u00e1r": { + "account_number": 382, + "root_type": "Asset" + }, + "Csekkek": { + "account_number": 383, + "root_type": "Asset" + }, + "Elsz\u00e1mol\u00e1si bet\u00e9tsz\u00e1mla": { + "account_number": 384, + "root_type": "Asset" + }, + "Elk\u00fcl\u00f6n\u00edtett bet\u00e9tsz\u00e1mla": { + "account_number": 385, + "root_type": "Asset" + }, + "Devizabet\u00e9tsz\u00e1mla": { + "account_number": 386, + "root_type": "Asset" + }, + "Elektronikus p\u00e9nz": { + "account_number": 387, + "root_type": "Asset" + }, + "\u00c1tvezet\u00e9si sz\u00e1mla": { + "account_number": 389, + "root_type": "Asset" + } + }, + "AKT\u00cdV ID\u0150BELI ELHAT\u00c1ROL\u00c1SOK": { + "account_number": 39, + "root_type": "Asset", + "is_group": 1, + "Akt\u00edv id\u0151beli elhat\u00e1rol\u00e1sok": { + "account_number": 391, + "root_type": "Asset" + } + } + }, + "SZ\u00c1MLAOSZT\u00c1LY FORR\u00c1SOK": { + "account_number": 4, + "root_type": "Liability", + "is_group": 1, + "SAJ\u00c1T T\u0150KE": { + "account_number": 41, + "root_type": "Liability", + "is_group": 1, + "account_type": "Equity", + "Jegyzett t\u0151ke": { + "account_number": 411, + "root_type": "Liability", + "account_type": "Equity" + }, + "T\u0151ketartal\u00e9k": { + "account_number": 412, + "root_type": "Liability", + "account_type": "Equity" + }, + "Eredm\u00e9nytartal\u00e9k": { + "account_number": 413, + "root_type": "Liability", + "account_type": "Equity" + }, + "Lek\u00f6t\u00f6tt tartal\u00e9k": { + "account_number": 414, + "root_type": "Liability", + "account_type": "Equity" + }, + "Ad\u00f3zott eredm\u00e9ny": { + "account_number": 419, + "root_type": "Liability", + "account_type": "Equity" + } + }, + "C\u00c9LTARTAL\u00c9KOK": { + "account_number": 42, + "root_type": "Liability", + "is_group": 1, + "C\u00e9ltartal\u00e9k a v\u00e1rhat\u00f3 k\u00f6telezetts\u00e9gekre": { + "account_number": 421, + "root_type": "Liability" + } + }, + "H\u00c1TRASOROLT K\u00d6TELEZETTS\u00c9GEK": { + "account_number": 43, + "root_type": "Liability", + "is_group": 1, + "H\u00e1trasorolt k\u00f6telezetts\u00e9gek": { + "account_number": 431, + "root_type": "Liability" + } + }, + "HOSSZ\u00da LEJ\u00c1RAT\u00da K\u00d6TELEZETTS\u00c9GEK": { + "account_number": 44, + "root_type": "Liability", + "is_group": 1, + "Hossz\u00fa lej\u00e1ratra kapott k\u00f6lcs\u00f6n\u00f6k": { + "account_number": 441, + "root_type": "Liability" + }, + "\u00c1tv\u00e1ltoztathat\u00f3 k\u00f6tv\u00e9nyek": { + "account_number": 442, + "root_type": "Liability" + }, + "Tartoz\u00e1sok k\u00f6tv\u00e9nykibocs\u00e1t\u00e1sb\u00f3l": { + "account_number": 443, + "root_type": "Liability" + }, + "Beruh\u00e1z\u00e1si \u00e9s fejleszt\u00e9si hitelek": { + "account_number": 444, + "root_type": "Liability" + }, + "Egy\u00e9b hossz\u00fa lej\u00e1rat\u00fa hitelek": { + "account_number": 445, + "root_type": "Liability" + }, + "P\u00e9nz\u00fcgyi l\u00edzing miatti k\u00f6telezetts\u00e9gek": { + "account_number": 446, + "root_type": "Liability" + }, + "Egy\u00e9b hossz\u00fa lej\u00e1rat\u00fa k\u00f6telezetts\u00e9gek": { + "account_number": 449, + "root_type": "Liability" + } + }, + "R\u00d6VID LEJ\u00c1RAT\u00da K\u00d6TELEZETTS\u00c9GEK": { + "account_number": "45-47", + "root_type": "Liability", + "is_group": 1, + "R\u00f6vid lej\u00e1rat\u00fa k\u00f6lcs\u00f6n\u00f6k": { + "account_number": 451, + "root_type": "Liability" + }, + "R\u00f6vid lej\u00e1rat\u00fa hitelek": { + "account_number": 452, + "root_type": "Liability" + }, + "Vev\u0151kt\u0151l kapott el\u0151legek": { + "account_number": 453, + "root_type": "Liability" + }, + "K\u00f6telezetts\u00e9gek \u00e1rusz\u00e1ll\u00edt\u00e1sb\u00f3l \u00e9s szolg\u00e1ltat\u00e1sb\u00f3l (sz\u00e1ll\u00edt\u00f3k)": { + "account_number": 454, + "root_type": "Liability" + }, + "Beruh\u00e1z\u00e1si sz\u00e1ll\u00edt\u00f3k": { + "account_number": 455, + "root_type": "Liability" + }, + "Nem sz\u00e1ml\u00e1zott sz\u00e1ll\u00edt\u00f3k": { + "account_number": 456, + "root_type": "Liability" + }, + "V\u00e1lt\u00f3tartoz\u00e1sok": { + "account_number": 457, + "root_type": "Liability" + }, + "Eredm\u00e9nyt terhel\u0151 ad\u00f3k elsz\u00e1mol\u00e1sa": { + "account_number": 461, + "root_type": "Liability" + }, + "Szem\u00e9lyi j\u00f6vedelemad\u00f3 (SZJA) elsz\u00e1mol\u00e1sa": { + "account_number": 462, + "root_type": "Liability" + }, + "J\u00f6ved\u00e9ki ad\u00f3 elsz\u00e1mol\u00e1sa": { + "account_number": 463, + "root_type": "Liability" + }, + "G\u00e9pj\u00e1rm\u0171 ad\u00f3 (c\u00e9gaut\u00f3ad\u00f3) elsz\u00e1mol\u00e1sa": { + "account_number": 464, + "root_type": "Liability" + }, + "V\u00e1melsz\u00e1mol\u00e1si sz\u00e1mla": { + "account_number": 465, + "root_type": "Liability" + }, + "El\u0151zetesen felsz\u00e1m\u00edtott \u00e1ltal\u00e1nos forgalmi ad\u00f3": { + "account_number": 466, + "root_type": "Liability" + }, + "Fizetend\u0151 \u00e1ltal\u00e1nos forgalmi ad\u00f3": { + "account_number": 467, + "root_type": "Liability" + }, + "\u00c1ltal\u00e1nos forgalmi ad\u00f3 elsz\u00e1mol\u00e1si sz\u00e1mla": { + "account_number": 468, + "root_type": "Liability" + }, + "Helyi ad\u00f3k elsz\u00e1mol\u00e1si sz\u00e1mla": { + "account_number": 469, + "root_type": "Liability" + }, + "J\u00f6vedelemelsz\u00e1mol\u00e1si sz\u00e1mla": { + "account_number": 471, + "root_type": "Liability" + }, + "Fel nem vett j\u00e1rand\u00f3s\u00e1gok": { + "account_number": 472, + "root_type": "Liability" + }, + "Szoci\u00e1lis hozz\u00e1j\u00e1rul\u00e1si ad\u00f3": { + "account_number": 473, + "root_type": "Liability" + }, + "Szakk\u00e9pz\u00e9si hozz\u00e1j\u00e1rul\u00e1s": { + "account_number": 474, + "root_type": "Liability" + }, + "Egy\u00e9b \u00e1llami ad\u00f3hat\u00f3s\u00e1ggal szembeni k\u00f6telezetts\u00e9g elsz\u00e1mol\u00e1sa": { + "account_number": 476, + "root_type": "Liability" + }, + "R\u00f6vid lej\u00e1rat\u00fa egy\u00e9b k\u00f6telezetts\u00e9gek munkav\u00e1llal\u00f3kkal \u00e9s tulajdonosokkal szemben": { + "account_number": 477, + "root_type": "Liability" + }, + "R\u00e9szesed\u00e9sekkel, \u00e9rt\u00e9kpap\u00edrokkal kapcsolatos k\u00f6telezetts\u00e9gek": { + "account_number": 478, + "root_type": "Liability" + }, + "K\u00fcl\u00f6nf\u00e9le egy\u00e9b r\u00f6vid lej\u00e1rat\u00fa k\u00f6telezetts\u00e9gek": { + "account_number": 479, + "root_type": "Liability" + } + }, + "PASSZ\u00cdV ID\u0150BELI ELHAT\u00c1ROL\u00c1SOK": { + "account_number": 48, + "root_type": "Liability", + "is_group": 1, + "Passz\u00edv id\u0151beli elhat\u00e1rol\u00e1sok": { + "account_number": 481, + "root_type": "Liability" + } + }, + "\u00c9VI M\u00c9RLEGSZ\u00c1ML\u00c1K": { + "account_number": 49, + "root_type": "Liability", + "is_group": 1, + "Nyit\u00f3m\u00e9rleg sz\u00e1mla": { + "account_number": 491, + "root_type": "Liability" + }, + "Z\u00e1r\u00f3m\u00e9rleg sz\u00e1mla": { + "account_number": 492, + "root_type": "Liability" + }, + "Ad\u00f3zott eredm\u00e9ny elsz\u00e1mol\u00e1si sz\u00e1mla": { + "account_number": 493, + "root_type": "Liability" + } + } + }, + "SZ\u00c1MLAOSZT\u00c1LY K\u00d6LTS\u00c9GNEMEK": { + "account_number": 5, + "root_type": "Expense", + "is_group": 1, + "ANYAGK\u00d6LTS\u00c9G": { + "account_number": 51, + "root_type": "Expense", + "is_group": 1, + "Alapanyagok k\u00f6lts\u00e9gei": { + "account_number": 511, + "root_type": "Expense" + }, + "Egy \u00e9ven bel\u00fcl elhaszn\u00e1l\u00f3d\u00f3 anyagi eszk\u00f6z\u00f6k k\u00f6lts\u00e9gei": { + "account_number": 512, + "root_type": "Expense" + }, + "Egy\u00e9b anyagk\u00f6lts\u00e9g": { + "account_number": 513, + "root_type": "Expense" + }, + "Anyagk\u00f6lts\u00e9g megt\u00e9r\u00fcl\u00e9s": { + "account_number": 519, + "root_type": "Expense" + } + }, + "IG\u00c9NYBE VETT SZOLG\u00c1LTAT\u00c1SOK K\u00d6LTS\u00c9GEI": { + "account_number": 52, + "root_type": "Expense", + "is_group": 1, + "Sz\u00e1ll\u00edt\u00e1srakod\u00e1s, rakt\u00e1roz\u00e1s k\u00f6lts\u00e9gei": { + "account_number": 521, + "root_type": "Expense" + }, + "B\u00e9rleti d\u00edjak": { + "account_number": 522, + "root_type": "Expense" + }, + "Karbantart\u00e1si k\u00f6lts\u00e9gek": { + "account_number": 523, + "root_type": "Expense" + }, + "Hirdet\u00e9s, rekl\u00e1m, propaganda k\u00f6lts\u00e9gek": { + "account_number": 524, + "root_type": "Expense" + }, + "Oktat\u00e1s \u00e9s tov\u00e1bbk\u00e9pz\u00e9s k\u00f6lts\u00e9gei": { + "account_number": 525, + "root_type": "Expense" + }, + "Utaz\u00e1si \u00e9s kik\u00fcldet\u00e9si k\u00f6lts\u00e9gek (napid\u00edj n\u00e9lk\u00fcl)": { + "account_number": 526, + "root_type": "Expense" + }, + "Ig\u00e9nybe vett egy\u00e9b szolg\u00e1ltat\u00e1sok k\u00f6lts\u00e9gei": { + "account_number": 529, + "root_type": "Expense" + } + }, + "EGY\u00c9B SZOLG\u00c1LTAT\u00c1SOK K\u00d6LTS\u00c9GEI": { + "account_number": 53, + "root_type": "Expense", + "is_group": 1, + "Hat\u00f3s\u00e1gi igazgat\u00e1si, szolg\u00e1ltat\u00e1si d\u00edjak, illet\u00e9kek": { + "account_number": 531, + "root_type": "Expense" + }, + "P\u00e9nz\u00fcgyi, befektet\u00e9si szolg\u00e1ltat\u00e1si d\u00edjak": { + "account_number": 532, + "root_type": "Expense" + }, + "Biztos\u00edt\u00e1si d\u00edjak": { + "account_number": 533, + "root_type": "Expense" + }, + "K\u00f6lts\u00e9gk\u00e9nt elsz\u00e1moland\u00f3 ad\u00f3k, j\u00e1rul\u00e9kok, term\u00e9kd\u00edjak": { + "account_number": 534, + "root_type": "Expense" + }, + "K\u00fcl\u00f6nf\u00e9le egy\u00e9b szolg\u00e1ltat\u00e1sok k\u00f6lts\u00e9gei": { + "account_number": 539, + "root_type": "Expense" + } + }, + "B\u00c9RK\u00d6LTS\u00c9G": { + "account_number": 54, + "root_type": "Expense", + "is_group": 1, + "B\u00e9rk\u00f6lts\u00e9g": { + "account_number": 541, + "root_type": "Expense" + } + }, + "SZEM\u00c9LYI JELLEG\u0170 EGY\u00c9B KIFIZET\u00c9SEK": { + "account_number": 55, + "root_type": "Expense", + "is_group": 1, + "Munkav\u00e1llal\u00f3knak, tagoknak fizetett szem\u00e9lyi jelleg\u0171 kifizet\u00e9sek": { + "account_number": 551, + "root_type": "Expense" + }, + "J\u00f3l\u00e9ti \u00e9s kultur\u00e1lis k\u00f6lts\u00e9gek": { + "account_number": 552, + "root_type": "Expense" + }, + "Reprezent\u00e1ci\u00f3s k\u00f6lts\u00e9gek": { + "account_number": 553, + "root_type": "Expense" + }, + "Egy\u00e9b szem\u00e9lyi jelleg\u0171 kifizet\u00e9sek": { + "account_number": 559, + "root_type": "Expense" + } + }, + "B\u00c9RJ\u00c1RUL\u00c9KOK": { + "account_number": 56, + "root_type": "Expense", + "is_group": 1, + "Szoci\u00e1lis hozz\u00e1j\u00e1rul\u00e1si ad\u00f3": { + "account_number": 561, + "root_type": "Expense" + }, + "Szakk\u00e9pz\u00e9si hozz\u00e1j\u00e1rul\u00e1s": { + "account_number": 563, + "root_type": "Expense" + }, + "Egy\u00e9b b\u00e9rj\u00e1rul\u00e9kok": { + "account_number": 569, + "root_type": "Expense" + } + }, + "\u00c9RT\u00c9KCS\u00d6KKEN\u00c9SI LE\u00cdR\u00c1S": { + "account_number": 57, + "root_type": "Expense", + "is_group": 1, + "Terv szerinti \u00e9rt\u00e9kcs\u00f6kken\u00e9si le\u00edr\u00e1s": { + "account_number": 571, + "root_type": "Expense" + }, + "Kis \u00e9rt\u00e9k\u0171 eszk\u00f6z\u00f6k egy \u00f6sszegben elsz\u00e1molt \u00e9rt\u00e9kcs\u00f6kken\u00e9si le\u00edr\u00e1sa": { + "account_number": 572, + "root_type": "Expense" + } + }, + "AKTIV\u00c1LT SAJ\u00c1T TELJES\u00cdTM\u00c9NYEK \u00c9RT\u00c9KE": { + "account_number": 58, + "root_type": "Expense", + "is_group": 1, + "Saj\u00e1t termel\u00e9s\u0171 k\u00e9szletek \u00e1llom\u00e1nyv\u00e1ltoz\u00e1sa": { + "account_number": 581, + "root_type": "Expense" + }, + "Saj\u00e1t el\u0151\u00e1ll\u00edt\u00e1s\u00fa eszk\u00f6z\u00f6k aktiv\u00e1lt \u00e9rt\u00e9ke": { + "account_number": 582, + "root_type": "Expense" + }, + "Aktiv\u00e1lt saj\u00e1t teljes\u00edtm\u00e9nyek \u00e1tvezet\u00e9si sz\u00e1mla": { + "account_number": 589, + "root_type": "Expense" + } + }, + "K\u00d6LTS\u00c9GNEMEK \u00c1TVEZET\u00c9SE": { + "account_number": 59, + "root_type": "Expense", + "is_group": 1, + "K\u00f6lts\u00e9gnemek \u00e1tvezet\u00e9si sz\u00e1mla": { + "account_number": 599, + "root_type": "Expense" + } + } + }, + "SZ\u00c1MLAOSZT\u00c1LY R\u00c1FORD\u00cdT\u00c1SOK": { + "account_number": 8, + "root_type": "Expense", + "is_group": 1, + "ANYAGJELLEG\u0170 R\u00c1FORD\u00cdT\u00c1SOK": { + "account_number": 81, + "root_type": "Expense", + "is_group": 1, + "Anyagk\u00f6lts\u00e9g": { + "account_number": 811, + "root_type": "Expense" + }, + "Ig\u00e9nybe vett szolg\u00e1ltat\u00e1sok \u00e9rt\u00e9ke": { + "account_number": 812, + "root_type": "Expense" + }, + "Egy\u00e9b szolg\u00e1ltat\u00e1sok \u00e9rt\u00e9ke": { + "account_number": 813, + "root_type": "Expense" + }, + "Eladott \u00e1ruk beszerz\u00e9si \u00e9rt\u00e9ke": { + "account_number": 814, + "root_type": "Expense" + }, + "Eladott (k\u00f6zvet\u00edtett) szolg\u00e1ltat\u00e1sok \u00e9rt\u00e9ke": { + "account_number": 815, + "root_type": "Expense" + } + }, + "SZEM\u00c9LYI JELLEG\u0170 R\u00c1FORD\u00cdT\u00c1SOK": { + "account_number": 82, + "root_type": "Expense", + "is_group": 1, + "B\u00e9rk\u00f6lts\u00e9g": { + "account_number": 821, + "root_type": "Expense" + }, + "Szem\u00e9lyi jelleg\u0171 egy\u00e9b kifizet\u00e9sek": { + "account_number": 822, + "root_type": "Expense" + }, + "B\u00e9rj\u00e1rul\u00e9kok": { + "account_number": 823, + "root_type": "Expense" + } + }, + "\u00c9RT\u00c9KCS\u00d6KKEN\u00c9SI LE\u00cdR\u00c1S": { + "account_number": 83, + "root_type": "Expense", + "is_group": 1, + "\u00c9rt\u00e9kcs\u00f6kken\u00e9si le\u00edr\u00e1s": { + "account_number": 831, + "root_type": "Expense" + } + }, + "EGY\u00c9B R\u00c1FORD\u00cdT\u00c1SOK": { + "account_number": 86, + "root_type": "Expense", + "is_group": 1, + "Egy\u00e9b r\u00e1ford\u00edt\u00e1snak min\u0151s\u00fcl\u0151 \u00e9rt\u00e9kes\u00edt\u00e9sek": { + "account_number": 861, + "root_type": "Expense", + "is_group": 1, + "\u00c9rt\u00e9kes\u00edtett immateri\u00e1lis javak, t\u00e1rgyi eszk\u00f6z\u00f6k k\u00f6nyv szerinti \u00e9rt\u00e9ke": { + "account_number": 8611, + "root_type": "Expense" + }, + "\u00c9rt\u00e9kes\u00edtett, \u00e1truh\u00e1zott (engedm\u00e9nyezett) k\u00f6vetel\u00e9sek k\u00f6nyv szerinti \u00e9rt\u00e9ke": { + "account_number": 8612, + "root_type": "Expense" + } + }, + "Egy\u00e9b r\u00e1ford\u00edt\u00e1snak min\u0151s\u00fcl\u0151 eszk\u00f6z kivezet\u00e9sek": { + "account_number": 862, + "root_type": "Expense", + "is_group": 1, + "Hi\u00e1nyz\u00f3, megsemmis\u00fclt, kiselejtezett immateri\u00e1lis javak, t\u00e1rgyi eszk\u00f6z\u00f6k nett\u00f3 \u00e9rt\u00e9ke": { + "account_number": 8621, + "root_type": "Expense" + }, + "Hi\u00e1nyz\u00f3, megsemmis\u00fclt, \u00e1llom\u00e1nyb\u00f3l kivezetett k\u00e9szletek k\u00f6nyv szerinti \u00e9rt\u00e9ke": { + "account_number": 8622, + "root_type": "Expense" + } + }, + "Behajthatatlan k\u00f6vetel\u00e9sek le\u00edrt \u00f6sszege": { + "account_number": 863, + "root_type": "Expense" + }, + "C\u00e9ltartal\u00e9k k\u00e9pz\u00e9s": { + "account_number": 864, + "root_type": "Expense" + }, + "Ut\u00f3lag adott, nem sz\u00e1ml\u00e1zott engedm\u00e9ny": { + "account_number": 865, + "root_type": "Expense" + }, + "Egy\u00e9b r\u00e1ford\u00edt\u00e1sk\u00e9nt elsz\u00e1molt ad\u00f3k, illet\u00e9kek, hozz\u00e1j\u00e1rul\u00e1sok": { + "account_number": 866, + "root_type": "Expense", + "is_group": 1, + "K\u00f6zponti k\u00f6lts\u00e9gvet\u00e9ssel elsz\u00e1molt ad\u00f3k, illet\u00e9kek, hozz\u00e1j\u00e1rul\u00e1sok": { + "account_number": 8661, + "root_type": "Expense" + }, + "Helyi \u00f6nkorm\u00e1nyzatokkal elsz\u00e1molt ad\u00f3k, illet\u00e9kek, hozz\u00e1j\u00e1rul\u00e1sok": { + "account_number": 8662, + "root_type": "Expense" + }, + "Elk\u00fcl\u00f6n\u00edtett \u00e1llami p\u00e9nzalapokkal elsz\u00e1molt ad\u00f3k, illet\u00e9kek, hozz\u00e1j\u00e1rul\u00e1sok": { + "account_number": 8663, + "root_type": "Expense" + }, + "T\u00e1rsadalombiztos\u00edt\u00e1ssal elsz\u00e1molt ad\u00f3k, illet\u00e9kek, hozz\u00e1j\u00e1rul\u00e1sok": { + "account_number": 8664, + "root_type": "Expense" + }, + "EU p\u00e9nz\u00fcgyi alapokkal elsz\u00e1molt ad\u00f3k, illet\u00e9kek, hozz\u00e1j\u00e1rul\u00e1sok": { + "account_number": 8665, + "root_type": "Expense" + }, + "R\u00e1ford\u00edt\u00e1sk\u00e9nt elsz\u00e1molt egy\u00e9b ad\u00f3k \u00e9s ad\u00f3jelleg\u0171 t\u00e9telek": { + "account_number": 8666, + "root_type": "Expense" + } + }, + "Egy\u00e9b r\u00e1ford\u00edt\u00e1sk\u00e9nt elsz\u00e1molt, ad\u00f3nak nem min\u0151s\u00fcl\u0151 kifizet\u00e9sek": { + "account_number": 867, + "root_type": "Expense", + "is_group": 1, + "K\u00e1resem\u00e9nnyel kapcsolatos fizetett, fizetend\u0151 \u00f6sszegek": { + "account_number": 8671, + "root_type": "Expense" + }, + "K\u00f6lts\u00e9gek (r\u00e1f.) ellent\u00e9telez\u00e9s\u00e9re visszafizet\u00e9si k\u00f6telezetts\u00e9g n\u00e9lk\u00fcl adott t\u00e1mogat\u00e1s, v\u00e9glegesen \u00e1tadott p\u00e9nzeszk\u00f6z, juttat\u00e1s": { + "account_number": 8672, + "root_type": "Expense" + }, + "Fejleszt\u00e9si c\u00e9lra, visszafizet\u00e9si k\u00f6telezetts\u00e9g n\u00e9lk\u00fcl adott t\u00e1mogat\u00e1s, juttat\u00e1s": { + "account_number": 8673, + "root_type": "Expense" + }, + "Fejleszt\u00e9si c\u00e9lra kapott t\u00e1mogat\u00e1s visszafizetend\u0151 \u00f6sszege": { + "account_number": 8674, + "root_type": "Expense" + }, + "Tao \u00e1ltal elismert b\u00edrs\u00e1gok, k\u00f6tb\u00e9rek, k\u00e9sedelmi kamatok, p\u00f3tl\u00e9kok, k\u00e1rt\u00e9r\u00edt\u00e9sek, s\u00e9relemd\u00edjak": { + "account_number": 8675, + "root_type": "Expense" + }, + "Tao \u00e1ltal el nem ismert b\u00edrs\u00e1gok, k\u00f6tb\u00e9rek, k\u00e9sedelmi kamatok, p\u00f3tl\u00e9kok, k\u00e1rt\u00e9r\u00edt\u00e9sek, s\u00e9relemd\u00edjak": { + "account_number": 8676, + "root_type": "Expense" + } + }, + "Terven fel\u00fcli \u00e9rt\u00e9kcs\u00f6kken\u00e9sek, \u00e9rt\u00e9kveszt\u00e9sek": { + "account_number": 868, + "root_type": "Expense", + "is_group": 1, + "Immateri\u00e1lis javak terven fel\u00fcli \u00e9rt\u00e9kcs\u00f6kken\u00e9se": { + "account_number": 8681, + "root_type": "Expense" + }, + "T\u00e1rgyi eszk\u00f6z\u00f6k terven fel\u00fcli \u00e9rt\u00e9kcs\u00f6kken\u00e9se": { + "account_number": 8682, + "root_type": "Expense" + }, + "K\u00e9szletek \u00e9rt\u00e9kveszt\u00e9se": { + "account_number": 8683, + "root_type": "Expense" + }, + "K\u00f6vetel\u00e9sek \u00e9rt\u00e9kveszt\u00e9se": { + "account_number": 8684, + "root_type": "Expense" + } + }, + "K\u00fcl\u00f6nf\u00e9le egy\u00e9b r\u00e1ford\u00edt\u00e1sok": { + "account_number": 869, + "root_type": "Expense", + "is_group": 1, + "T\u00e1rsas\u00e1gba bevitt, \u00e9rt\u00e9kp. v. r\u00e9sz. nem min\u0151s\u00fcl\u0151 vagyont. k\u00f6nyv sz. \u00e9s l\u00e9tes\u00edt\u0151 o. meghat. \u00e9rt\u00e9k\u00e9nek vesztes\u00e9gjelleg\u0171 k\u00fcl.": { + "account_number": 8691, + "root_type": "Expense" + }, + "Ellent\u00e9telez\u00e9s n\u00e9lk\u00fcl \u00e1tv\u00e1llalt k\u00f6telezetts\u00e9g szerz\u0151d\u00e9s szerinti \u00f6sszege": { + "account_number": 8692, + "root_type": "Expense" + }, + "T\u00e9r\u00edt\u00e9s n\u00e9lk\u00fcl \u00e1tadott, r\u00e9szesed\u00e9snek vagy \u00e9rt\u00e9kpap\u00edrnak nem min\u0151s\u00fcl\u0151 eszk\u00f6z\u00f6k nyilv\u00e1ntart\u00e1s szerinti \u00e9rt\u00e9ke": { + "account_number": 8693, + "root_type": "Expense" + }, + "T\u00e9r\u00edt\u00e9s n\u00e9lk\u00fcl ny\u00fajtott szolg\u00e1ltat\u00e1sok beker\u00fcl\u00e9si \u00e9rt\u00e9ke": { + "account_number": 8694, + "root_type": "Expense" + }, + "Elengedett k\u00f6vetel\u00e9sek k\u00f6nyv szerinti \u00e9rt\u00e9ke": { + "account_number": 8695, + "root_type": "Expense" + }, + "Egy\u00e9b, vagyoncs\u00f6kken\u00e9ssel j\u00e1r\u00f3 r\u00e1ford\u00edt\u00e1sok": { + "account_number": 8696, + "root_type": "Expense" + } + } + }, + "P\u00c9NZ\u00dcGYI M\u0170VELETEK R\u00c1FORD\u00cdT\u00c1SAI": { + "account_number": 87, + "root_type": "Expense", + "is_group": 1, + "R\u00e9szesed\u00e9sekb\u0151l sz\u00e1rmaz\u00f3 r\u00e1ford\u00edt\u00e1sok, \u00e1rfolyamvesztes\u00e9gek": { + "account_number": 871, + "root_type": "Expense" + }, + "Befektetett p\u00e9nz\u00fcgyi eszk\u00f6z\u00f6kb\u0151l (\u00e9rt\u00e9kpap\u00edrokb\u00f3l, k\u00f6lcs\u00f6n\u00f6kb\u0151l) sz\u00e1rmaz\u00f3 r\u00e1ford\u00edt\u00e1sok \u00e1rfolyamvesztes\u00e9gek": { + "account_number": 872, + "root_type": "Expense" + }, + "Hitelint\u00e9zetnek fizetend\u0151 kamatok \u00e9s kamatjelleg\u0171 r\u00e1ford\u00edt\u00e1sok": { + "account_number": 873, + "root_type": "Expense" + }, + "Nem hitelint\u00e9zetnek fizetend\u0151 kamatok \u00e9s kamatjelleg\u0171 r\u00e1ford\u00edt\u00e1sok": { + "account_number": 874, + "root_type": "Expense" + }, + "R\u00e9szesed\u00e9sek, \u00e9rt\u00e9kpap\u00edrok, bankbet\u00e9tek \u00e9rt\u00e9kveszt\u00e9se \u00e9s vissza\u00edr\u00e1sa": { + "account_number": 875, + "root_type": "Expense", + "is_group": 1, + "R\u00e9szesed\u00e9sek, \u00e9rt\u00e9kpap\u00edrok, bankbet\u00e9tek \u00e9rt\u00e9kveszt\u00e9se": { + "account_number": 8751, + "root_type": "Expense" + }, + "R\u00e9szesed\u00e9sek, \u00e9rt\u00e9kpap\u00edrok, bankbet\u00e9tek \u00e9rt\u00e9kveszt\u00e9s\u00e9nek vissza\u00edr\u00e1sa": { + "account_number": 8752, + "root_type": "Expense" + } + }, + "K\u00fclf\u00f6ldi p\u00e9nz\u00e9rt\u00e9kre sz\u00f3l\u00f3 eszk\u00f6z\u00f6k \u00e9s k\u00f6telezetts\u00e9gek \u00e1rfolyamvesztes\u00e9gei": { + "account_number": 876, + "root_type": "Expense", + "is_group": 1, + "Deviza \u00e9s valutak\u00e9szletek forintra \u00e1tv\u00e1lt\u00e1s\u00e1nak \u00e1rfolyamvesztes\u00e9ge": { + "account_number": 8761, + "root_type": "Expense" + }, + "K\u00fclf\u00f6ldi p\u00e9nz\u00e9rt\u00e9kre sz\u00f3l\u00f3 eszk\u00f6z\u00f6k \u00e9s k\u00f6telezetts\u00e9gek p\u00e9nz\u00fcgyileg rendezett \u00e1rfolyamvesztes\u00e9ge": { + "account_number": 8762, + "root_type": "Expense" + } + }, + "P\u00e9nz\u00fcgyi m\u0171veletek egy\u00e9b r\u00e1ford\u00edt\u00e1sai": { + "account_number": 877, + "root_type": "Expense" + }, + "P\u00e9nz\u00fcgyi rendez\u00e9shez kapcsol\u00f3d\u00f3an adott engedm\u00e9ny": { + "account_number": 878, + "root_type": "Expense" + }, + "Egy\u00e9b vagyoncs\u00f6kken\u00e9ssel j\u00e1r\u00f3 p\u00e9nz\u00fcgyi r\u00e1ford\u00edt\u00e1sok": { + "account_number": 879, + "root_type": "Expense" + } + }, + "EREDM\u00c9NYT TERHEL\u0150 AD\u00d3K": { + "account_number": 89, + "root_type": "Expense", + "is_group": 1, + "T\u00e1rsas\u00e1gi ad\u00f3": { + "account_number": 891, + "root_type": "Expense" + }, + "Kisv\u00e1llalati ad\u00f3": { + "account_number": 893, + "root_type": "Expense" + }, + "Eredm\u00e9nyt terhel\u0151 egy\u00e9b ad\u00f3k": { + "account_number": 899, + "root_type": "Expense" + } + } + }, + "SZ\u00c1MLAOSZT\u00c1LY BEV\u00c9TELEK": { + "account_number": 9, + "root_type": "Income", + "is_group": 1, + "BELF\u00d6LDI \u00c9RT\u00c9KES\u00cdT\u00c9S \u00c1RBEV\u00c9TELE": { + "account_number": 91, + "root_type": "Income", + "is_group": 1, + "Belf\u00f6ldinek \u00e9rt\u00e9kes\u00edtett saj\u00e1t termel\u00e9s\u0171 k\u00e9szletek \u00e1rbev\u00e9tele": { + "account_number": 911, + "root_type": "Income" + }, + "Belf\u00f6ldinek \u00e9rt\u00e9kes\u00edtett v\u00e1s\u00e1rolt k\u00e9szletek \u00e1rbev\u00e9tele": { + "account_number": 912, + "root_type": "Income" + }, + "Belf\u00f6ldinek ny\u00fajtott szolg\u00e1ltat\u00e1sok \u00e1rbev\u00e9tele": { + "account_number": 913, + "root_type": "Income" + }, + "Belf\u00f6ldi \u00e9rt\u00e9kes\u00edt\u00e9ssel kapcsolatos \u00e1rt\u00e1mogat\u00e1s": { + "account_number": 918, + "root_type": "Income" + }, + "Egy\u00e9b belf\u00f6ldi \u00e9rt\u00e9kes\u00edt\u00e9s \u00e1rbev\u00e9tele": { + "account_number": 919, + "root_type": "Income" + } + }, + "EXPORT\u00c9RT\u00c9KES\u00cdT\u00c9S \u00c1RBEV\u00c9TELE": { + "account_number": 92, + "root_type": "Income", + "is_group": 1, + "K\u00fclf\u00f6ldinek \u00e9rt\u00e9kes\u00edtett saj\u00e1t termel\u00e9s\u0171 k\u00e9szletek \u00e1rbev\u00e9tele": { + "account_number": 921, + "root_type": "Income" + }, + "K\u00fclf\u00f6ldinek \u00e9rt\u00e9kes\u00edtett v\u00e1s\u00e1rolt k\u00e9szletek \u00e1rbev\u00e9tele": { + "account_number": 922, + "root_type": "Income" + }, + "K\u00fclf\u00f6ldinek ny\u00fajtott szolg\u00e1ltat\u00e1sok \u00e1rbev\u00e9tele": { + "account_number": 923, + "root_type": "Income" + }, + "K\u00fclf\u00f6ldi \u00e9rt\u00e9kes\u00edt\u00e9ssel kapcsolatos \u00e1rt\u00e1mogat\u00e1s": { + "account_number": 928, + "root_type": "Income" + }, + "Egy\u00e9b k\u00fclf\u00f6ldi \u00e9rt\u00e9kes\u00edt\u00e9s \u00e1rbev\u00e9tele": { + "account_number": 929, + "root_type": "Income" + } + }, + "EGY\u00c9B BEV\u00c9TELEK": { + "account_number": 96, + "root_type": "Income", + "is_group": 1, + "Egy\u00e9b bev\u00e9telnek min\u0151s\u00fcl\u0151 \u00e9rt\u00e9kes\u00edt\u00e9sek": { + "account_number": 961, + "root_type": "Income", + "is_group": 1, + "\u00c9rt\u00e9kes\u00edtett immateri\u00e1lis javak, t\u00e1rgyi eszk\u00f6z\u00f6k ellen\u00e9rt\u00e9ke": { + "account_number": 9611, + "root_type": "Income" + }, + "\u00c9rt\u00e9kes\u00edtett, \u00e1truh\u00e1zott (engedm\u00e9nyezett) k\u00f6vetel\u00e9sek ellen\u00e9rt\u00e9ke": { + "account_number": 9612, + "root_type": "Income" + } + }, + "K\u00f6vetel\u00e9s k\u00f6nyv szerinti \u00e9rt\u00e9k\u00e9t meghalad\u00f3an realiz\u00e1lt \u00f6sszeg": { + "account_number": 963, + "root_type": "Income" + }, + "C\u00e9ltartal\u00e9k felold\u00e1s": { + "account_number": 964, + "root_type": "Income" + }, + "Ut\u00f3lag kapott, nem sz\u00e1ml\u00e1zott engedm\u00e9ny": { + "account_number": 965, + "root_type": "Income" + }, + "M\u0171k\u00f6d\u00e9si c\u00e9lra kapott t\u00e1mogat\u00e1s, juttat\u00e1s": { + "account_number": 966, + "root_type": "Income", + "is_group": 1, + "K\u00f6zponti k\u00f6lts\u00e9gvet\u00e9sb\u0151l kapott t\u00e1mogat\u00e1s, juttat\u00e1s": { + "account_number": 9661, + "root_type": "Income" + }, + "Helyi \u00f6nkorm\u00e1nyzatokt\u00f3l kapott t\u00e1mogat\u00e1s, juttat\u00e1s": { + "account_number": 9662, + "root_type": "Income" + }, + "Eur\u00f3pai Uni\u00f3t\u00f3l kapott t\u00e1mogat\u00e1s, juttat\u00e1s": { + "account_number": 9663, + "root_type": "Income" + }, + "Egy\u00e9b forr\u00e1sb\u00f3l kapott t\u00e1mogat\u00e1s, juttat\u00e1s": { + "account_number": 9664, + "root_type": "Income" + } + }, + "Egy\u00e9b bev\u00e9telk\u00e9nt elsz\u00e1molt p\u00e9nzbev\u00e9telek": { + "account_number": 967, + "root_type": "Income", + "is_group": 1, + "K\u00e1resem\u00e9nnyel kapcsolatos t\u00e9r\u00edt\u00e9sek": { + "account_number": 9671, + "root_type": "Income" + }, + "K\u00f6lts\u00e9gek (r\u00e1ford\u00edt\u00e1sok) ellent\u00e9telez\u00e9s\u00e9re kapott t\u00e1mogat\u00e1s, juttat\u00e1s": { + "account_number": 9672, + "root_type": "Income" + }, + "Fejleszt\u00e9si c\u00e9lra kapott t\u00e1mogat\u00e1s, juttat\u00e1s": { + "account_number": 9673, + "root_type": "Income" + }, + "Kapott b\u00edrs\u00e1gok, k\u00f6tb\u00e9rek, fekb\u00e9rek, k\u00e9sedelmi kamatok, k\u00e1rt\u00e9r\u00edt\u00e9sek": { + "account_number": 9674, + "root_type": "Income" + } + }, + "Terven fel\u00fcli \u00e9rt\u00e9kcs\u00f6kken\u00e9sek, \u00e9rt\u00e9kveszt\u00e9sek vissza\u00edr\u00e1sa": { + "account_number": 968, + "root_type": "Income", + "is_group": 1, + "Immateri\u00e1lis javak terven fel\u00fcli \u00e9rt\u00e9kcs\u00f6kken\u00e9s\u00e9nek vissza\u00edr\u00e1sa": { + "account_number": 9681, + "root_type": "Income" + }, + "T\u00e1rgyi eszk\u00f6z\u00f6k terven fel\u00fcli \u00e9rt\u00e9kcs\u00f6kken\u00e9s\u00e9nek vissza\u00edr\u00e1sa": { + "account_number": 9682, + "root_type": "Income" + }, + "K\u00e9szletek \u00e9rt\u00e9kveszt\u00e9s\u00e9nek vissza\u00edr\u00e1sa": { + "account_number": 9683, + "root_type": "Income" + }, + "R\u00e9szesed\u00e9sek \u00e9rt\u00e9kveszt\u00e9s\u00e9nek vissza\u00edr\u00e1sa": { + "account_number": 9684, + "root_type": "Income" + } + }, + "K\u00fcl\u00f6nf\u00e9le egy\u00e9b bev\u00e9telek": { + "account_number": 969, + "root_type": "Income", + "is_group": 1, + "Gazd. t\u00e1rs. bevitt, \u00e9rt\u00e9kp. v. r\u00e9sz. nem min\u0151s\u00fcl\u0151 vagyont. nyilv. sz. \u00e9rt. \u00e9s l\u00e9tes\u00edt\u0151 o. meghat. \u00e9rt. nyeres\u00e9gjelleg\u0171 k\u00fcl.": { + "account_number": 9691, + "root_type": "Income" + }, + "El\u00e9v\u00fclt k\u00f6telezetts\u00e9g k\u00f6nyv szerinti \u00e9rt\u00e9ke": { + "account_number": 9692, + "root_type": "Income" + }, + "T\u00e9r\u00edt\u00e9s n\u00e9lk\u00fcl \u00e1tvett, aj\u00e1nd\u00e9kk\u00e9nt, hagyat\u00e9kk\u00e9nt kapott, fellelt eszk\u00f6z\u00f6k piaci vagy jogszab\u00e1ly szerinti \u00e9rt\u00e9ke": { + "account_number": 9693, + "root_type": "Income" + }, + "T\u00e9r\u00edt\u00e9s n\u00e9lk\u00fcl kapott szolg\u00e1ltat\u00e1sok piaci vagy jogszab\u00e1ly szerinti \u00e9rt\u00e9ke": { + "account_number": 9694, + "root_type": "Income" + }, + "Elengedett \u00e9s ellent\u00e9telez\u00e9s n\u00e9lk\u00fcl \u00e1tv\u00e1llalt k\u00f6telezetts\u00e9g \u00e9rt\u00e9ke": { + "account_number": 9695, + "root_type": "Income" + }, + "Egy\u00e9b, vagyonn\u00f6veked\u00e9ssel j\u00e1r\u00f3 bev\u00e9telek": { + "account_number": 9696, + "root_type": "Income" + } + } + }, + "P\u00c9NZ\u00dcGYI M\u0170VELETEK BEV\u00c9TELEI": { + "account_number": 97, + "root_type": "Income", + "is_group": 1, + "R\u00e9szesed\u00e9sekb\u0151l sz\u00e1rmaz\u00f3 bev\u00e9telek, \u00e1rfolyamnyeres\u00e9gek": { + "account_number": 971, + "root_type": "Income" + }, + "Befektetett p\u00e9nz\u00fcgyi eszk\u00f6z\u00f6kb\u0151l (\u00e9rt\u00e9kpap\u00edrokb\u00f3l, k\u00f6lcs\u00f6n\u00f6kb\u0151l) sz\u00e1rmaz\u00f3 bev\u00e9telek, \u00e1rfolyamnyeres\u00e9gek": { + "account_number": 972, + "root_type": "Income" + }, + "Hitelint\u00e9zett\u0151l kapott kamatok \u00e9s kamatjelleg\u0171 bev\u00e9telek": { + "account_number": 973, + "root_type": "Income" + }, + "Nem hitelint\u00e9zett\u0151l kapott kamatok \u00e9s kamatjelleg\u0171 bev\u00e9telek": { + "account_number": 974, + "root_type": "Income" + }, + "Kapott (j\u00e1r\u00f3) osztal\u00e9k \u00e9s r\u00e9szesed\u00e9s": { + "account_number": 975, + "root_type": "Income" + }, + "K\u00fclf\u00f6ldi p\u00e9nz\u00e9rt\u00e9kre sz\u00f3l\u00f3 eszk\u00f6z\u00f6k \u00e9s k\u00f6telezetts\u00e9gek \u00e1rfolyamnyeres\u00e9gei": { + "account_number": 976, + "root_type": "Income", + "is_group": 1, + "Deviza \u00e9s valutak\u00e9szletek forintra \u00e1tv\u00e1lt\u00e1s\u00e1nak \u00e1rfolyamnyeres\u00e9ge": { + "account_number": 9761, + "root_type": "Income" + }, + "K\u00fclf\u00f6ldi p\u00e9nz\u00e9rt\u00e9kre sz\u00f3l\u00f3 eszk\u00f6z\u00f6k \u00e9s k\u00f6telezetts\u00e9gek p\u00e9nz\u00fcgyileg rendezett \u00e1rfolyamnyeres\u00e9ge": { + "account_number": 9762, + "root_type": "Income" + } + }, + "P\u00e9nz\u00fcgyi m\u0171veletek egy\u00e9b bev\u00e9telei": { + "account_number": 977, + "root_type": "Income" + }, + "P\u00e9nz\u00fcgyi rendez\u00e9shez kapcsol\u00f3d\u00f3an kapott engedm\u00e9ny": { + "account_number": 978, + "root_type": "Income" + }, + "Egy\u00e9b vagyonn\u00f6veked\u00e9ssel j\u00e1r\u00f3 p\u00e9nz\u00fcgyi bev\u00e9telek": { + "account_number": 979, + "root_type": "Income" + } + } + } + } +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js index 750e129ba7..8a6b021b8a 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js +++ b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js @@ -3,10 +3,6 @@ frappe.ui.form.on('Accounting Dimension Filter', { refresh: function(frm, cdt, cdn) { - if (frm.doc.accounting_dimension) { - frm.set_df_property('dimensions', 'label', frm.doc.accounting_dimension, cdn, 'dimension_value'); - } - let help_content = `
@@ -68,6 +64,7 @@ frappe.ui.form.on('Accounting Dimension Filter', { frm.clear_table("dimensions"); let row = frm.add_child("dimensions"); row.accounting_dimension = frm.doc.accounting_dimension; + frm.fields_dict["dimensions"].grid.update_docfield_property("dimension_value", "label", frm.doc.accounting_dimension); frm.refresh_field("dimensions"); frm.trigger('setup_filters'); }, diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index 3e0b82c561..3f985b640b 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -56,7 +56,9 @@ "acc_frozen_upto", "column_break_25", "frozen_accounts_modifier", - "report_settings_sb" + "report_settings_sb", + "tab_break_dpet", + "show_balance_in_coa" ], "fields": [ { @@ -91,7 +93,7 @@ }, { "default": "0", - "description": "Enabling ensure each Sales Invoice has a unique value in Supplier Invoice No. field", + "description": "Enabling ensure each Purchase Invoice has a unique value in Supplier Invoice No. field", "fieldname": "check_supplier_invoice_uniqueness", "fieldtype": "Check", "label": "Check Supplier Invoice Number Uniqueness" @@ -347,6 +349,17 @@ "fieldname": "allow_multi_currency_invoices_against_single_party_account", "fieldtype": "Check", "label": "Allow multi-currency invoices against single party account " + }, + { + "fieldname": "tab_break_dpet", + "fieldtype": "Tab Break", + "label": "Chart Of Accounts" + }, + { + "default": "1", + "fieldname": "show_balance_in_coa", + "fieldtype": "Check", + "label": "Show Balances in Chart Of Accounts" } ], "icon": "icon-cog", @@ -354,7 +367,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2022-07-11 13:37:50.605141", + "modified": "2023-01-02 12:07:42.434214", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Settings", diff --git a/erpnext/accounts/doctype/bank/bank.js b/erpnext/accounts/doctype/bank/bank.js index 059e1d3158..35d606ba3a 100644 --- a/erpnext/accounts/doctype/bank/bank.js +++ b/erpnext/accounts/doctype/bank/bank.js @@ -118,6 +118,10 @@ erpnext.integrations.refreshPlaidLink = class refreshPlaidLink { } plaid_success(token, response) { - frappe.show_alert({ message: __('Plaid Link Updated'), indicator: 'green' }); + frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.update_bank_account_ids', { + response: response, + }).then(() => { + frappe.show_alert({ message: __('Plaid Link Updated'), indicator: 'green' }); + }); } }; diff --git a/erpnext/accounts/doctype/bank_account/bank_account.py b/erpnext/accounts/doctype/bank_account/bank_account.py index addcf62e5b..b91f0f9137 100644 --- a/erpnext/accounts/doctype/bank_account/bank_account.py +++ b/erpnext/accounts/doctype/bank_account/bank_account.py @@ -77,6 +77,6 @@ def get_party_bank_account(party_type, party): @frappe.whitelist() def get_bank_account_details(bank_account): - return frappe.db.get_value( + return frappe.get_cached_value( "Bank Account", bank_account, ["account", "bank", "bank_account_no"], as_dict=1 ) diff --git a/erpnext/accounts/doctype/bank_clearance/bank_clearance.js b/erpnext/accounts/doctype/bank_clearance/bank_clearance.js index 63cc46518f..71f2dcca1b 100644 --- a/erpnext/accounts/doctype/bank_clearance/bank_clearance.js +++ b/erpnext/accounts/doctype/bank_clearance/bank_clearance.js @@ -4,6 +4,23 @@ frappe.ui.form.on("Bank Clearance", { setup: function(frm) { frm.add_fetch("account", "account_currency", "account_currency"); + + frm.set_query("account", function() { + return { + "filters": { + "account_type": ["in",["Bank","Cash"]], + "is_group": 0, + } + }; + }); + + frm.set_query("bank_account", function () { + return { + filters: { + 'is_company_account': 1 + }, + }; + }); }, onload: function(frm) { @@ -12,14 +29,7 @@ frappe.ui.form.on("Bank Clearance", { locals[":Company"][frappe.defaults.get_user_default("Company")]["default_bank_account"]: ""; frm.set_value("account", default_bank_account); - frm.set_query("account", function() { - return { - "filters": { - "account_type": ["in",["Bank","Cash"]], - "is_group": 0 - } - }; - }); + frm.set_value("from_date", frappe.datetime.month_start()); frm.set_value("to_date", frappe.datetime.month_end()); @@ -27,6 +37,11 @@ frappe.ui.form.on("Bank Clearance", { refresh: function(frm) { frm.disable_save(); + frm.add_custom_button(__('Get Payment Entries'), () => + frm.trigger("get_payment_entries") + ); + + frm.change_custom_button_type('Get Payment Entries', null, 'primary'); }, update_clearance_date: function(frm) { @@ -36,22 +51,30 @@ frappe.ui.form.on("Bank Clearance", { callback: function(r, rt) { frm.refresh_field("payment_entries"); frm.refresh_fields(); + + if (!frm.doc.payment_entries.length) { + frm.change_custom_button_type('Get Payment Entries', null, 'primary'); + frm.change_custom_button_type('Update Clearance Date', null, 'default'); + } } }); }, + get_payment_entries: function(frm) { return frappe.call({ method: "get_payment_entries", doc: frm.doc, callback: function(r, rt) { frm.refresh_field("payment_entries"); - frm.refresh_fields(); - $(frm.fields_dict.payment_entries.wrapper).find("[data-fieldname=amount]").each(function(i,v){ - if (i !=0){ - $(v).addClass("text-right") - } - }) + if (frm.doc.payment_entries.length) { + frm.add_custom_button(__('Update Clearance Date'), () => + frm.trigger("update_clearance_date") + ); + + frm.change_custom_button_type('Get Payment Entries', null, 'default'); + frm.change_custom_button_type('Update Clearance Date', null, 'primary'); + } } }); } diff --git a/erpnext/accounts/doctype/bank_clearance/bank_clearance.json b/erpnext/accounts/doctype/bank_clearance/bank_clearance.json index a436d1effb..591d01949b 100644 --- a/erpnext/accounts/doctype/bank_clearance/bank_clearance.json +++ b/erpnext/accounts/doctype/bank_clearance/bank_clearance.json @@ -1,4 +1,5 @@ { + "actions": [], "allow_copy": 1, "creation": "2013-01-10 16:34:05", "doctype": "DocType", @@ -13,11 +14,8 @@ "bank_account", "include_reconciled_entries", "include_pos_transactions", - "get_payment_entries", "section_break_10", - "payment_entries", - "update_clearance_date", - "total_amount" + "payment_entries" ], "fields": [ { @@ -76,11 +74,6 @@ "fieldtype": "Check", "label": "Include POS Transactions" }, - { - "fieldname": "get_payment_entries", - "fieldtype": "Button", - "label": "Get Payment Entries" - }, { "fieldname": "section_break_10", "fieldtype": "Section Break" @@ -91,25 +84,14 @@ "fieldtype": "Table", "label": "Payment Entries", "options": "Bank Clearance Detail" - }, - { - "fieldname": "update_clearance_date", - "fieldtype": "Button", - "label": "Update Clearance Date" - }, - { - "fieldname": "total_amount", - "fieldtype": "Currency", - "label": "Total Amount", - "options": "account_currency", - "read_only": 1 } ], "hide_toolbar": 1, "icon": "fa fa-check", "idx": 1, "issingle": 1, - "modified": "2020-04-06 16:12:06.628008", + "links": [], + "modified": "2022-11-28 17:24:13.008692", "modified_by": "Administrator", "module": "Accounts", "name": "Bank Clearance", @@ -126,5 +108,6 @@ "quick_entry": 1, "read_only": 1, "sort_field": "modified", - "sort_order": "ASC" + "sort_order": "ASC", + "states": [] } \ No newline at end of file diff --git a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py index 1a572d9823..80878ac506 100644 --- a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py +++ b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py @@ -99,7 +99,7 @@ class BankClearance(Document): .where(loan_disbursement.clearance_date.isnull()) .where(loan_disbursement.disbursement_account.isin([self.bank_account, self.account])) .orderby(loan_disbursement.disbursement_date) - .orderby(loan_disbursement.name, frappe.qb.desc) + .orderby(loan_disbursement.name, order=frappe.qb.desc) ).run(as_dict=1) loan_repayment = frappe.qb.DocType("Loan Repayment") @@ -126,7 +126,9 @@ class BankClearance(Document): if frappe.db.has_column("Loan Repayment", "repay_from_salary"): query = query.where((loan_repayment.repay_from_salary == 0)) - query = query.orderby(loan_repayment.posting_date).orderby(loan_repayment.name, frappe.qb.desc) + query = query.orderby(loan_repayment.posting_date).orderby( + loan_repayment.name, order=frappe.qb.desc + ) loan_repayments = query.run(as_dict=True) @@ -177,7 +179,6 @@ class BankClearance(Document): ) self.set("payment_entries", []) - self.total_amount = 0.0 default_currency = erpnext.get_default_currency() for d in entries: @@ -196,7 +197,6 @@ class BankClearance(Document): d.pop("debit") d.pop("account_currency") row.update(d) - self.total_amount += flt(amount) @frappe.whitelist() def update_clearance_date(self): diff --git a/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.js b/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.js index febf85ca6c..99cc0a72fb 100644 --- a/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.js +++ b/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.js @@ -43,20 +43,13 @@ frappe.ui.form.on('Bank Guarantee', { reference_docname: function(frm) { if (frm.doc.reference_docname && frm.doc.reference_doctype) { - let fields_to_fetch = ["grand_total"]; let party_field = frm.doc.reference_doctype == "Sales Order" ? "customer" : "supplier"; - if (frm.doc.reference_doctype == "Sales Order") { - fields_to_fetch.push("project"); - } - - fields_to_fetch.push(party_field); frappe.call({ - method: "erpnext.accounts.doctype.bank_guarantee.bank_guarantee.get_vouchar_detials", + method: "erpnext.accounts.doctype.bank_guarantee.bank_guarantee.get_voucher_details", args: { - "column_list": fields_to_fetch, - "doctype": frm.doc.reference_doctype, - "docname": frm.doc.reference_docname + "bank_guarantee_type": frm.doc.bg_type, + "reference_name": frm.doc.reference_docname }, callback: function(r) { if (r.message) { diff --git a/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.py b/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.py index 9144a29c6e..02eb599acc 100644 --- a/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.py +++ b/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.py @@ -2,11 +2,8 @@ # For license information, please see license.txt -import json - import frappe from frappe import _ -from frappe.desk.search import sanitize_searchfield from frappe.model.document import Document @@ -25,14 +22,18 @@ class BankGuarantee(Document): @frappe.whitelist() -def get_vouchar_detials(column_list, doctype, docname): - column_list = json.loads(column_list) - for col in column_list: - sanitize_searchfield(col) - return frappe.db.sql( - """ select {columns} from `tab{doctype}` where name=%s""".format( - columns=", ".join(column_list), doctype=doctype - ), - docname, - as_dict=1, - )[0] +def get_voucher_details(bank_guarantee_type: str, reference_name: str): + if not isinstance(reference_name, str): + raise TypeError("reference_name must be a string") + + fields_to_fetch = ["grand_total"] + + if bank_guarantee_type == "Receiving": + doctype = "Sales Order" + fields_to_fetch.append("customer") + fields_to_fetch.append("project") + else: + doctype = "Purchase Order" + fields_to_fetch.append("supplier") + + return frappe.db.get_value(doctype, reference_name, fields_to_fetch, as_dict=True) 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 46ba27c004..ae84154f2d 100644 --- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js +++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js @@ -12,19 +12,31 @@ frappe.ui.form.on("Bank Reconciliation Tool", { }, }; }); + let no_bank_transactions_text = + `
${__("No Matching Bank Transactions Found")}
` + set_field_options("no_bank_transactions", no_bank_transactions_text); }, onload: function (frm) { frm.trigger('bank_account'); }, + filter_by_reference_date: function (frm) { + if (frm.doc.filter_by_reference_date) { + frm.set_value("bank_statement_from_date", ""); + frm.set_value("bank_statement_to_date", ""); + } else { + frm.set_value("from_reference_date", ""); + frm.set_value("to_reference_date", ""); + } + }, + refresh: function (frm) { frappe.require("bank-reconciliation-tool.bundle.js", () => frm.trigger("make_reconciliation_tool") ); - frm.upload_statement_button = frm.page.set_secondary_action( - __("Upload Bank Statement"), - () => + + frm.add_custom_button(__("Upload Bank Statement"), () => frappe.call({ method: "erpnext.accounts.doctype.bank_statement_import.bank_statement_import.upload_bank_statement", @@ -46,6 +58,20 @@ frappe.ui.form.on("Bank Reconciliation Tool", { }, }) ); + + frm.add_custom_button(__('Auto Reconcile'), function() { + frappe.call({ + method: "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.auto_reconcile_vouchers", + args: { + bank_account: frm.doc.bank_account, + from_date: frm.doc.bank_statement_from_date, + to_date: frm.doc.bank_statement_to_date, + filter_by_reference_date: frm.doc.filter_by_reference_date, + from_reference_date: frm.doc.from_reference_date, + to_reference_date: frm.doc.to_reference_date, + }, + }) + }); }, after_save: function (frm) { @@ -129,7 +155,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", { } }, - render_chart: frappe.utils.debounce((frm) => { + render_chart(frm) { frm.cards_manager = new erpnext.accounts.bank_reconciliation.NumberCardManager( { $reconciliation_tool_cards: frm.get_field( @@ -141,7 +167,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", { currency: frm.currency, } ); - }, 500), + }, render(frm) { if (frm.doc.bank_account) { @@ -157,6 +183,9 @@ frappe.ui.form.on("Bank Reconciliation Tool", { ).$wrapper, bank_statement_from_date: frm.doc.bank_statement_from_date, bank_statement_to_date: frm.doc.bank_statement_to_date, + filter_by_reference_date: frm.doc.filter_by_reference_date, + from_reference_date: frm.doc.from_reference_date, + to_reference_date: frm.doc.to_reference_date, bank_statement_closing_balance: frm.doc.bank_statement_closing_balance, cards_manager: frm.cards_manager, diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.json b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.json index b643e6e091..80993d6608 100644 --- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.json +++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.json @@ -10,6 +10,9 @@ "column_break_1", "bank_statement_from_date", "bank_statement_to_date", + "from_reference_date", + "to_reference_date", + "filter_by_reference_date", "column_break_2", "account_opening_balance", "bank_statement_closing_balance", @@ -36,13 +39,13 @@ "fieldtype": "Column Break" }, { - "depends_on": "eval: doc.bank_account", + "depends_on": "eval: doc.bank_account && !doc.filter_by_reference_date", "fieldname": "bank_statement_from_date", "fieldtype": "Date", "label": "From Date" }, { - "depends_on": "eval: doc.bank_statement_from_date", + "depends_on": "eval: doc.bank_account && !doc.filter_by_reference_date", "fieldname": "bank_statement_to_date", "fieldtype": "Date", "label": "To Date" @@ -83,13 +86,31 @@ "fieldname": "no_bank_transactions", "fieldtype": "HTML", "options": "
No Matching Bank Transactions Found
" + }, + { + "depends_on": "eval:doc.filter_by_reference_date", + "fieldname": "from_reference_date", + "fieldtype": "Date", + "label": "From Reference Date" + }, + { + "depends_on": "eval:doc.filter_by_reference_date", + "fieldname": "to_reference_date", + "fieldtype": "Date", + "label": "To Reference Date" + }, + { + "default": "0", + "fieldname": "filter_by_reference_date", + "fieldtype": "Check", + "label": "Filter by Reference Date" } ], "hide_toolbar": 1, "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2021-04-21 11:13:49.831769", + "modified": "2023-01-13 13:00:02.022919", "modified_by": "Administrator", "module": "Accounts", "name": "Bank Reconciliation Tool", @@ -108,5 +129,6 @@ ], "quick_entry": 1, "sort_field": "modified", - "sort_order": "DESC" + "sort_order": "DESC", + "states": [] } \ No newline at end of file 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 cc3727ce83..c4a23a640c 100644 --- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py +++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py @@ -8,9 +8,9 @@ import frappe from frappe import _ from frappe.model.document import Document from frappe.query_builder.custom import ConstantColumn -from frappe.utils import flt +from frappe.utils import cint, flt -from erpnext.accounts.doctype.bank_transaction.bank_transaction import get_paid_amount +from erpnext.accounts.doctype.bank_transaction.bank_transaction import get_total_allocated_amount from erpnext.accounts.report.bank_reconciliation_statement.bank_reconciliation_statement import ( get_amounts_not_reflected_in_system, get_entries, @@ -28,7 +28,7 @@ def get_bank_transactions(bank_account, from_date=None, to_date=None): filters = [] filters.append(["bank_account", "=", bank_account]) filters.append(["docstatus", "=", 1]) - filters.append(["unallocated_amount", ">", 0]) + filters.append(["unallocated_amount", ">", 0.0]) if to_date: filters.append(["date", "<=", to_date]) if from_date: @@ -50,6 +50,7 @@ def get_bank_transactions(bank_account, from_date=None, to_date=None): "party", ], filters=filters, + order_by="date", ) return transactions @@ -65,7 +66,7 @@ def get_account_balance(bank_account, till_date): balance_as_per_system = get_balance_on(filters["account"], filters["report_date"]) - total_debit, total_credit = 0, 0 + total_debit, total_credit = 0.0, 0.0 for d in data: total_debit += flt(d.debit) total_credit += flt(d.credit) @@ -144,10 +145,8 @@ def create_journal_entry_bts( accounts.append( { "account": second_account, - "credit_in_account_currency": bank_transaction.deposit if bank_transaction.deposit > 0 else 0, - "debit_in_account_currency": bank_transaction.withdrawal - if bank_transaction.withdrawal > 0 - else 0, + "credit_in_account_currency": bank_transaction.deposit, + "debit_in_account_currency": bank_transaction.withdrawal, "party_type": party_type, "party": party, } @@ -157,10 +156,8 @@ def create_journal_entry_bts( { "account": company_account, "bank_account": bank_transaction.bank_account, - "credit_in_account_currency": bank_transaction.withdrawal - if bank_transaction.withdrawal > 0 - else 0, - "debit_in_account_currency": bank_transaction.deposit if bank_transaction.deposit > 0 else 0, + "credit_in_account_currency": bank_transaction.withdrawal, + "debit_in_account_currency": bank_transaction.deposit, } ) @@ -184,16 +181,22 @@ def create_journal_entry_bts( journal_entry.insert() journal_entry.submit() - if bank_transaction.deposit > 0: + if bank_transaction.deposit > 0.0: paid_amount = bank_transaction.deposit else: paid_amount = bank_transaction.withdrawal vouchers = json.dumps( - [{"payment_doctype": "Journal Entry", "payment_name": journal_entry.name, "amount": paid_amount}] + [ + { + "payment_doctype": "Journal Entry", + "payment_name": journal_entry.name, + "amount": paid_amount, + } + ] ) - return reconcile_vouchers(bank_transaction.name, vouchers) + return reconcile_vouchers(bank_transaction_name, vouchers) @frappe.whitelist() @@ -217,7 +220,7 @@ def create_payment_entry_bts( as_dict=True, )[0] paid_amount = bank_transaction.unallocated_amount - payment_type = "Receive" if bank_transaction.deposit > 0 else "Pay" + payment_type = "Receive" if bank_transaction.deposit > 0.0 else "Pay" company_account = frappe.get_value("Bank Account", bank_transaction.bank_account, "account") company = frappe.get_value("Account", company_account, "company") @@ -256,9 +259,89 @@ def create_payment_entry_bts( payment_entry.submit() vouchers = json.dumps( - [{"payment_doctype": "Payment Entry", "payment_name": payment_entry.name, "amount": paid_amount}] + [ + { + "payment_doctype": "Payment Entry", + "payment_name": payment_entry.name, + "amount": paid_amount, + } + ] ) - return reconcile_vouchers(bank_transaction.name, vouchers) + return reconcile_vouchers(bank_transaction_name, vouchers) + + +@frappe.whitelist() +def auto_reconcile_vouchers( + bank_account, + from_date=None, + to_date=None, + filter_by_reference_date=None, + from_reference_date=None, + to_reference_date=None, +): + frappe.flags.auto_reconcile_vouchers = True + document_types = ["payment_entry", "journal_entry"] + bank_transactions = get_bank_transactions(bank_account) + matched_transaction = [] + for transaction in bank_transactions: + linked_payments = get_linked_payments( + transaction.name, + document_types, + from_date, + to_date, + filter_by_reference_date, + from_reference_date, + to_reference_date, + ) + vouchers = [] + for r in linked_payments: + vouchers.append( + { + "payment_doctype": r[1], + "payment_name": r[2], + "amount": r[4], + } + ) + transaction = frappe.get_doc("Bank Transaction", transaction.name) + account = frappe.db.get_value("Bank Account", transaction.bank_account, "account") + matched_trans = 0 + for voucher in vouchers: + gl_entry = frappe.db.get_value( + "GL Entry", + dict( + account=account, voucher_type=voucher["payment_doctype"], voucher_no=voucher["payment_name"] + ), + ["credit", "debit"], + as_dict=1, + ) + gl_amount, transaction_amount = ( + (gl_entry.credit, transaction.deposit) + if gl_entry.credit > 0 + else (gl_entry.debit, transaction.withdrawal) + ) + allocated_amount = gl_amount if gl_amount >= transaction_amount else transaction_amount + transaction.append( + "payment_entries", + { + "payment_document": voucher["payment_doctype"], + "payment_entry": voucher["payment_name"], + "allocated_amount": allocated_amount, + }, + ) + matched_transaction.append(str(transaction.name)) + transaction.save() + transaction.update_allocations() + matched_transaction_len = len(set(matched_transaction)) + if matched_transaction_len == 0: + frappe.msgprint(_("No matching references found for auto reconciliation")) + elif matched_transaction_len == 1: + frappe.msgprint(_("{0} transaction is reconcilied").format(matched_transaction_len)) + else: + frappe.msgprint(_("{0} transactions are reconcilied").format(matched_transaction_len)) + + frappe.flags.auto_reconcile_vouchers = False + + return frappe.get_doc("Bank Transaction", transaction.name) @frappe.whitelist() @@ -266,80 +349,88 @@ def reconcile_vouchers(bank_transaction_name, vouchers): # 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 - for voucher in vouchers: - voucher["payment_entry"] = frappe.get_doc(voucher["payment_doctype"], voucher["payment_name"]) - total_amount += get_paid_amount( - frappe._dict( - { - "payment_document": voucher["payment_doctype"], - "payment_entry": voucher["payment_name"], - } - ), - 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" - ) - ) - account = frappe.db.get_value("Bank Account", transaction.bank_account, "account") - - for voucher in vouchers: - gl_entry = frappe.db.get_value( - "GL Entry", - dict( - account=account, voucher_type=voucher["payment_doctype"], voucher_no=voucher["payment_name"] - ), - ["credit", "debit"], - as_dict=1, - ) - gl_amount, transaction_amount = ( - (gl_entry.credit, transaction.deposit) - if gl_entry.credit > 0 - else (gl_entry.debit, transaction.withdrawal) - ) - allocated_amount = gl_amount if gl_amount >= transaction_amount else transaction_amount - - transaction.append( - "payment_entries", - { - "payment_document": voucher["payment_entry"].doctype, - "payment_entry": voucher["payment_entry"].name, - "allocated_amount": allocated_amount, - }, - ) - - transaction.save() - transaction.update_allocations() + transaction.add_payment_entries(vouchers) return frappe.get_doc("Bank Transaction", bank_transaction_name) @frappe.whitelist() -def get_linked_payments(bank_transaction_name, document_types=None): +def get_linked_payments( + bank_transaction_name, + document_types=None, + from_date=None, + to_date=None, + filter_by_reference_date=None, + from_reference_date=None, + to_reference_date=None, +): # get all matching payments for a bank transaction transaction = frappe.get_doc("Bank Transaction", bank_transaction_name) bank_account = frappe.db.get_values( "Bank Account", transaction.bank_account, ["account", "company"], as_dict=True )[0] - (account, company) = (bank_account.account, bank_account.company) - matching = check_matching(account, company, transaction, document_types) - return matching + (gl_account, company) = (bank_account.account, bank_account.company) + matching = check_matching( + gl_account, + company, + transaction, + document_types, + from_date, + to_date, + filter_by_reference_date, + from_reference_date, + to_reference_date, + ) + return subtract_allocations(gl_account, matching) -def check_matching(bank_account, company, transaction, document_types): +def subtract_allocations(gl_account, vouchers): + "Look up & subtract any existing Bank Transaction allocations" + copied = [] + for voucher in vouchers: + rows = get_total_allocated_amount(voucher[1], voucher[2]) + amount = None + for row in rows: + if row["gl_account"] == gl_account: + amount = row["total"] + break + + if amount: + l = list(voucher) + l[3] -= amount + copied.append(tuple(l)) + else: + copied.append(voucher) + return copied + + +def check_matching( + bank_account, + company, + transaction, + document_types, + from_date, + to_date, + filter_by_reference_date, + from_reference_date, + to_reference_date, +): + exact_match = True if "exact_match" in document_types else False # combine all types of vouchers - subquery = get_queries(bank_account, company, transaction, document_types) + subquery = get_queries( + bank_account, + company, + transaction, + document_types, + from_date, + to_date, + filter_by_reference_date, + from_reference_date, + to_reference_date, + exact_match, + ) filters = { "amount": transaction.unallocated_amount, - "payment_type": "Receive" if transaction.deposit > 0 else "Pay", + "payment_type": "Receive" if transaction.deposit > 0.0 else "Pay", "reference_no": transaction.reference_number, "party_type": transaction.party_type, "party": transaction.party, @@ -348,7 +439,9 @@ def check_matching(bank_account, company, transaction, document_types): matching_vouchers = [] - matching_vouchers.extend(get_loan_vouchers(bank_account, transaction, document_types, filters)) + matching_vouchers.extend( + get_loan_vouchers(bank_account, transaction, document_types, filters, exact_match) + ) for query in subquery: matching_vouchers.extend( @@ -357,14 +450,23 @@ def check_matching(bank_account, company, transaction, document_types): filters, ) ) - return sorted(matching_vouchers, key=lambda x: x[0], reverse=True) if matching_vouchers else [] -def get_queries(bank_account, company, transaction, document_types): +def get_queries( + bank_account, + company, + transaction, + document_types, + from_date, + to_date, + filter_by_reference_date, + from_reference_date, + to_reference_date, + exact_match, +): # get queries to get matching vouchers - amount_condition = "=" if "exact_match" in document_types else "<=" - account_from_to = "paid_to" if transaction.deposit > 0 else "paid_from" + account_from_to = "paid_to" if transaction.deposit > 0.0 else "paid_from" queries = [] # get matching queries from all the apps @@ -375,8 +477,13 @@ def get_queries(bank_account, company, transaction, document_types): company, transaction, document_types, - amount_condition, + exact_match, account_from_to, + from_date, + to_date, + filter_by_reference_date, + from_reference_date, + to_reference_date, ) or [] ) @@ -385,43 +492,106 @@ def get_queries(bank_account, company, transaction, document_types): def get_matching_queries( - bank_account, company, transaction, document_types, amount_condition, account_from_to + bank_account, + company, + transaction, + document_types, + exact_match, + account_from_to, + from_date, + to_date, + filter_by_reference_date, + from_reference_date, + to_reference_date, ): queries = [] if "payment_entry" in document_types: - pe_amount_matching = get_pe_matching_query(amount_condition, account_from_to, transaction) - queries.extend([pe_amount_matching]) + query = get_pe_matching_query( + exact_match, + account_from_to, + transaction, + from_date, + to_date, + filter_by_reference_date, + from_reference_date, + to_reference_date, + ) + queries.append(query) if "journal_entry" in document_types: - je_amount_matching = get_je_matching_query(amount_condition, transaction) - queries.extend([je_amount_matching]) + query = get_je_matching_query( + exact_match, + transaction, + from_date, + to_date, + filter_by_reference_date, + from_reference_date, + to_reference_date, + ) + queries.append(query) - if transaction.deposit > 0 and "sales_invoice" in document_types: - si_amount_matching = get_si_matching_query(amount_condition) - queries.extend([si_amount_matching]) + if transaction.deposit > 0.0 and "sales_invoice" in document_types: + query = get_si_matching_query(exact_match) + queries.append(query) - if transaction.withdrawal > 0: + if transaction.withdrawal > 0.0: if "purchase_invoice" in document_types: - pi_amount_matching = get_pi_matching_query(amount_condition) - queries.extend([pi_amount_matching]) + query = get_pi_matching_query(exact_match) + queries.append(query) + + if "bank_transaction" in document_types: + query = get_bt_matching_query(exact_match, transaction) + queries.append(query) return queries -def get_loan_vouchers(bank_account, transaction, document_types, filters): +def get_loan_vouchers(bank_account, transaction, document_types, filters, exact_match): vouchers = [] - amount_condition = True if "exact_match" in document_types else False - if transaction.withdrawal > 0 and "loan_disbursement" in document_types: - vouchers.extend(get_ld_matching_query(bank_account, amount_condition, filters)) + if transaction.withdrawal > 0.0 and "loan_disbursement" in document_types: + vouchers.extend(get_ld_matching_query(bank_account, exact_match, filters)) - if transaction.deposit > 0 and "loan_repayment" in document_types: - vouchers.extend(get_lr_matching_query(bank_account, amount_condition, filters)) + if transaction.deposit > 0.0 and "loan_repayment" in document_types: + vouchers.extend(get_lr_matching_query(bank_account, exact_match, filters)) return vouchers -def get_ld_matching_query(bank_account, amount_condition, filters): +def get_bt_matching_query(exact_match, transaction): + # get matching bank transaction query + # find bank transactions in the same bank account with opposite sign + # same bank account must have same company and currency + field = "deposit" if transaction.withdrawal > 0.0 else "withdrawal" + + return f""" + + SELECT + (CASE WHEN reference_number = %(reference_no)s THEN 1 ELSE 0 END + + CASE WHEN {field} = %(amount)s THEN 1 ELSE 0 END + + CASE WHEN ( party_type = %(party_type)s AND party = %(party)s ) THEN 1 ELSE 0 END + + CASE WHEN unallocated_amount = %(amount)s THEN 1 ELSE 0 END + + 1) AS rank, + 'Bank Transaction' AS doctype, + name, + unallocated_amount AS paid_amount, + reference_number AS reference_no, + date AS reference_date, + party, + party_type, + date AS posting_date, + currency + FROM + `tabBank Transaction` + WHERE + status != 'Reconciled' + AND name != '{transaction.name}' + AND bank_account = '{transaction.bank_account}' + AND {field} {'= %(amount)s' if exact_match else '> 0.0'} + """ + + +def get_ld_matching_query(bank_account, exact_match, filters): loan_disbursement = frappe.qb.DocType("Loan Disbursement") matching_reference = loan_disbursement.reference_number == filters.get("reference_number") matching_party = loan_disbursement.applicant_type == filters.get( @@ -449,17 +619,17 @@ def get_ld_matching_query(bank_account, amount_condition, filters): .where(loan_disbursement.disbursement_account == bank_account) ) - if amount_condition: + if exact_match: query.where(loan_disbursement.disbursed_amount == filters.get("amount")) else: - query.where(loan_disbursement.disbursed_amount <= filters.get("amount")) + query.where(loan_disbursement.disbursed_amount > 0.0) vouchers = query.run(as_list=True) return vouchers -def get_lr_matching_query(bank_account, amount_condition, filters): +def get_lr_matching_query(bank_account, exact_match, filters): loan_repayment = frappe.qb.DocType("Loan Repayment") matching_reference = loan_repayment.reference_number == filters.get("reference_number") matching_party = loan_repayment.applicant_type == filters.get( @@ -490,88 +660,129 @@ def get_lr_matching_query(bank_account, amount_condition, filters): if frappe.db.has_column("Loan Repayment", "repay_from_salary"): query = query.where((loan_repayment.repay_from_salary == 0)) - if amount_condition: + if exact_match: query.where(loan_repayment.amount_paid == filters.get("amount")) else: - query.where(loan_repayment.amount_paid <= filters.get("amount")) + query.where(loan_repayment.amount_paid > 0.0) vouchers = query.run() return vouchers -def get_pe_matching_query(amount_condition, account_from_to, transaction): +def get_pe_matching_query( + exact_match, + account_from_to, + transaction, + from_date, + to_date, + filter_by_reference_date, + from_reference_date, + to_reference_date, +): # get matching payment entries query - if transaction.deposit > 0: + if transaction.deposit > 0.0: currency_field = "paid_to_account_currency as currency" else: currency_field = "paid_from_account_currency as currency" + filter_by_date = f"AND posting_date between '{from_date}' and '{to_date}'" + order_by = " posting_date" + filter_by_reference_no = "" + if cint(filter_by_reference_date): + filter_by_date = f"AND reference_date between '{from_reference_date}' and '{to_reference_date}'" + order_by = " reference_date" + if frappe.flags.auto_reconcile_vouchers == True: + filter_by_reference_no = f"AND reference_no = '{transaction.reference_number}'" return f""" - SELECT - (CASE WHEN reference_no=%(reference_no)s THEN 1 ELSE 0 END - + CASE WHEN (party_type = %(party_type)s AND party = %(party)s ) THEN 1 ELSE 0 END - + 1 ) AS rank, - 'Payment Entry' as doctype, - name, - paid_amount, - reference_no, - reference_date, - party, - party_type, - posting_date, - {currency_field} - FROM - `tabPayment Entry` - WHERE - paid_amount {amount_condition} %(amount)s - AND docstatus = 1 - AND payment_type IN (%(payment_type)s, 'Internal Transfer') - AND ifnull(clearance_date, '') = "" - AND {account_from_to} = %(bank_account)s + SELECT + (CASE WHEN reference_no=%(reference_no)s THEN 1 ELSE 0 END + + CASE WHEN (party_type = %(party_type)s AND party = %(party)s ) THEN 1 ELSE 0 END + + CASE WHEN paid_amount = %(amount)s THEN 1 ELSE 0 END + + 1 ) AS rank, + 'Payment Entry' as doctype, + name, + paid_amount, + reference_no, + reference_date, + party, + party_type, + posting_date, + {currency_field} + FROM + `tabPayment Entry` + WHERE + docstatus = 1 + AND payment_type IN (%(payment_type)s, 'Internal Transfer') + AND ifnull(clearance_date, '') = "" + AND {account_from_to} = %(bank_account)s + AND paid_amount {'= %(amount)s' if exact_match else '> 0.0'} + {filter_by_date} + {filter_by_reference_no} + order by{order_by} """ -def get_je_matching_query(amount_condition, transaction): +def get_je_matching_query( + exact_match, + transaction, + from_date, + to_date, + filter_by_reference_date, + from_reference_date, + to_reference_date, +): # 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 - cr_or_dr = "credit" if transaction.withdrawal > 0 else "debit" - + cr_or_dr = "credit" if transaction.withdrawal > 0.0 else "debit" + filter_by_date = f"AND je.posting_date between '{from_date}' and '{to_date}'" + order_by = " je.posting_date" + filter_by_reference_no = "" + if cint(filter_by_reference_date): + filter_by_date = f"AND je.cheque_date between '{from_reference_date}' and '{to_reference_date}'" + order_by = " je.cheque_date" + if frappe.flags.auto_reconcile_vouchers == True: + filter_by_reference_no = f"AND je.cheque_no = '{transaction.reference_number}'" return f""" - SELECT (CASE WHEN je.cheque_no=%(reference_no)s THEN 1 ELSE 0 END + + CASE WHEN jea.{cr_or_dr}_in_account_currency = %(amount)s THEN 1 ELSE 0 END + 1) AS rank , - 'Journal Entry' as doctype, + 'Journal Entry' AS doctype, je.name, - jea.{cr_or_dr}_in_account_currency as paid_amount, - je.cheque_no as reference_no, - je.cheque_date as reference_date, - je.pay_to_recd_from as party, + jea.{cr_or_dr}_in_account_currency AS paid_amount, + je.cheque_no AS reference_no, + je.cheque_date AS reference_date, + je.pay_to_recd_from AS party, jea.party_type, je.posting_date, - jea.account_currency as currency + jea.account_currency AS currency FROM - `tabJournal Entry Account` as jea + `tabJournal Entry Account` AS jea JOIN - `tabJournal Entry` as je + `tabJournal Entry` AS je ON jea.parent = je.name WHERE - (je.clearance_date is null or je.clearance_date='0000-00-00') + je.docstatus = 1 + AND je.voucher_type NOT IN ('Opening Entry') + AND (je.clearance_date IS NULL OR je.clearance_date='0000-00-00') AND jea.account = %(bank_account)s - AND jea.{cr_or_dr}_in_account_currency {amount_condition} %(amount)s + AND jea.{cr_or_dr}_in_account_currency {'= %(amount)s' if exact_match else '> 0.0'} AND je.docstatus = 1 + {filter_by_date} + {filter_by_reference_no} + order by {order_by} """ -def get_si_matching_query(amount_condition): - # get matchin sales invoice query +def get_si_matching_query(exact_match): + # get matching sales invoice query return f""" SELECT - ( CASE WHEN si.customer = %(party)s THEN 1 ELSE 0 END + ( CASE WHEN si.customer = %(party)s THEN 1 ELSE 0 END + + CASE WHEN sip.amount = %(amount)s THEN 1 ELSE 0 END + 1 ) AS rank, 'Sales Invoice' as doctype, si.name, @@ -589,18 +800,20 @@ def get_si_matching_query(amount_condition): `tabSales Invoice` as si ON sip.parent = si.name - WHERE (sip.clearance_date is null or sip.clearance_date='0000-00-00') + WHERE + si.docstatus = 1 + AND (sip.clearance_date is null or sip.clearance_date='0000-00-00') AND sip.account = %(bank_account)s - AND sip.amount {amount_condition} %(amount)s - AND si.docstatus = 1 + AND sip.amount {'= %(amount)s' if exact_match else '> 0.0'} """ -def get_pi_matching_query(amount_condition): - # get matching purchase invoice query +def get_pi_matching_query(exact_match): + # get matching purchase invoice query when they are also used as payment entries (is_paid) return f""" SELECT ( CASE WHEN supplier = %(party)s THEN 1 ELSE 0 END + + CASE WHEN paid_amount = %(amount)s THEN 1 ELSE 0 END + 1 ) AS rank, 'Purchase Invoice' as doctype, name, @@ -614,9 +827,9 @@ def get_pi_matching_query(amount_condition): FROM `tabPurchase Invoice` WHERE - paid_amount {amount_condition} %(amount)s - AND docstatus = 1 + docstatus = 1 AND is_paid = 1 AND ifnull(clearance_date, '') = "" - AND cash_bank_account = %(bank_account)s + AND cash_bank_account = %(bank_account)s + AND paid_amount {'= %(amount)s' if exact_match else '> 0.0'} """ 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 a964965c26..04af32346b 100644 --- a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js +++ b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js @@ -100,7 +100,7 @@ frappe.ui.form.on("Bank Statement Import", { if (frm.doc.status.includes("Success")) { frm.add_custom_button( - __("Go to {0} List", [frm.doc.reference_doctype]), + __("Go to {0} List", [__(frm.doc.reference_doctype)]), () => frappe.set_route("List", frm.doc.reference_doctype) ); } @@ -141,7 +141,7 @@ frappe.ui.form.on("Bank Statement Import", { }, show_import_status(frm) { - let import_log = JSON.parse(frm.doc.import_log || "[]"); + let import_log = JSON.parse(frm.doc.statement_import_log || "[]"); let successful_records = import_log.filter((log) => log.success); let failed_records = import_log.filter((log) => !log.success); if (successful_records.length === 0) return; @@ -309,7 +309,7 @@ frappe.ui.form.on("Bank Statement Import", { // method: 'frappe.core.doctype.data_import.data_import.get_preview_from_template', show_import_preview(frm, preview_data) { - let import_log = JSON.parse(frm.doc.import_log || "[]"); + let import_log = JSON.parse(frm.doc.statement_import_log || "[]"); if ( frm.import_preview && @@ -439,7 +439,7 @@ frappe.ui.form.on("Bank Statement Import", { }, show_import_log(frm) { - let import_log = JSON.parse(frm.doc.import_log || "[]"); + let import_log = JSON.parse(frm.doc.statement_import_log || "[]"); let logs = import_log; frm.toggle_display("import_log", false); frm.toggle_display("import_log_section", logs.length > 0); diff --git a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.json b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.json index 7ffff02850..eede3bdc6d 100644 --- a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.json +++ b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.json @@ -24,7 +24,7 @@ "section_import_preview", "import_preview", "import_log_section", - "import_log", + "statement_import_log", "show_failed_logs", "import_log_preview", "reference_doctype", @@ -90,12 +90,6 @@ "options": "JSON", "read_only": 1 }, - { - "fieldname": "import_log", - "fieldtype": "Code", - "label": "Import Log", - "options": "JSON" - }, { "fieldname": "import_log_section", "fieldtype": "Section Break", @@ -198,11 +192,17 @@ { "fieldname": "column_break_4", "fieldtype": "Column Break" + }, + { + "fieldname": "statement_import_log", + "fieldtype": "Code", + "label": "Statement Import Log", + "options": "JSON" } ], "hide_toolbar": 1, "links": [], - "modified": "2021-05-12 14:17:37.777246", + "modified": "2022-09-07 11:11:40.293317", "modified_by": "Administrator", "module": "Accounts", "name": "Bank Statement Import", diff --git a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py index 3f5c064f4b..d8880f7041 100644 --- a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py +++ b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py @@ -53,15 +53,13 @@ class BankStatementImport(DataImport): if "Bank Account" not in json.dumps(preview["columns"]): frappe.throw(_("Please add the Bank Account column")) - from frappe.core.page.background_jobs.background_jobs import get_info + from frappe.utils.background_jobs import is_job_queued from frappe.utils.scheduler import is_scheduler_inactive if is_scheduler_inactive() and not frappe.flags.in_test: frappe.throw(_("Scheduler is inactive. Cannot import data."), title=_("Scheduler Inactive")) - enqueued_jobs = [d.get("job_name") for d in get_info()] - - if self.name not in enqueued_jobs: + if not is_job_queued(self.name): enqueue( start_import, queue="default", diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.js b/erpnext/accounts/doctype/bank_transaction/bank_transaction.js index 6f2900a680..e548b4c7e9 100644 --- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.js +++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.js @@ -12,8 +12,13 @@ frappe.ui.form.on("Bank Transaction", { }; }); }, - - bank_account: function(frm) { + refresh(frm) { + frm.add_custom_button(__('Unreconcile Transaction'), () => { + frm.call('remove_payment_entries') + .then( () => frm.refresh() ); + }); + }, + bank_account: function (frm) { set_bank_statement_filter(frm); }, @@ -34,6 +39,7 @@ frappe.ui.form.on("Bank Transaction", { "Journal Entry", "Sales Invoice", "Purchase Invoice", + "Bank Transaction", ]; } }); @@ -49,7 +55,7 @@ const update_clearance_date = (frm, cdt, cdn) => { frappe .xcall( "erpnext.accounts.doctype.bank_transaction.bank_transaction.unclear_reference_payment", - { doctype: cdt, docname: cdn } + { doctype: cdt, docname: cdn, bt_name: frm.doc.name } ) .then((e) => { if (e == "success") { diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.json b/erpnext/accounts/doctype/bank_transaction/bank_transaction.json index 2bdaa1049b..768d2f0fa4 100644 --- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.json +++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.json @@ -20,9 +20,11 @@ "currency", "section_break_10", "description", - "section_break_14", "reference_number", + "column_break_10", "transaction_id", + "transaction_type", + "section_break_14", "payment_entries", "section_break_18", "allocated_amount", @@ -190,11 +192,21 @@ "label": "Withdrawal", "oldfieldname": "credit", "options": "currency" + }, + { + "fieldname": "column_break_10", + "fieldtype": "Column Break" + }, + { + "fieldname": "transaction_type", + "fieldtype": "Data", + "label": "Transaction Type", + "length": 50 } ], "is_submittable": 1, "links": [], - "modified": "2022-03-21 19:05:04.208222", + "modified": "2022-05-29 18:36:50.475964", "modified_by": "Administrator", "module": "Accounts", "name": "Bank Transaction", @@ -248,4 +260,4 @@ "states": [], "title_field": "bank_account", "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py index a788514335..15162376c1 100644 --- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py +++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py @@ -1,9 +1,6 @@ # Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt - -from functools import reduce - import frappe from frappe.utils import flt @@ -18,72 +15,137 @@ class BankTransaction(StatusUpdater): self.clear_linked_payment_entries() self.set_status() + _saving_flag = False + + # nosemgrep: frappe-semgrep-rules.rules.frappe-modifying-but-not-comitting def on_update_after_submit(self): - self.update_allocations() - self.clear_linked_payment_entries() - self.set_status(update=True) + "Run on save(). Avoid recursion caused by multiple saves" + if not self._saving_flag: + self._saving_flag = True + self.clear_linked_payment_entries() + self.update_allocations() + self._saving_flag = False def on_cancel(self): self.clear_linked_payment_entries(for_cancel=True) self.set_status(update=True) def update_allocations(self): + "The doctype does not allow modifications after submission, so write to the db direct" if self.payment_entries: - allocated_amount = reduce( - lambda x, y: flt(x) + flt(y), [x.allocated_amount for x in self.payment_entries] - ) + allocated_amount = sum(p.allocated_amount for p in self.payment_entries) else: - allocated_amount = 0 + allocated_amount = 0.0 - if allocated_amount: - frappe.db.set_value(self.doctype, self.name, "allocated_amount", flt(allocated_amount)) - frappe.db.set_value( - self.doctype, - self.name, - "unallocated_amount", - abs(flt(self.withdrawal) - flt(self.deposit)) - flt(allocated_amount), - ) + amount = abs(flt(self.withdrawal) - flt(self.deposit)) + self.db_set("allocated_amount", flt(allocated_amount)) + self.db_set("unallocated_amount", amount - flt(allocated_amount)) + self.reload() + self.set_status(update=True) - else: - frappe.db.set_value(self.doctype, self.name, "allocated_amount", 0) - frappe.db.set_value( - self.doctype, self.name, "unallocated_amount", abs(flt(self.withdrawal) - flt(self.deposit)) - ) + def add_payment_entries(self, vouchers): + "Add the vouchers with zero allocation. Save() will perform the allocations and clearance" + if 0.0 >= self.unallocated_amount: + frappe.throw(frappe._(f"Bank Transaction {self.name} is already fully reconciled")) - amount = self.deposit or self.withdrawal - if amount == self.allocated_amount: - frappe.db.set_value(self.doctype, self.name, "status", "Reconciled") + added = False + for voucher in vouchers: + # Can't add same voucher twice + found = False + for pe in self.payment_entries: + if ( + pe.payment_document == voucher["payment_doctype"] + and pe.payment_entry == voucher["payment_name"] + ): + found = True + + if not found: + pe = { + "payment_document": voucher["payment_doctype"], + "payment_entry": voucher["payment_name"], + "allocated_amount": 0.0, # Temporary + } + child = self.append("payment_entries", pe) + added = True + + # runs on_update_after_submit + if added: + self.save() + + def allocate_payment_entries(self): + """Refactored from bank reconciliation tool. + Non-zero allocations must be amended/cleared manually + Get the bank transaction amount (b) and remove as we allocate + For each payment_entry if allocated_amount == 0: + - get the amount already allocated against all transactions (t), need latest date + - get the voucher amount (from gl) (v) + - allocate (a = v - t) + - a = 0: should already be cleared, so clear & remove payment_entry + - 0 < a <= u: allocate a & clear + - 0 < a, a > u: allocate u + - 0 > a: Error: already over-allocated + - clear means: set the latest transaction date as clearance date + """ + gl_bank_account = frappe.db.get_value("Bank Account", self.bank_account, "account") + remaining_amount = self.unallocated_amount + for payment_entry in self.payment_entries: + if payment_entry.allocated_amount == 0.0: + unallocated_amount, should_clear, latest_transaction = get_clearance_details( + self, payment_entry + ) + + if 0.0 == unallocated_amount: + if should_clear: + latest_transaction.clear_linked_payment_entry(payment_entry) + self.db_delete_payment_entry(payment_entry) + + elif remaining_amount <= 0.0: + self.db_delete_payment_entry(payment_entry) + + elif 0.0 < unallocated_amount and unallocated_amount <= remaining_amount: + payment_entry.db_set("allocated_amount", unallocated_amount) + remaining_amount -= unallocated_amount + if should_clear: + latest_transaction.clear_linked_payment_entry(payment_entry) + + elif 0.0 < unallocated_amount and unallocated_amount > remaining_amount: + payment_entry.db_set("allocated_amount", remaining_amount) + remaining_amount = 0.0 + + elif 0.0 > unallocated_amount: + self.db_delete_payment_entry(payment_entry) + frappe.throw( + frappe._(f"Voucher {payment_entry.payment_entry} is over-allocated by {unallocated_amount}") + ) self.reload() - def clear_linked_payment_entries(self, for_cancel=False): + def db_delete_payment_entry(self, payment_entry): + frappe.db.delete("Bank Transaction Payments", {"name": payment_entry.name}) + + @frappe.whitelist() + def remove_payment_entries(self): for payment_entry in self.payment_entries: - if payment_entry.payment_document == "Sales Invoice": - self.clear_sales_invoice(payment_entry, for_cancel=for_cancel) - elif payment_entry.payment_document in get_doctypes_for_bank_reconciliation(): - self.clear_simple_entry(payment_entry, for_cancel=for_cancel) + self.remove_payment_entry(payment_entry) + # runs on_update_after_submit + self.save() - def clear_simple_entry(self, payment_entry, for_cancel=False): - if payment_entry.payment_document == "Payment Entry": - if ( - frappe.db.get_value("Payment Entry", payment_entry.payment_entry, "payment_type") - == "Internal Transfer" - ): - if len(get_reconciled_bank_transactions(payment_entry)) < 2: - return + def remove_payment_entry(self, payment_entry): + "Clear payment entry and clearance" + self.clear_linked_payment_entry(payment_entry, for_cancel=True) + self.remove(payment_entry) - clearance_date = self.date if not for_cancel else None - frappe.db.set_value( - payment_entry.payment_document, payment_entry.payment_entry, "clearance_date", clearance_date - ) + def clear_linked_payment_entries(self, for_cancel=False): + if for_cancel: + for payment_entry in self.payment_entries: + self.clear_linked_payment_entry(payment_entry, for_cancel) + else: + self.allocate_payment_entries() - def clear_sales_invoice(self, payment_entry, for_cancel=False): - clearance_date = self.date if not for_cancel else None - frappe.db.set_value( - "Sales Invoice Payment", - dict(parenttype=payment_entry.payment_document, parent=payment_entry.payment_entry), - "clearance_date", - clearance_date, + def clear_linked_payment_entry(self, payment_entry, for_cancel=False): + clearance_date = None if for_cancel else self.date + set_voucher_clearance( + payment_entry.payment_document, payment_entry.payment_entry, clearance_date, self ) @@ -93,38 +155,112 @@ def get_doctypes_for_bank_reconciliation(): return frappe.get_hooks("bank_reconciliation_doctypes") -def get_reconciled_bank_transactions(payment_entry): - reconciled_bank_transactions = frappe.get_all( - "Bank Transaction Payments", - filters={"payment_entry": payment_entry.payment_entry}, - fields=["parent"], +def get_clearance_details(transaction, payment_entry): + """ + There should only be one bank gle for a voucher. + Could be none for a Bank Transaction. + But if a JE, could affect two banks. + Should only clear the voucher if all bank gles are allocated. + """ + gl_bank_account = frappe.db.get_value("Bank Account", transaction.bank_account, "account") + gles = get_related_bank_gl_entries(payment_entry.payment_document, payment_entry.payment_entry) + bt_allocations = get_total_allocated_amount( + payment_entry.payment_document, payment_entry.payment_entry ) - return reconciled_bank_transactions + unallocated_amount = min( + transaction.unallocated_amount, + get_paid_amount(payment_entry, transaction.currency, gl_bank_account), + ) + unmatched_gles = len(gles) + latest_transaction = transaction + for gle in gles: + if gle["gl_account"] == gl_bank_account: + if gle["amount"] <= 0.0: + frappe.throw( + frappe._(f"Voucher {payment_entry.payment_entry} value is broken: {gle['amount']}") + ) + + unmatched_gles -= 1 + unallocated_amount = gle["amount"] + for a in bt_allocations: + if a["gl_account"] == gle["gl_account"]: + unallocated_amount = gle["amount"] - a["total"] + if frappe.utils.getdate(transaction.date) < a["latest_date"]: + latest_transaction = frappe.get_doc("Bank Transaction", a["latest_name"]) + else: + # Must be a Journal Entry affecting more than one bank + for a in bt_allocations: + if a["gl_account"] == gle["gl_account"] and a["total"] == gle["amount"]: + unmatched_gles -= 1 + + return unallocated_amount, unmatched_gles == 0, latest_transaction -def get_total_allocated_amount(payment_entry): - return frappe.db.sql( +def get_related_bank_gl_entries(doctype, docname): + # nosemgrep: frappe-semgrep-rules.rules.frappe-using-db-sql + result = frappe.db.sql( """ SELECT - SUM(btp.allocated_amount) as allocated_amount, - bt.name + ABS(gle.credit_in_account_currency - gle.debit_in_account_currency) AS amount, + gle.account AS gl_account FROM - `tabBank Transaction Payments` as btp + `tabGL Entry` gle LEFT JOIN - `tabBank Transaction` bt ON bt.name=btp.parent + `tabAccount` ac ON ac.name=gle.account WHERE - btp.payment_document = %s - AND - btp.payment_entry = %s - AND - bt.docstatus = 1""", - (payment_entry.payment_document, payment_entry.payment_entry), + ac.account_type = 'Bank' + AND gle.voucher_type = %(doctype)s + AND gle.voucher_no = %(docname)s + AND is_cancelled = 0 + """, + dict(doctype=doctype, docname=docname), as_dict=True, ) + return result -def get_paid_amount(payment_entry, currency, bank_account): +def get_total_allocated_amount(doctype, docname): + """ + Gets the sum of allocations for a voucher on each bank GL account + along with the latest bank transaction name & date + NOTE: query may also include just saved vouchers/payments but with zero allocated_amount + """ + # nosemgrep: frappe-semgrep-rules.rules.frappe-using-db-sql + result = frappe.db.sql( + """ + SELECT total, latest_name, latest_date, gl_account FROM ( + SELECT + ROW_NUMBER() OVER w AS rownum, + SUM(btp.allocated_amount) OVER(PARTITION BY ba.account) AS total, + FIRST_VALUE(bt.name) OVER w AS latest_name, + FIRST_VALUE(bt.date) OVER w AS latest_date, + ba.account AS gl_account + FROM + `tabBank Transaction Payments` btp + LEFT JOIN `tabBank Transaction` bt ON bt.name=btp.parent + LEFT JOIN `tabBank Account` ba ON ba.name=bt.bank_account + WHERE + btp.payment_document = %(doctype)s + AND btp.payment_entry = %(docname)s + AND bt.docstatus = 1 + WINDOW w AS (PARTITION BY ba.account ORDER BY bt.date desc) + ) temp + WHERE + rownum = 1 + """, + dict(doctype=doctype, docname=docname), + as_dict=True, + ) + for row in result: + # Why is this *sometimes* a byte string? + if isinstance(row["latest_name"], bytes): + row["latest_name"] = row["latest_name"].decode() + row["latest_date"] = frappe.utils.getdate(row["latest_date"]) + return result + + +def get_paid_amount(payment_entry, currency, gl_bank_account): if payment_entry.payment_document in ["Payment Entry", "Sales Invoice", "Purchase Invoice"]: paid_amount_field = "paid_amount" @@ -137,7 +273,7 @@ def get_paid_amount(payment_entry, currency, bank_account): ) elif doc.payment_type == "Pay": paid_amount_field = ( - "paid_amount" if doc.paid_to_account_currency == currency else "base_paid_amount" + "paid_amount" if doc.paid_from_account_currency == currency else "base_paid_amount" ) return frappe.db.get_value( @@ -147,7 +283,7 @@ def get_paid_amount(payment_entry, currency, bank_account): elif payment_entry.payment_document == "Journal Entry": return frappe.db.get_value( "Journal Entry Account", - {"parent": payment_entry.payment_entry, "account": bank_account}, + {"parent": payment_entry.payment_entry, "account": gl_bank_account}, "sum(credit_in_account_currency)", ) @@ -166,6 +302,12 @@ def get_paid_amount(payment_entry, currency, bank_account): payment_entry.payment_document, payment_entry.payment_entry, "amount_paid" ) + elif payment_entry.payment_document == "Bank Transaction": + dep, wth = frappe.db.get_value( + "Bank Transaction", payment_entry.payment_entry, ("deposit", "withdrawal") + ) + return abs(flt(wth) - flt(dep)) + else: frappe.throw( "Please reconcile {0}: {1} manually".format( @@ -174,18 +316,55 @@ def get_paid_amount(payment_entry, currency, bank_account): ) -@frappe.whitelist() -def unclear_reference_payment(doctype, docname): - if frappe.db.exists(doctype, docname): - doc = frappe.get_doc(doctype, docname) - if doctype == "Sales Invoice": - frappe.db.set_value( - "Sales Invoice Payment", - dict(parenttype=doc.payment_document, parent=doc.payment_entry), - "clearance_date", - None, - ) - else: - frappe.db.set_value(doc.payment_document, doc.payment_entry, "clearance_date", None) +def set_voucher_clearance(doctype, docname, clearance_date, self): + if doctype in [ + "Payment Entry", + "Journal Entry", + "Purchase Invoice", + "Expense Claim", + "Loan Repayment", + "Loan Disbursement", + ]: + if ( + doctype == "Payment Entry" + and frappe.db.get_value("Payment Entry", docname, "payment_type") == "Internal Transfer" + and len(get_reconciled_bank_transactions(doctype, docname)) < 2 + ): + return + frappe.db.set_value(doctype, docname, "clearance_date", clearance_date) - return doc.payment_entry + elif doctype == "Sales Invoice": + frappe.db.set_value( + "Sales Invoice Payment", + dict(parenttype=doctype, parent=docname), + "clearance_date", + clearance_date, + ) + + elif doctype == "Bank Transaction": + # For when a second bank transaction has fixed another, e.g. refund + bt = frappe.get_doc(doctype, docname) + if clearance_date: + vouchers = [{"payment_doctype": "Bank Transaction", "payment_name": self.name}] + bt.add_payment_entries(vouchers) + else: + for pe in bt.payment_entries: + if pe.payment_document == self.doctype and pe.payment_entry == self.name: + bt.remove(pe) + bt.save() + break + + +def get_reconciled_bank_transactions(doctype, docname): + return frappe.get_all( + "Bank Transaction Payments", + filters={"payment_document": doctype, "payment_entry": docname}, + pluck="parent", + ) + + +@frappe.whitelist() +def unclear_reference_payment(doctype, docname, bt_name): + bt = frappe.get_doc("Bank Transaction", bt_name) + set_voucher_clearance(doctype, docname, None, bt) + return docname diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction_upload.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction_upload.py index 372c53d499..efb9d8c5ba 100644 --- a/erpnext/accounts/doctype/bank_transaction/bank_transaction_upload.py +++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction_upload.py @@ -74,7 +74,7 @@ def get_header_mapping(columns, bank_account): def get_bank_mapping(bank_account): - bank_name = frappe.db.get_value("Bank Account", bank_account, "bank") + bank_name = frappe.get_cached_value("Bank Account", bank_account, "bank") bank = frappe.get_doc("Bank", bank_name) mapping = {row.file_field: row.bank_transaction_field for row in bank.bank_transaction_mapping} diff --git a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py index a5d0413799..f900e0775c 100644 --- a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py +++ b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py @@ -5,6 +5,7 @@ import json import unittest import frappe +from frappe import utils from frappe.tests.utils import FrappeTestCase from erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool import ( @@ -40,7 +41,12 @@ class TestBankTransaction(FrappeTestCase): "Bank Transaction", dict(description="Re 95282925234 FE/000002917 AT171513000281183046 Conrad Electronic"), ) - linked_payments = get_linked_payments(bank_transaction.name, ["payment_entry", "exact_match"]) + linked_payments = get_linked_payments( + bank_transaction.name, + ["payment_entry", "exact_match"], + from_date=bank_transaction.date, + to_date=utils.today(), + ) self.assertTrue(linked_payments[0][6] == "Conrad Electronic") # This test validates a simple reconciliation leading to the clearance of the bank transaction and the payment @@ -81,7 +87,12 @@ class TestBankTransaction(FrappeTestCase): "Bank Transaction", dict(description="Auszahlung Karte MC/000002916 AUTOMAT 698769 K002 27.10. 14:07"), ) - linked_payments = get_linked_payments(bank_transaction.name, ["payment_entry", "exact_match"]) + linked_payments = get_linked_payments( + bank_transaction.name, + ["payment_entry", "exact_match"], + from_date=bank_transaction.date, + to_date=utils.today(), + ) self.assertTrue(linked_payments[0][3]) # Check error if already reconciled diff --git a/erpnext/accounts/doctype/budget/budget.json b/erpnext/accounts/doctype/budget/budget.json index fc4dd200ea..f0566f4436 100644 --- a/erpnext/accounts/doctype/budget/budget.json +++ b/erpnext/accounts/doctype/budget/budget.json @@ -1,6 +1,7 @@ { "actions": [], "allow_import": 1, + "autoname": "naming_series:", "creation": "2016-05-16 11:42:29.632528", "doctype": "DocType", "editable_grid": 1, @@ -9,6 +10,7 @@ "budget_against", "company", "cost_center", + "naming_series", "project", "fiscal_year", "column_break_3", @@ -190,15 +192,26 @@ "label": "Budget Accounts", "options": "Budget Account", "reqd": 1 + }, + { + "fieldname": "naming_series", + "fieldtype": "Data", + "hidden": 1, + "label": "Series", + "no_copy": 1, + "print_hide": 1, + "read_only": 1, + "set_only_once": 1 } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-10-06 15:13:54.055854", + "modified": "2022-10-10 22:14:36.361509", "modified_by": "Administrator", "module": "Accounts", "name": "Budget", + "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ { @@ -220,5 +233,6 @@ ], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/accounts/doctype/budget/budget.py b/erpnext/accounts/doctype/budget/budget.py index 5527f9fb99..4c628a4d95 100644 --- a/erpnext/accounts/doctype/budget/budget.py +++ b/erpnext/accounts/doctype/budget/budget.py @@ -5,7 +5,6 @@ import frappe from frappe import _ from frappe.model.document import Document -from frappe.model.naming import make_autoname from frappe.utils import add_months, flt, fmt_money, get_last_day, getdate from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( @@ -23,11 +22,6 @@ class DuplicateBudgetError(frappe.ValidationError): class Budget(Document): - def autoname(self): - self.name = make_autoname( - self.get(frappe.scrub(self.budget_against)) + "/" + self.fiscal_year + "/.###" - ) - def validate(self): if not self.get(frappe.scrub(self.budget_against)): frappe.throw(_("{0} is mandatory").format(self.budget_against)) @@ -65,7 +59,7 @@ class Budget(Document): account_list = [] for d in self.get("accounts"): if d.account: - account_details = frappe.db.get_value( + account_details = frappe.get_cached_value( "Account", d.account, ["is_group", "company", "report_type"], as_dict=1 ) @@ -109,8 +103,11 @@ class Budget(Document): ): self.applicable_on_booking_actual_expenses = 1 + def before_naming(self): + self.naming_series = f"{{{frappe.scrub(self.budget_against)}}}./.{self.fiscal_year}/.###" -def validate_expense_against_budget(args): + +def validate_expense_against_budget(args, expense_amount=0): args = frappe._dict(args) if args.get("company") and not args.fiscal_year: @@ -178,15 +175,20 @@ def validate_expense_against_budget(args): ) # nosec if budget_records: - validate_budget_records(args, budget_records) + validate_budget_records(args, budget_records, expense_amount) -def validate_budget_records(args, budget_records): +def validate_budget_records(args, budget_records, expense_amount): for budget in budget_records: if flt(budget.budget_amount): - amount = get_amount(args, budget) + amount = expense_amount or get_amount(args, budget) yearly_action, monthly_action = get_actions(args, budget) + if yearly_action in ("Stop", "Warn"): + compare_expense_with_budget( + args, flt(budget.budget_amount), _("Annual"), yearly_action, budget.budget_against, amount + ) + if monthly_action in ["Stop", "Warn"]: budget_amount = get_accumulated_monthly_budget( budget.monthly_distribution, args.posting_date, args.fiscal_year, budget.budget_amount @@ -198,28 +200,28 @@ def validate_budget_records(args, budget_records): args, budget_amount, _("Accumulated Monthly"), monthly_action, budget.budget_against, amount ) - if ( - yearly_action in ("Stop", "Warn") - and monthly_action != "Stop" - and yearly_action != monthly_action - ): - compare_expense_with_budget( - args, flt(budget.budget_amount), _("Annual"), yearly_action, budget.budget_against, amount - ) - def compare_expense_with_budget(args, budget_amount, action_for, action, budget_against, amount=0): - actual_expense = amount or get_actual_expense(args) - if actual_expense > budget_amount: - diff = actual_expense - budget_amount + actual_expense = get_actual_expense(args) + total_expense = actual_expense + amount + + if total_expense > budget_amount: + if actual_expense > budget_amount: + error_tense = _("is already") + diff = actual_expense - budget_amount + else: + error_tense = _("will be") + diff = total_expense - budget_amount + currency = frappe.get_cached_value("Company", args.company, "default_currency") - msg = _("{0} Budget for Account {1} against {2} {3} is {4}. It will exceed by {5}").format( + msg = _("{0} Budget for Account {1} against {2} {3} is {4}. It {5} exceed by {6}").format( _(action_for), frappe.bold(args.account), - args.budget_against_field, + frappe.unscrub(args.budget_against_field), frappe.bold(budget_against), frappe.bold(fmt_money(budget_amount, currency=currency)), + error_tense, frappe.bold(fmt_money(diff, currency=currency)), ) @@ -230,9 +232,9 @@ def compare_expense_with_budget(args, budget_amount, action_for, action, budget_ action = "Warn" if action == "Stop": - frappe.throw(msg, BudgetError) + frappe.throw(msg, BudgetError, title=_("Budget Exceeded")) else: - frappe.msgprint(msg, indicator="orange") + frappe.msgprint(msg, indicator="orange", title=_("Budget Exceeded")) def get_actions(args, budget): @@ -309,7 +311,7 @@ def get_other_condition(args, budget, for_doc): if args.get("fiscal_year"): date_field = "schedule_date" if for_doc == "Material Request" else "transaction_date" - start_date, end_date = frappe.db.get_value( + start_date, end_date = frappe.get_cached_value( "Fiscal Year", args.get("fiscal_year"), ["year_start_date", "year_end_date"] ) @@ -354,7 +356,9 @@ def get_actual_expense(args): """ select sum(gle.debit) - sum(gle.credit) from `tabGL Entry` gle - where gle.account=%(account)s + where + is_cancelled = 0 + and gle.account=%(account)s {condition1} and gle.fiscal_year=%(fiscal_year)s and gle.company=%(company)s @@ -382,7 +386,7 @@ def get_accumulated_monthly_budget(monthly_distribution, posting_date, fiscal_ye ): distribution.setdefault(d.month, d.percentage_allocation) - dt = frappe.db.get_value("Fiscal Year", fiscal_year, "year_start_date") + dt = frappe.get_cached_value("Fiscal Year", fiscal_year, "year_start_date") accumulated_percentage = 0.0 while dt <= getdate(posting_date): diff --git a/erpnext/accounts/doctype/budget/test_budget.py b/erpnext/accounts/doctype/budget/test_budget.py index c48c7d97a2..11af9a29f6 100644 --- a/erpnext/accounts/doctype/budget/test_budget.py +++ b/erpnext/accounts/doctype/budget/test_budget.py @@ -334,6 +334,39 @@ class TestBudget(unittest.TestCase): budget.cancel() jv.cancel() + def test_monthly_budget_against_main_cost_center(self): + from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center + from erpnext.accounts.doctype.cost_center_allocation.test_cost_center_allocation import ( + create_cost_center_allocation, + ) + + cost_centers = [ + "Main Budget Cost Center 1", + "Sub Budget Cost Center 1", + "Sub Budget Cost Center 2", + ] + + for cc in cost_centers: + create_cost_center(cost_center_name=cc, company="_Test Company") + + create_cost_center_allocation( + "_Test Company", + "Main Budget Cost Center 1 - _TC", + {"Sub Budget Cost Center 1 - _TC": 60, "Sub Budget Cost Center 2 - _TC": 40}, + ) + + make_budget(budget_against="Cost Center", cost_center="Main Budget Cost Center 1 - _TC") + + jv = make_journal_entry( + "_Test Account Cost for Goods Sold - _TC", + "_Test Bank - _TC", + 400000, + "Main Budget Cost Center 1 - _TC", + posting_date=nowdate(), + ) + + self.assertRaises(BudgetError, jv.submit) + def set_total_expense_zero(posting_date, budget_against_field=None, budget_against_CC=None): if budget_against_field == "project": diff --git a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py index 01bf1c23e9..220b74727b 100644 --- a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py +++ b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py @@ -45,14 +45,14 @@ def validate_columns(data): @frappe.whitelist() def validate_company(company): - parent_company, allow_account_creation_against_child_company = frappe.db.get_value( - "Company", {"name": company}, ["parent_company", "allow_account_creation_against_child_company"] + parent_company, allow_account_creation_against_child_company = frappe.get_cached_value( + "Company", company, ["parent_company", "allow_account_creation_against_child_company"] ) if parent_company and (not allow_account_creation_against_child_company): msg = _("{} is a child company.").format(frappe.bold(company)) + " " msg += _("Please import accounts against parent company or enable {} in company master.").format( - frappe.bold("Allow Account Creation Against Child Company") + frappe.bold(_("Allow Account Creation Against Child Company")) ) frappe.throw(msg, title=_("Wrong Company")) @@ -485,6 +485,10 @@ def set_default_accounts(company): "default_payable_account": frappe.db.get_value( "Account", {"company": company.name, "account_type": "Payable", "is_group": 0} ), + "default_provisional_account": frappe.db.get_value( + "Account", + {"company": company.name, "account_type": "Service Received But Not Billed", "is_group": 0}, + ), } ) diff --git a/erpnext/accounts/doctype/cost_center/cost_center.py b/erpnext/accounts/doctype/cost_center/cost_center.py index 31055c3fb4..e8b34bbf03 100644 --- a/erpnext/accounts/doctype/cost_center/cost_center.py +++ b/erpnext/accounts/doctype/cost_center/cost_center.py @@ -16,7 +16,7 @@ class CostCenter(NestedSet): from erpnext.accounts.utils import get_autoname_with_number self.name = get_autoname_with_number( - self.cost_center_number, self.cost_center_name, None, self.company + self.cost_center_number, self.cost_center_name, self.company ) def validate(self): diff --git a/erpnext/accounts/doctype/cost_center_allocation/cost_center_allocation.py b/erpnext/accounts/doctype/cost_center_allocation/cost_center_allocation.py index d25016fe59..54ffe21a15 100644 --- a/erpnext/accounts/doctype/cost_center_allocation/cost_center_allocation.py +++ b/erpnext/accounts/doctype/cost_center_allocation/cost_center_allocation.py @@ -28,9 +28,14 @@ class InvalidDateError(frappe.ValidationError): class CostCenterAllocation(Document): + def __init__(self, *args, **kwargs): + super(CostCenterAllocation, self).__init__(*args, **kwargs) + self._skip_from_date_validation = False + def validate(self): self.validate_total_allocation_percentage() - self.validate_from_date_based_on_existing_gle() + if not self._skip_from_date_validation: + self.validate_from_date_based_on_existing_gle() self.validate_backdated_allocation() self.validate_main_cost_center() self.validate_child_cost_centers() diff --git a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json index 7921fcc2b9..c62b711f2c 100644 --- a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json +++ b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json @@ -6,6 +6,7 @@ "engine": "InnoDB", "field_order": [ "api_details_section", + "disabled", "service_provider", "api_endpoint", "url", @@ -77,12 +78,18 @@ "label": "Service Provider", "options": "frankfurter.app\nexchangerate.host\nCustom", "reqd": 1 + }, + { + "default": "0", + "fieldname": "disabled", + "fieldtype": "Check", + "label": "Disabled" } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2022-01-10 15:51:14.521174", + "modified": "2023-01-09 12:19:03.955906", "modified_by": "Administrator", "module": "Accounts", "name": "Currency Exchange Settings", diff --git a/erpnext/accounts/doctype/dunning/dunning.py b/erpnext/accounts/doctype/dunning/dunning.py index 9874d66fa5..b4df0a5270 100644 --- a/erpnext/accounts/doctype/dunning/dunning.py +++ b/erpnext/accounts/doctype/dunning/dunning.py @@ -19,7 +19,7 @@ class Dunning(AccountsController): self.validate_overdue_days() self.validate_amount() if not self.income_account: - self.income_account = frappe.db.get_value("Company", self.company, "default_income_account") + self.income_account = frappe.get_cached_value("Company", self.company, "default_income_account") def validate_overdue_days(self): self.overdue_days = (getdate(self.posting_date) - getdate(self.due_date)).days or 0 @@ -40,7 +40,7 @@ class Dunning(AccountsController): def on_cancel(self): if self.dunning_amount: - self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry") + self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry") make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) def make_gl_entries(self): diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.js b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.js index 926a442f80..f72ecc9e50 100644 --- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.js +++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.js @@ -26,7 +26,7 @@ frappe.ui.form.on('Exchange Rate Revaluation', { doc: frm.doc, callback: function(r) { if (r.message) { - frm.add_custom_button(__('Journal Entry'), function() { + frm.add_custom_button(__('Journal Entries'), function() { return frm.events.make_jv(frm); }, __('Create')); } @@ -35,10 +35,11 @@ frappe.ui.form.on('Exchange Rate Revaluation', { } }, - get_entries: function(frm) { + get_entries: function(frm, account) { frappe.call({ method: "get_accounts_data", doc: cur_frm.doc, + account: account, callback: function(r){ frappe.model.clear_table(frm.doc, "accounts"); if(r.message) { @@ -57,7 +58,6 @@ frappe.ui.form.on('Exchange Rate Revaluation', { let total_gain_loss = 0; frm.doc.accounts.forEach((d) => { - d.gain_loss = flt(d.new_balance_in_base_currency, precision("new_balance_in_base_currency", d)) - flt(d.balance_in_base_currency, precision("balance_in_base_currency", d)); total_gain_loss += flt(d.gain_loss, precision("gain_loss", d)); }); @@ -66,13 +66,19 @@ frappe.ui.form.on('Exchange Rate Revaluation', { }, make_jv : function(frm) { + let revaluation_journal = null; + let zero_balance_journal = null; frappe.call({ - method: "make_jv_entry", + method: "make_jv_entries", doc: frm.doc, + freeze: true, + freeze_message: "Making Journal Entries...", callback: function(r){ if (r.message) { - var doc = frappe.model.sync(r.message)[0]; - frappe.set_route("Form", doc.doctype, doc.name); + let response = r.message; + if(response['revaluation_jv'] || response['zero_balance_jv']) { + frappe.msgprint(__("Journals have been created")); + } } } }); diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.json b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.json index a7315a672a..0d198ca120 100644 --- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.json +++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.json @@ -1,389 +1,160 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 1, - "allow_rename": 0, - "autoname": "ACC-ERR-.YYYY.-.#####", - "beta": 0, - "creation": "2018-04-13 18:25:55.943587", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "allow_import": 1, + "autoname": "ACC-ERR-.YYYY.-.#####", + "creation": "2018-04-13 18:25:55.943587", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "posting_date", + "column_break_2", + "company", + "section_break_4", + "get_entries", + "accounts", + "section_break_6", + "gain_loss_unbooked", + "gain_loss_booked", + "column_break_10", + "total_gain_loss", + "amended_from" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "Today", - "fieldname": "posting_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": "Posting 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 - }, + "default": "Today", + "fieldname": "posting_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Posting Date", + "reqd": 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 - }, + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "company", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Company", - "length": 0, - "no_copy": 0, - "options": "Company", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "company", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Company", + "options": "Company", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "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 - }, + "fieldname": "section_break_4", + "fieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "get_entries", - "fieldtype": "Button", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Get Entries", - "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": "get_entries", + "fieldtype": "Button", + "label": "Get Entries" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "accounts", - "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Exchange Rate Revaluation Account", - "length": 0, - "no_copy": 1, - "options": "Exchange Rate Revaluation Account", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "accounts", + "fieldtype": "Table", + "label": "Exchange Rate Revaluation Account", + "no_copy": 1, + "options": "Exchange Rate Revaluation Account", + "reqd": 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 - }, + "fieldname": "section_break_6", + "fieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "total_gain_loss", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Total Gain/Loss", - "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 - }, + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Exchange Rate Revaluation", + "print_hide": 1, + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "amended_from", - "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": "Amended From", - "length": 0, - "no_copy": 1, - "options": "Exchange Rate Revaluation", - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldname": "gain_loss_unbooked", + "fieldtype": "Currency", + "label": "Gain/Loss from Revaluation", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "description": "Gain/Loss accumulated in foreign currency account. Accounts with '0' balance in either Base or Account currency", + "fieldname": "gain_loss_booked", + "fieldtype": "Currency", + "label": "Gain/Loss already booked", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "total_gain_loss", + "fieldtype": "Currency", + "label": "Total Gain/Loss", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "column_break_10", + "fieldtype": "Column Break" } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 1, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-08-21 16:15:34.660715", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Exchange Rate Revaluation", - "name_case": "", - "owner": "Administrator", + ], + "is_submittable": 1, + "links": [], + "modified": "2022-12-29 19:38:24.416529", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Exchange Rate Revaluation", + "naming_rule": "Expression (old style)", + "owner": "Administrator", "permissions": [ { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 1, + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "submit": 1, "write": 1 - }, + }, { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Accounts Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 1, + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts Manager", + "share": 1, + "submit": 1, "write": 1 - }, + }, { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Accounts User", - "set_user_permissions": 0, - "share": 1, - "submit": 1, + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts User", + "share": 1, + "submit": 1, "write": 1 } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py index 2f81c5fb75..d67d59b5d4 100644 --- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py +++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py @@ -3,10 +3,12 @@ import frappe -from frappe import _ +from frappe import _, qb from frappe.model.document import Document from frappe.model.meta import get_field_precision -from frappe.utils import flt +from frappe.query_builder import Criterion, Order +from frappe.query_builder.functions import NullIf, Sum +from frappe.utils import flt, get_link_to_form import erpnext from erpnext.accounts.doctype.journal_entry.journal_entry import get_balance_on @@ -19,11 +21,25 @@ class ExchangeRateRevaluation(Document): def set_total_gain_loss(self): total_gain_loss = 0 + + gain_loss_booked = 0 + gain_loss_unbooked = 0 + for d in self.accounts: - d.gain_loss = flt( - d.new_balance_in_base_currency, d.precision("new_balance_in_base_currency") - ) - flt(d.balance_in_base_currency, d.precision("balance_in_base_currency")) + if not d.zero_balance: + d.gain_loss = flt( + d.new_balance_in_base_currency, d.precision("new_balance_in_base_currency") + ) - flt(d.balance_in_base_currency, d.precision("balance_in_base_currency")) + + if d.zero_balance: + gain_loss_booked += flt(d.gain_loss, d.precision("gain_loss")) + else: + gain_loss_unbooked += flt(d.gain_loss, d.precision("gain_loss")) + total_gain_loss += flt(d.gain_loss, d.precision("gain_loss")) + + self.gain_loss_booked = gain_loss_booked + self.gain_loss_unbooked = gain_loss_unbooked self.total_gain_loss = flt(total_gain_loss, self.precision("total_gain_loss")) def validate_mandatory(self): @@ -35,98 +51,206 @@ class ExchangeRateRevaluation(Document): @frappe.whitelist() def check_journal_entry_condition(self): - total_debit = frappe.db.get_value( - "Journal Entry Account", - {"reference_type": "Exchange Rate Revaluation", "reference_name": self.name, "docstatus": 1}, - "sum(debit) as sum", + exchange_gain_loss_account = self.get_for_unrealized_gain_loss_account() + + jea = qb.DocType("Journal Entry Account") + journals = ( + qb.from_(jea) + .select(jea.parent) + .distinct() + .where( + (jea.reference_type == "Exchange Rate Revaluation") + & (jea.reference_name == self.name) + & (jea.docstatus == 1) + ) + .run() ) - total_amt = 0 - for d in self.accounts: - total_amt = total_amt + d.new_balance_in_base_currency + if journals: + gle = qb.DocType("GL Entry") + total_amt = ( + qb.from_(gle) + .select((Sum(gle.credit) - Sum(gle.debit)).as_("total_amount")) + .where( + (gle.voucher_type == "Journal Entry") + & (gle.voucher_no.isin(journals)) + & (gle.account == exchange_gain_loss_account) + & (gle.is_cancelled == 0) + ) + .run() + ) - if total_amt != total_debit: - return True + if total_amt and total_amt[0][0] != self.total_gain_loss: + return True + else: + return False - return False + return True @frappe.whitelist() - def get_accounts_data(self, account=None): - accounts = [] + def get_accounts_data(self): self.validate_mandatory() - company_currency = erpnext.get_company_currency(self.company) + account_details = self.get_account_balance_from_gle( + company=self.company, posting_date=self.posting_date, account=None, party_type=None, party=None + ) + accounts_with_new_balance = self.calculate_new_account_balance( + self.company, self.posting_date, account_details + ) + + if not accounts_with_new_balance: + self.throw_invalid_response_message(account_details) + + return accounts_with_new_balance + + @staticmethod + def get_account_balance_from_gle(company, posting_date, account, party_type, party): + account_details = [] + + if company and posting_date: + company_currency = erpnext.get_company_currency(company) + + acc = qb.DocType("Account") + if account: + accounts = [account] + else: + res = ( + qb.from_(acc) + .select(acc.name) + .where( + (acc.is_group == 0) + & (acc.report_type == "Balance Sheet") + & (acc.root_type.isin(["Asset", "Liability", "Equity"])) + & (acc.account_type != "Stock") + & (acc.company == company) + & (acc.account_currency != company_currency) + ) + .orderby(acc.name) + .run(as_list=True) + ) + accounts = [x[0] for x in res] + + if accounts: + having_clause = (qb.Field("balance") != qb.Field("balance_in_account_currency")) & ( + (qb.Field("balance_in_account_currency") != 0) | (qb.Field("balance") != 0) + ) + + gle = qb.DocType("GL Entry") + + # conditions + conditions = [] + conditions.append(gle.account.isin(accounts)) + conditions.append(gle.posting_date.lte(posting_date)) + conditions.append(gle.is_cancelled == 0) + + if party_type: + conditions.append(gle.party_type == party_type) + if party: + conditions.append(gle.party == party) + + account_details = ( + qb.from_(gle) + .select( + gle.account, + gle.party_type, + gle.party, + gle.account_currency, + (Sum(gle.debit_in_account_currency) - Sum(gle.credit_in_account_currency)).as_( + "balance_in_account_currency" + ), + (Sum(gle.debit) - Sum(gle.credit)).as_("balance"), + (Sum(gle.debit) - Sum(gle.credit) == 0) + ^ (Sum(gle.debit_in_account_currency) - Sum(gle.credit_in_account_currency) == 0).as_( + "zero_balance" + ), + ) + .where(Criterion.all(conditions)) + .groupby(gle.account, NullIf(gle.party_type, ""), NullIf(gle.party, "")) + .having(having_clause) + .orderby(gle.account) + .run(as_dict=True) + ) + + return account_details + + @staticmethod + def calculate_new_account_balance(company, posting_date, account_details): + accounts = [] + company_currency = erpnext.get_company_currency(company) precision = get_field_precision( frappe.get_meta("Exchange Rate Revaluation Account").get_field("new_balance_in_base_currency"), company_currency, ) - account_details = self.get_accounts_from_gle() - for d in account_details: - current_exchange_rate = ( - d.balance / d.balance_in_account_currency if d.balance_in_account_currency else 0 - ) - new_exchange_rate = get_exchange_rate(d.account_currency, company_currency, self.posting_date) - new_balance_in_base_currency = flt(d.balance_in_account_currency * new_exchange_rate) - gain_loss = flt(new_balance_in_base_currency, precision) - flt(d.balance, precision) - if gain_loss: - accounts.append( - { - "account": d.account, - "party_type": d.party_type, - "party": d.party, - "account_currency": d.account_currency, - "balance_in_base_currency": d.balance, - "balance_in_account_currency": d.balance_in_account_currency, - "current_exchange_rate": current_exchange_rate, - "new_exchange_rate": new_exchange_rate, - "new_balance_in_base_currency": new_balance_in_base_currency, - } + if account_details: + # Handle Accounts with balance in both Account/Base Currency + for d in [x for x in account_details if not x.zero_balance]: + current_exchange_rate = ( + d.balance / d.balance_in_account_currency if d.balance_in_account_currency else 0 ) + new_exchange_rate = get_exchange_rate(d.account_currency, company_currency, posting_date) + new_balance_in_base_currency = flt(d.balance_in_account_currency * new_exchange_rate) + gain_loss = flt(new_balance_in_base_currency, precision) - flt(d.balance, precision) + if gain_loss: + accounts.append( + { + "account": d.account, + "party_type": d.party_type, + "party": d.party, + "account_currency": d.account_currency, + "balance_in_base_currency": d.balance, + "balance_in_account_currency": d.balance_in_account_currency, + "zero_balance": d.zero_balance, + "current_exchange_rate": current_exchange_rate, + "new_exchange_rate": new_exchange_rate, + "new_balance_in_base_currency": new_balance_in_base_currency, + "new_balance_in_account_currency": d.balance_in_account_currency, + "gain_loss": gain_loss, + } + ) - if not accounts: - self.throw_invalid_response_message(account_details) + # Handle Accounts with '0' balance in Account/Base Currency + for d in [x for x in account_details if x.zero_balance]: + + # TODO: Set new balance in Base/Account currency + if d.balance > 0: + current_exchange_rate = new_exchange_rate = 0 + + new_balance_in_account_currency = 0 # this will be '0' + new_balance_in_base_currency = 0 # this will be '0' + gain_loss = flt(new_balance_in_base_currency, precision) - flt(d.balance, precision) + else: + new_exchange_rate = 0 + new_balance_in_base_currency = 0 + new_balance_in_account_currency = 0 + + current_exchange_rate = calculate_exchange_rate_using_last_gle( + company, d.account, d.party_type, d.party + ) + + gain_loss = new_balance_in_account_currency - ( + current_exchange_rate * d.balance_in_account_currency + ) + + if gain_loss: + accounts.append( + { + "account": d.account, + "party_type": d.party_type, + "party": d.party, + "account_currency": d.account_currency, + "balance_in_base_currency": d.balance, + "balance_in_account_currency": d.balance_in_account_currency, + "zero_balance": d.zero_balance, + "current_exchange_rate": current_exchange_rate, + "new_exchange_rate": new_exchange_rate, + "new_balance_in_base_currency": new_balance_in_base_currency, + "new_balance_in_account_currency": new_balance_in_account_currency, + "gain_loss": gain_loss, + } + ) return accounts - def get_accounts_from_gle(self): - company_currency = erpnext.get_company_currency(self.company) - accounts = frappe.db.sql_list( - """ - select name - from tabAccount - where is_group = 0 - and report_type = 'Balance Sheet' - and root_type in ('Asset', 'Liability', 'Equity') - and account_type != 'Stock' - and company=%s - and account_currency != %s - order by name""", - (self.company, company_currency), - ) - - account_details = [] - if accounts: - account_details = frappe.db.sql( - """ - select - account, party_type, party, account_currency, - sum(debit_in_account_currency) - sum(credit_in_account_currency) as balance_in_account_currency, - sum(debit) - sum(credit) as balance - from `tabGL Entry` - where account in (%s) - and posting_date <= %s - and is_cancelled = 0 - group by account, NULLIF(party_type,''), NULLIF(party,'') - having sum(debit) != sum(credit) - order by account - """ - % (", ".join(["%s"] * len(accounts)), "%s"), - tuple(accounts + [self.posting_date]), - as_dict=1, - ) - - return account_details - def throw_invalid_response_message(self, account_details): if account_details: message = _("No outstanding invoices require exchange rate revaluation") @@ -134,11 +258,7 @@ class ExchangeRateRevaluation(Document): message = _("No outstanding invoices found") frappe.msgprint(message) - @frappe.whitelist() - def make_jv_entry(self): - if self.total_gain_loss == 0: - return - + def get_for_unrealized_gain_loss_account(self): unrealized_exchange_gain_loss_account = frappe.get_cached_value( "Company", self.company, "unrealized_exchange_gain_loss_account" ) @@ -146,6 +266,130 @@ class ExchangeRateRevaluation(Document): frappe.throw( _("Please set Unrealized Exchange Gain/Loss Account in Company {0}").format(self.company) ) + return unrealized_exchange_gain_loss_account + + @frappe.whitelist() + def make_jv_entries(self): + zero_balance_jv = self.make_jv_for_zero_balance() + if zero_balance_jv: + frappe.msgprint( + f"Zero Balance Journal: {get_link_to_form('Journal Entry', zero_balance_jv.name)}" + ) + + revaluation_jv = self.make_jv_for_revaluation() + if revaluation_jv: + frappe.msgprint( + f"Revaluation Journal: {get_link_to_form('Journal Entry', revaluation_jv.name)}" + ) + + return { + "revaluation_jv": revaluation_jv.name if revaluation_jv else None, + "zero_balance_jv": zero_balance_jv.name if zero_balance_jv else None, + } + + def make_jv_for_zero_balance(self): + if self.gain_loss_booked == 0: + return + + accounts = [x for x in self.accounts if x.zero_balance] + + if not accounts: + return + + unrealized_exchange_gain_loss_account = self.get_for_unrealized_gain_loss_account() + + journal_entry = frappe.new_doc("Journal Entry") + journal_entry.voucher_type = "Exchange Gain Or Loss" + journal_entry.company = self.company + journal_entry.posting_date = self.posting_date + journal_entry.multi_currency = 1 + + journal_entry_accounts = [] + for d in accounts: + journal_account = frappe._dict( + { + "account": d.get("account"), + "party_type": d.get("party_type"), + "party": d.get("party"), + "account_currency": d.get("account_currency"), + "balance": flt( + d.get("balance_in_account_currency"), d.precision("balance_in_account_currency") + ), + "exchange_rate": 0, + "cost_center": erpnext.get_default_cost_center(self.company), + "reference_type": "Exchange Rate Revaluation", + "reference_name": self.name, + } + ) + + # Account Currency has balance + if d.get("balance_in_account_currency") and not d.get("new_balance_in_account_currency"): + dr_or_cr = ( + "credit_in_account_currency" + if d.get("balance_in_account_currency") > 0 + else "debit_in_account_currency" + ) + reverse_dr_or_cr = ( + "debit_in_account_currency" + if dr_or_cr == "credit_in_account_currency" + else "credit_in_account_currency" + ) + journal_account.update( + { + dr_or_cr: flt( + abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency") + ), + reverse_dr_or_cr: 0, + "debit": 0, + "credit": 0, + } + ) + elif d.get("balance_in_base_currency") and not d.get("new_balance_in_base_currency"): + # Base currency has balance + dr_or_cr = "credit" if d.get("balance_in_base_currency") > 0 else "debit" + reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit" + journal_account.update( + { + dr_or_cr: flt( + abs(d.get("balance_in_base_currency")), d.precision("balance_in_base_currency") + ), + reverse_dr_or_cr: 0, + "debit_in_account_currency": 0, + "credit_in_account_currency": 0, + } + ) + + journal_entry_accounts.append(journal_account) + + journal_entry_accounts.append( + { + "account": unrealized_exchange_gain_loss_account, + "balance": get_balance_on(unrealized_exchange_gain_loss_account), + "debit": abs(self.gain_loss_booked) if self.gain_loss_booked < 0 else 0, + "credit": abs(self.gain_loss_booked) if self.gain_loss_booked > 0 else 0, + "debit_in_account_currency": abs(self.gain_loss_booked) if self.gain_loss_booked < 0 else 0, + "credit_in_account_currency": self.gain_loss_booked if self.gain_loss_booked > 0 else 0, + "cost_center": erpnext.get_default_cost_center(self.company), + "exchange_rate": 1, + "reference_type": "Exchange Rate Revaluation", + "reference_name": self.name, + } + ) + + journal_entry.set("accounts", journal_entry_accounts) + journal_entry.set_total_debit_credit() + journal_entry.save() + return journal_entry + + def make_jv_for_revaluation(self): + if self.gain_loss_unbooked == 0: + return + + accounts = [x for x in self.accounts if not x.zero_balance] + if not accounts: + return + + unrealized_exchange_gain_loss_account = self.get_for_unrealized_gain_loss_account() journal_entry = frappe.new_doc("Journal Entry") journal_entry.voucher_type = "Exchange Rate Revaluation" @@ -154,7 +398,7 @@ class ExchangeRateRevaluation(Document): journal_entry.multi_currency = 1 journal_entry_accounts = [] - for d in self.accounts: + for d in accounts: dr_or_cr = ( "debit_in_account_currency" if d.get("balance_in_account_currency") > 0 @@ -179,6 +423,7 @@ class ExchangeRateRevaluation(Document): dr_or_cr: flt( abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency") ), + "cost_center": erpnext.get_default_cost_center(self.company), "exchange_rate": flt(d.get("new_exchange_rate"), d.precision("new_exchange_rate")), "reference_type": "Exchange Rate Revaluation", "reference_name": self.name, @@ -196,6 +441,7 @@ class ExchangeRateRevaluation(Document): reverse_dr_or_cr: flt( abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency") ), + "cost_center": erpnext.get_default_cost_center(self.company), "exchange_rate": flt(d.get("current_exchange_rate"), d.precision("current_exchange_rate")), "reference_type": "Exchange Rate Revaluation", "reference_name": self.name, @@ -206,8 +452,11 @@ class ExchangeRateRevaluation(Document): { "account": unrealized_exchange_gain_loss_account, "balance": get_balance_on(unrealized_exchange_gain_loss_account), - "debit_in_account_currency": abs(self.total_gain_loss) if self.total_gain_loss < 0 else 0, - "credit_in_account_currency": self.total_gain_loss if self.total_gain_loss > 0 else 0, + "debit_in_account_currency": abs(self.gain_loss_unbooked) + if self.gain_loss_unbooked < 0 + else 0, + "credit_in_account_currency": self.gain_loss_unbooked if self.gain_loss_unbooked > 0 else 0, + "cost_center": erpnext.get_default_cost_center(self.company), "exchange_rate": 1, "reference_type": "Exchange Rate Revaluation", "reference_name": self.name, @@ -217,38 +466,91 @@ class ExchangeRateRevaluation(Document): journal_entry.set("accounts", journal_entry_accounts) journal_entry.set_amounts_in_company_currency() journal_entry.set_total_debit_credit() - return journal_entry.as_dict() + journal_entry.save() + return journal_entry + + +def calculate_exchange_rate_using_last_gle(company, account, party_type, party): + """ + Use last GL entry to calculate exchange rate + """ + last_exchange_rate = None + if company and account: + gl = qb.DocType("GL Entry") + + # build conditions + conditions = [] + conditions.append(gl.company == company) + conditions.append(gl.account == account) + conditions.append(gl.is_cancelled == 0) + if party_type: + conditions.append(gl.party_type == party_type) + if party: + conditions.append(gl.party == party) + + voucher_type, voucher_no = ( + qb.from_(gl) + .select(gl.voucher_type, gl.voucher_no) + .where(Criterion.all(conditions)) + .orderby(gl.posting_date, order=Order.desc) + .limit(1) + .run()[0] + ) + + last_exchange_rate = ( + qb.from_(gl) + .select((gl.debit - gl.credit) / (gl.debit_in_account_currency - gl.credit_in_account_currency)) + .where( + (gl.voucher_type == voucher_type) & (gl.voucher_no == voucher_no) & (gl.account == account) + ) + .orderby(gl.posting_date, order=Order.desc) + .limit(1) + .run()[0][0] + ) + + return last_exchange_rate @frappe.whitelist() -def get_account_details(account, company, posting_date, party_type=None, party=None): - account_currency, account_type = frappe.db.get_value( +def get_account_details(company, posting_date, account, party_type=None, party=None): + if not (company and posting_date): + frappe.throw(_("Company and Posting Date is mandatory")) + + account_currency, account_type = frappe.get_cached_value( "Account", account, ["account_currency", "account_type"] ) + if account_type in ["Receivable", "Payable"] and not (party_type and party): frappe.throw(_("Party Type and Party is mandatory for {0} account").format(account_type)) account_details = {} company_currency = erpnext.get_company_currency(company) - balance = get_balance_on( - account, date=posting_date, party_type=party_type, party=party, in_account_currency=False + + account_details = { + "account_currency": account_currency, + } + account_balance = ExchangeRateRevaluation.get_account_balance_from_gle( + company=company, posting_date=posting_date, account=account, party_type=party_type, party=party ) - if balance: - balance_in_account_currency = get_balance_on( - account, date=posting_date, party_type=party_type, party=party + + if account_balance and ( + account_balance[0].balance or account_balance[0].balance_in_account_currency + ): + account_with_new_balance = ExchangeRateRevaluation.calculate_new_account_balance( + company, posting_date, account_balance ) - current_exchange_rate = ( - balance / balance_in_account_currency if balance_in_account_currency else 0 + row = account_with_new_balance[0] + account_details.update( + { + "balance_in_base_currency": row["balance_in_base_currency"], + "balance_in_account_currency": row["balance_in_account_currency"], + "current_exchange_rate": row["current_exchange_rate"], + "new_exchange_rate": row["new_exchange_rate"], + "new_balance_in_base_currency": row["new_balance_in_base_currency"], + "new_balance_in_account_currency": row["new_balance_in_account_currency"], + "zero_balance": row["zero_balance"], + "gain_loss": row["gain_loss"], + } ) - new_exchange_rate = get_exchange_rate(account_currency, company_currency, posting_date) - new_balance_in_base_currency = balance_in_account_currency * new_exchange_rate - account_details = { - "account_currency": account_currency, - "balance_in_base_currency": balance, - "balance_in_account_currency": balance_in_account_currency, - "current_exchange_rate": current_exchange_rate, - "new_exchange_rate": new_exchange_rate, - "new_balance_in_base_currency": new_balance_in_base_currency, - } return account_details diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation_account/exchange_rate_revaluation_account.json b/erpnext/accounts/doctype/exchange_rate_revaluation_account/exchange_rate_revaluation_account.json index 30ff9ebed5..2968359a0d 100644 --- a/erpnext/accounts/doctype/exchange_rate_revaluation_account/exchange_rate_revaluation_account.json +++ b/erpnext/accounts/doctype/exchange_rate_revaluation_account/exchange_rate_revaluation_account.json @@ -1,475 +1,161 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, + "actions": [], "creation": "2018-04-13 18:30:06.110433", - "custom": 0, - "docstatus": 0, "doctype": "DocType", - "document_type": "", "editable_grid": 1, "engine": "InnoDB", + "field_order": [ + "account", + "party_type", + "party", + "column_break_2", + "account_currency", + "account_balances", + "balance_in_account_currency", + "column_break_46yz", + "new_balance_in_account_currency", + "balances", + "current_exchange_rate", + "column_break_xown", + "new_exchange_rate", + "column_break_9", + "balance_in_base_currency", + "column_break_ukce", + "new_balance_in_base_currency", + "section_break_ngrs", + "gain_loss", + "zero_balance" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "account", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, "label": "Account", - "length": 0, - "no_copy": 0, "options": "Account", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "party_type", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Party Type", - "length": 0, - "no_copy": 0, - "options": "DocType", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "DocType" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "party", "fieldtype": "Dynamic Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Party", - "length": 0, - "no_copy": 0, - "options": "party_type", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "party_type" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "column_break_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, - "fetch_if_empty": 0, "fieldname": "account_currency", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Account Currency", - "length": 0, - "no_copy": 0, "options": "Currency", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "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, - "fetch_if_empty": 0, "fieldname": "balance_in_account_currency", "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Balance In Account Currency", - "length": 0, - "no_copy": 0, "options": "account_currency", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "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, - "fetch_if_empty": 0, "fieldname": "balances", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldtype": "Section Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "current_exchange_rate", "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": "Current Exchange Rate", - "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 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "balance_in_base_currency", "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, "label": "Balance In Base Currency", - "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 + "options": "Company:company:default_currency", + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "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": "Section Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "new_exchange_rate", "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": "New Exchange Rate", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "new_balance_in_base_currency", "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, "label": "New Balance In Base Currency", - "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 + "options": "Company:company:default_currency", + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "gain_loss", "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, "label": "Gain/Loss", - "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 + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "default": "0", + "description": "This Account has '0' balance in either Base Currency or Account Currency", + "fieldname": "zero_balance", + "fieldtype": "Check", + "label": "Zero Balance" + }, + { + "fieldname": "new_balance_in_account_currency", + "fieldtype": "Currency", + "label": "New Balance In Account Currency", + "options": "account_currency", + "read_only": 1 + }, + { + "fieldname": "account_balances", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_46yz", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_xown", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_ukce", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_ngrs", + "fieldtype": "Section Break" } ], - "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": "2019-06-26 18:57:51.762345", + "links": [], + "modified": "2022-12-29 19:38:52.915295", "modified_by": "Administrator", "module": "Accounts", "name": "Exchange Rate Revaluation Account", - "name_case": "", "owner": "Administrator", "permissions": [], "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, "sort_field": "modified", "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + "states": [], + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/accounts/doctype/fiscal_year/fiscal_year.py b/erpnext/accounts/doctype/fiscal_year/fiscal_year.py index 069ab5ea84..3207e4195e 100644 --- a/erpnext/accounts/doctype/fiscal_year/fiscal_year.py +++ b/erpnext/accounts/doctype/fiscal_year/fiscal_year.py @@ -9,10 +9,6 @@ from frappe.model.document import Document from frappe.utils import add_days, add_years, cstr, getdate -class FiscalYearIncorrectDate(frappe.ValidationError): - pass - - class FiscalYear(Document): @frappe.whitelist() def set_as_default(self): @@ -53,23 +49,18 @@ class FiscalYear(Document): ) def validate_dates(self): + self.validate_from_to_dates("year_start_date", "year_end_date") if self.is_short_year: # Fiscal Year can be shorter than one year, in some jurisdictions # under certain circumstances. For example, in the USA and Germany. return - if getdate(self.year_start_date) > getdate(self.year_end_date): - frappe.throw( - _("Fiscal Year Start Date should be one year earlier than Fiscal Year End Date"), - FiscalYearIncorrectDate, - ) - date = getdate(self.year_start_date) + relativedelta(years=1) - relativedelta(days=1) if getdate(self.year_end_date) != date: frappe.throw( _("Fiscal Year End Date should be one year after Fiscal Year Start Date"), - FiscalYearIncorrectDate, + frappe.exceptions.InvalidDates, ) def on_update(self): @@ -169,5 +160,6 @@ def auto_create_fiscal_year(): def get_from_and_to_date(fiscal_year): - fields = ["year_start_date as from_date", "year_end_date as to_date"] - return frappe.db.get_value("Fiscal Year", fiscal_year, fields, as_dict=1) + fields = ["year_start_date", "year_end_date"] + cached_results = frappe.get_cached_value("Fiscal Year", fiscal_year, fields, as_dict=1) + return dict(from_date=cached_results.year_start_date, to_date=cached_results.year_end_date) diff --git a/erpnext/accounts/doctype/fiscal_year/test_fiscal_year.py b/erpnext/accounts/doctype/fiscal_year/test_fiscal_year.py index 6e946f7466..181406bad7 100644 --- a/erpnext/accounts/doctype/fiscal_year/test_fiscal_year.py +++ b/erpnext/accounts/doctype/fiscal_year/test_fiscal_year.py @@ -7,8 +7,6 @@ import unittest import frappe from frappe.utils import now_datetime -from erpnext.accounts.doctype.fiscal_year.fiscal_year import FiscalYearIncorrectDate - test_ignore = ["Company"] @@ -26,7 +24,7 @@ class TestFiscalYear(unittest.TestCase): } ) - self.assertRaises(FiscalYearIncorrectDate, fy.insert) + self.assertRaises(frappe.exceptions.InvalidDates, fy.insert) def test_record_generator(): @@ -35,8 +33,8 @@ def test_record_generator(): "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", + "year_start_date": "2011-04-01", + "year_end_date": "2011-12-31", } ] diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index 1987c8340d..f07a4fa3bc 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -58,7 +58,7 @@ class GLEntry(Document): validate_balance_type(self.account, adv_adj) validate_frozen_account(self.account, adv_adj) - if frappe.db.get_value("Account", self.account, "account_type") not in [ + if frappe.get_cached_value("Account", self.account, "account_type") not in [ "Receivable", "Payable", ]: @@ -95,7 +95,15 @@ class GLEntry(Document): ) # Zero value transaction is not allowed - if not (flt(self.debit, self.precision("debit")) or flt(self.credit, self.precision("credit"))): + if not ( + flt(self.debit, self.precision("debit")) + or flt(self.credit, self.precision("credit")) + or ( + self.voucher_type == "Journal Entry" + and frappe.get_cached_value("Journal Entry", self.voucher_no, "voucher_type") + == "Exchange Gain Or Loss" + ) + ): frappe.throw( _("{0} {1}: Either debit or credit amount is required for {2}").format( self.voucher_type, self.voucher_no, self.account @@ -120,7 +128,7 @@ class GLEntry(Document): frappe.throw(msg, title=_("Missing Cost Center")) def validate_dimensions_for_pl_and_bs(self): - account_type = frappe.db.get_value("Account", self.account, "report_type") + account_type = frappe.get_cached_value("Account", self.account, "report_type") for dimension in get_checks_for_pl_and_bs_accounts(): if ( @@ -188,7 +196,7 @@ class GLEntry(Document): def check_pl_account(self): if ( self.is_opening == "Yes" - and frappe.db.get_value("Account", self.account, "report_type") == "Profit and Loss" + and frappe.get_cached_value("Account", self.account, "report_type") == "Profit and Loss" and not self.is_cancelled ): frappe.throw( @@ -281,7 +289,7 @@ class GLEntry(Document): def validate_balance_type(account, adv_adj=False): if not adv_adj and account: - balance_must_be = frappe.db.get_value("Account", account, "balance_must_be") + balance_must_be = frappe.get_cached_value("Account", account, "balance_must_be") if balance_must_be: balance = frappe.db.sql( """select sum(debit) - sum(credit) @@ -366,7 +374,7 @@ def update_outstanding_amt( if against_voucher_type in ["Sales Invoice", "Purchase Invoice", "Fees"]: ref_doc = frappe.get_doc(against_voucher_type, against_voucher) - # Didn't use db_set for optimisation purpose + # Didn't use db_set for optimization purpose ref_doc.outstanding_amount = bal frappe.db.set_value(against_voucher_type, against_voucher, "outstanding_amount", bal) diff --git a/erpnext/accounts/doctype/item_tax_template/item_tax_template.py b/erpnext/accounts/doctype/item_tax_template/item_tax_template.py index 23f36ec6d8..7e2fca8d24 100644 --- a/erpnext/accounts/doctype/item_tax_template/item_tax_template.py +++ b/erpnext/accounts/doctype/item_tax_template/item_tax_template.py @@ -21,7 +21,7 @@ class ItemTaxTemplate(Document): check_list = [] for d in self.get("taxes"): if d.tax_type: - account_type = frappe.db.get_value("Account", d.tax_type, "account_type") + account_type = frappe.get_cached_value("Account", d.tax_type, "account_type") if account_type not in [ "Tax", diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index 7af41f398a..089f20b467 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js @@ -8,7 +8,7 @@ frappe.provide("erpnext.journal_entry"); frappe.ui.form.on("Journal Entry", { setup: function(frm) { frm.add_fetch("bank_account", "account", "account"); - frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice']; + frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', "Repost Payment Ledger"]; }, refresh: function(frm) { @@ -173,8 +173,8 @@ frappe.ui.form.on("Journal Entry", { var update_jv_details = function(doc, r) { $.each(r, function(i, d) { var row = frappe.model.add_child(doc, "Journal Entry Account", "accounts"); - row.account = d.account; - row.balance = d.balance; + frappe.model.set_value(row.doctype, row.name, "account", d.account) + frappe.model.set_value(row.doctype, row.name, "balance", d.balance) }); refresh_field("accounts"); } @@ -253,9 +253,6 @@ erpnext.accounts.JournalEntry = class JournalEntry extends frappe.ui.form.Contro var party_account_field = jvd.reference_type==="Sales Invoice" ? "debit_to": "credit_to"; out.filters.push([jvd.reference_type, party_account_field, "=", jvd.account]); - if (in_list(['Debit Note', 'Credit Note'], doc.voucher_type)) { - out.filters.push([jvd.reference_type, "is_return", "=", 1]); - } } if(in_list(["Sales Order", "Purchase Order"], jvd.reference_type)) { @@ -312,8 +309,7 @@ erpnext.accounts.JournalEntry = class JournalEntry extends frappe.ui.form.Contro } } - get_outstanding(doctype, docname, company, child, due_date) { - var me = this; + get_outstanding(doctype, docname, company, child) { var args = { "doctype": doctype, "docname": docname, diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.json b/erpnext/accounts/doctype/journal_entry/journal_entry.json index 8e5ba3718f..498fc7c295 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.json +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.json @@ -88,7 +88,7 @@ "label": "Entry Type", "oldfieldname": "voucher_type", "oldfieldtype": "Select", - "options": "Journal Entry\nInter Company Journal Entry\nBank Entry\nCash Entry\nCredit Card Entry\nDebit Note\nCredit Note\nContra Entry\nExcise Entry\nWrite Off Entry\nOpening Entry\nDepreciation Entry\nExchange Rate Revaluation\nDeferred Revenue\nDeferred Expense", + "options": "Journal Entry\nInter Company Journal Entry\nBank Entry\nCash Entry\nCredit Card Entry\nDebit Note\nCredit Note\nContra Entry\nExcise Entry\nWrite Off Entry\nOpening Entry\nDepreciation Entry\nExchange Rate Revaluation\nExchange Gain Or Loss\nDeferred Revenue\nDeferred Expense", "reqd": 1, "search_index": 1 }, @@ -137,8 +137,7 @@ "fieldname": "finance_book", "fieldtype": "Link", "label": "Finance Book", - "options": "Finance Book", - "read_only": 1 + "options": "Finance Book" }, { "fieldname": "2_add_edit_gl_entries", @@ -539,7 +538,7 @@ "idx": 176, "is_submittable": 1, "links": [], - "modified": "2022-06-23 22:01:32.348337", + "modified": "2023-01-17 12:53:53.280620", "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 63c6547f1d..db399b7bad 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -6,7 +6,7 @@ import json import frappe from frappe import _, msgprint, scrub -from frappe.utils import cint, cstr, flt, fmt_money, formatdate, get_link_to_form, nowdate +from frappe.utils import cstr, flt, fmt_money, formatdate, get_link_to_form, nowdate import erpnext from erpnext.accounts.deferred_revenue import get_deferred_booking_accounts @@ -23,6 +23,9 @@ from erpnext.accounts.utils import ( get_stock_accounts, get_stock_and_account_balance, ) +from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import ( + get_depr_schedule, +) from erpnext.controllers.accounts_controller import AccountsController @@ -34,9 +37,6 @@ class JournalEntry(AccountsController): def __init__(self, *args, **kwargs): super(JournalEntry, self).__init__(*args, **kwargs) - def get_feed(self): - return self.voucher_type - def validate(self): if self.voucher_type == "Opening Entry": self.is_opening = "Yes" @@ -81,6 +81,7 @@ class JournalEntry(AccountsController): self.check_credit_limit() self.make_gl_entries() self.update_advance_paid() + self.update_asset_value() self.update_inter_company_jv() self.update_invoice_discounting() @@ -88,7 +89,13 @@ class JournalEntry(AccountsController): from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries unlink_ref_doc_from_payment_entries(self) - self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry") + self.ignore_linked_doctypes = ( + "GL Entry", + "Stock Ledger Entry", + "Payment Ledger Entry", + "Repost Payment Ledger", + "Repost Payment Ledger Items", + ) self.make_gl_entries(1) self.update_advance_paid() self.unlink_advance_entry_reference() @@ -184,7 +191,9 @@ class JournalEntry(AccountsController): } ) - tax_withholding_details = get_party_tax_withholding_details(inv, self.tax_withholding_category) + tax_withholding_details, advance_taxes, voucher_wise_amount = get_party_tax_withholding_details( + inv, self.tax_withholding_category + ) if not tax_withholding_details: return @@ -223,6 +232,29 @@ class JournalEntry(AccountsController): for d in to_remove: self.remove(d) + def update_asset_value(self): + if self.voucher_type != "Depreciation Entry": + return + + processed_assets = [] + + for d in self.get("accounts"): + if ( + d.reference_type == "Asset" and d.reference_name and d.reference_name not in processed_assets + ): + processed_assets.append(d.reference_name) + + asset = frappe.get_doc("Asset", d.reference_name) + + if asset.calculate_depreciation: + continue + + depr_value = d.debit or d.credit + + asset.db_set("value_after_depreciation", asset.value_after_depreciation - depr_value) + + asset.set_status() + def update_inter_company_jv(self): if ( self.voucher_type == "Inter Company Journal Entry" @@ -281,19 +313,45 @@ class JournalEntry(AccountsController): d.db_update() def unlink_asset_reference(self): + if self.voucher_type != "Depreciation Entry": + return + + processed_assets = [] + for d in self.get("accounts"): - if d.reference_type == "Asset" and d.reference_name: + if ( + d.reference_type == "Asset" and d.reference_name and d.reference_name not in processed_assets + ): + processed_assets.append(d.reference_name) + asset = frappe.get_doc("Asset", d.reference_name) - for s in asset.get("schedules"): - if s.journal_entry == self.name: - s.db_set("journal_entry", None) - idx = cint(s.finance_book_id) or 1 - finance_books = asset.get("finance_books")[idx - 1] - finance_books.value_after_depreciation += s.depreciation_amount - finance_books.db_update() + if asset.calculate_depreciation: + je_found = False - asset.set_status() + for row in asset.get("finance_books"): + if je_found: + break + + depr_schedule = get_depr_schedule(asset.name, "Active", row.finance_book) + + for s in depr_schedule or []: + if s.journal_entry == self.name: + s.db_set("journal_entry", None) + + row.value_after_depreciation += s.depreciation_amount + row.db_update() + + asset.set_status() + + je_found = True + break + else: + depr_value = d.debit or d.credit + + asset.db_set("value_after_depreciation", asset.value_after_depreciation + depr_value) + + asset.set_status() def unlink_inter_company_jv(self): if ( @@ -317,7 +375,7 @@ class JournalEntry(AccountsController): def validate_party(self): for d in self.get("accounts"): - account_type = frappe.db.get_value("Account", d.account, "account_type") + account_type = frappe.get_cached_value("Account", d.account, "account_type") if account_type in ["Receivable", "Payable"]: if not (d.party_type and d.party): frappe.throw( @@ -380,7 +438,7 @@ class JournalEntry(AccountsController): def validate_against_jv(self): for d in self.get("accounts"): if d.reference_type == "Journal Entry": - account_root_type = frappe.db.get_value("Account", d.account, "root_type") + account_root_type = frappe.get_cached_value("Account", d.account, "root_type") if account_root_type == "Asset" and flt(d.debit) > 0: frappe.throw( _( @@ -590,28 +648,30 @@ class JournalEntry(AccountsController): d.against_account = frappe.db.get_value(d.reference_type, d.reference_name, field) else: for d in self.get("accounts"): - if flt(d.debit > 0): + if flt(d.debit) > 0: accounts_debited.append(d.party or d.account) if flt(d.credit) > 0: accounts_credited.append(d.party or d.account) for d in self.get("accounts"): - if flt(d.debit > 0): + if flt(d.debit) > 0: d.against_account = ", ".join(list(set(accounts_credited))) - if flt(d.credit > 0): + if flt(d.credit) > 0: d.against_account = ", ".join(list(set(accounts_debited))) def validate_debit_credit_amount(self): - for d in self.get("accounts"): - if not flt(d.debit) and not flt(d.credit): - frappe.throw(_("Row {0}: Both Debit and Credit values cannot be zero").format(d.idx)) + if not (self.voucher_type == "Exchange Gain Or Loss" and self.multi_currency): + for d in self.get("accounts"): + if not flt(d.debit) and not flt(d.credit): + frappe.throw(_("Row {0}: Both Debit and Credit values cannot be zero").format(d.idx)) def validate_total_debit_and_credit(self): self.set_total_debit_credit() - if self.difference: - frappe.throw( - _("Total Debit must be equal to Total Credit. The difference is {0}").format(self.difference) - ) + if not (self.voucher_type == "Exchange Gain Or Loss" and self.multi_currency): + if self.difference: + frappe.throw( + _("Total Debit must be equal to Total Credit. The difference is {0}").format(self.difference) + ) def set_total_debit_credit(self): self.total_debit, self.total_credit, self.difference = 0, 0, 0 @@ -629,7 +689,7 @@ class JournalEntry(AccountsController): def validate_multi_currency(self): alternate_currency = [] for d in self.get("accounts"): - account = frappe.db.get_value( + account = frappe.get_cached_value( "Account", d.account, ["account_currency", "account_type"], as_dict=1 ) if account: @@ -649,16 +709,17 @@ class JournalEntry(AccountsController): self.set_exchange_rate() def set_amounts_in_company_currency(self): - for d in self.get("accounts"): - d.debit_in_account_currency = flt( - d.debit_in_account_currency, d.precision("debit_in_account_currency") - ) - d.credit_in_account_currency = flt( - d.credit_in_account_currency, d.precision("credit_in_account_currency") - ) + if not (self.voucher_type == "Exchange Gain Or Loss" and self.multi_currency): + for d in self.get("accounts"): + d.debit_in_account_currency = flt( + d.debit_in_account_currency, d.precision("debit_in_account_currency") + ) + d.credit_in_account_currency = flt( + d.credit_in_account_currency, d.precision("credit_in_account_currency") + ) - d.debit = flt(d.debit_in_account_currency * flt(d.exchange_rate), d.precision("debit")) - d.credit = flt(d.credit_in_account_currency * flt(d.exchange_rate), d.precision("credit")) + d.debit = flt(d.debit_in_account_currency * flt(d.exchange_rate), d.precision("debit")) + d.credit = flt(d.credit_in_account_currency * flt(d.exchange_rate), d.precision("credit")) def set_exchange_rate(self): for d in self.get("accounts"): @@ -757,10 +818,10 @@ class JournalEntry(AccountsController): pay_to_recd_from = d.party if pay_to_recd_from and pay_to_recd_from == d.party: - party_amount += d.debit_in_account_currency or d.credit_in_account_currency + party_amount += flt(d.debit_in_account_currency) or flt(d.credit_in_account_currency) party_account_currency = d.account_currency - elif frappe.db.get_value("Account", d.account, "account_type") in ["Bank", "Cash"]: + elif frappe.get_cached_value("Account", d.account, "account_type") in ["Bank", "Cash"]: bank_amount += d.debit_in_account_currency or d.credit_in_account_currency bank_account_currency = d.account_currency @@ -787,7 +848,7 @@ class JournalEntry(AccountsController): def build_gl_map(self): gl_map = [] for d in self.get("accounts"): - if d.debit or d.credit: + if d.debit or d.credit or (self.voucher_type == "Exchange Gain Or Loss"): r = [d.user_remark, self.remark] r = [x for x in r if x] remarks = "\n".join(r) @@ -835,7 +896,7 @@ class JournalEntry(AccountsController): make_gl_entries(gl_map, cancel=cancel, adv_adj=adv_adj, update_outstanding=update_outstanding) @frappe.whitelist() - def get_balance(self): + def get_balance(self, difference_account=None): if not self.get("accounts"): msgprint(_("'Entries' cannot be empty"), raise_exception=True) else: @@ -850,7 +911,13 @@ class JournalEntry(AccountsController): blank_row = d if not blank_row: - blank_row = self.append("accounts", {}) + blank_row = self.append( + "accounts", + { + "account": difference_account, + "cost_center": erpnext.get_default_cost_center(self.company), + }, + ) blank_row.exchange_rate = 1 if diff > 0: @@ -985,7 +1052,7 @@ def get_default_bank_cash_account(company, account_type=None, mode_of_payment=No account = account_list[0].name if account: - account_details = frappe.db.get_value( + account_details = frappe.get_cached_value( "Account", account, ["account_currency", "account_type"], as_dict=1 ) @@ -1114,7 +1181,7 @@ def get_payment_entry(ref_doc, args): "party_type": args.get("party_type"), "party": ref_doc.get(args.get("party_type").lower()), "cost_center": cost_center, - "account_type": frappe.db.get_value("Account", args.get("party_account"), "account_type"), + "account_type": frappe.get_cached_value("Account", args.get("party_account"), "account_type"), "account_currency": args.get("party_account_currency") or get_account_currency(args.get("party_account")), "balance": get_balance_on(args.get("party_account")), @@ -1208,6 +1275,7 @@ def get_outstanding(args): args = json.loads(args) company_currency = erpnext.get_company_currency(args.get("company")) + due_date = None if args.get("doctype") == "Journal Entry": condition = " and party=%(party)s" if args.get("party") else "" @@ -1232,10 +1300,12 @@ def get_outstanding(args): invoice = frappe.db.get_value( args["doctype"], args["docname"], - ["outstanding_amount", "conversion_rate", scrub(party_type)], + ["outstanding_amount", "conversion_rate", scrub(party_type), "due_date"], as_dict=1, ) + due_date = invoice.get("due_date") + exchange_rate = ( invoice.conversion_rate if (args.get("account_currency") != company_currency) else 1 ) @@ -1258,6 +1328,7 @@ def get_outstanding(args): "exchange_rate": exchange_rate, "party_type": party_type, "party": invoice.get(scrub(party_type)), + "reference_due_date": due_date, } @@ -1277,7 +1348,7 @@ def get_party_account_and_balance(company, party_type, party, cost_center=None): "account": account, "balance": account_balance, "party_balance": party_balance, - "account_currency": frappe.db.get_value("Account", account, "account_currency"), + "account_currency": frappe.get_cached_value("Account", account, "account_currency"), } @@ -1290,7 +1361,7 @@ def get_account_balance_and_party_type( frappe.msgprint(_("No Permission"), raise_exception=1) company_currency = erpnext.get_company_currency(company) - account_details = frappe.db.get_value( + account_details = frappe.get_cached_value( "Account", account, ["account_type", "account_currency"], as_dict=1 ) @@ -1343,7 +1414,7 @@ def get_exchange_rate( ): from erpnext.setup.utils import get_exchange_rate - account_details = frappe.db.get_value( + account_details = frappe.get_cached_value( "Account", account, ["account_type", "root_type", "account_currency", "company"], as_dict=1 ) diff --git a/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json b/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json index dff883aef9..47ad19e0f9 100644 --- a/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json +++ b/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json @@ -202,6 +202,7 @@ "fieldname": "reference_type", "fieldtype": "Select", "label": "Reference Type", + "no_copy": 1, "options": "\nSales Invoice\nPurchase Invoice\nJournal Entry\nSales Order\nPurchase Order\nExpense Claim\nAsset\nLoan\nPayroll Entry\nEmployee Advance\nExchange Rate Revaluation\nInvoice Discounting\nFees\nFull and Final Statement" }, { @@ -209,13 +210,15 @@ "fieldtype": "Dynamic Link", "in_list_view": 1, "label": "Reference Name", + "no_copy": 1, "options": "reference_type" }, { "depends_on": "eval:doc.reference_type&&!in_list(doc.reference_type, ['Expense Claim', 'Asset', 'Employee Loan', 'Employee Advance'])", "fieldname": "reference_due_date", - "fieldtype": "Select", - "label": "Reference Due Date" + "fieldtype": "Date", + "label": "Reference Due Date", + "no_copy": 1 }, { "fieldname": "project", @@ -274,19 +277,22 @@ "fieldname": "reference_detail_no", "fieldtype": "Data", "hidden": 1, - "label": "Reference Detail No" + "label": "Reference Detail No", + "no_copy": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2021-08-30 21:27:32.200299", + "modified": "2022-10-26 20:03:10.906259", "modified_by": "Administrator", "module": "Accounts", "name": "Journal Entry Account", + "naming_rule": "Random", "owner": "Administrator", "permissions": [], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/accounts/doctype/journal_entry_template/journal_entry_template.js b/erpnext/accounts/doctype/journal_entry_template/journal_entry_template.js index 1c19c1d225..88f1c9069c 100644 --- a/erpnext/accounts/doctype/journal_entry_template/journal_entry_template.js +++ b/erpnext/accounts/doctype/journal_entry_template/journal_entry_template.js @@ -2,7 +2,7 @@ // For license information, please see license.txt frappe.ui.form.on("Journal Entry Template", { - setup: function(frm) { + refresh: function(frm) { frappe.model.set_default_values(frm.doc); frm.set_query("account" ,"accounts", function(){ @@ -45,21 +45,6 @@ frappe.ui.form.on("Journal Entry Template", { frm.trigger("clear_child"); switch(frm.doc.voucher_type){ - case "Opening Entry": - frm.set_value("is_opening", "Yes"); - frappe.call({ - type:"GET", - method: "erpnext.accounts.doctype.journal_entry.journal_entry.get_opening_accounts", - args: { - "company": frm.doc.company - }, - callback: function(r) { - if(r.message) { - add_accounts(frm.doc, r.message); - } - } - }); - break; case "Bank Entry": case "Cash Entry": frappe.call({ diff --git a/erpnext/accounts/doctype/ledger_merge/ledger_merge.py b/erpnext/accounts/doctype/ledger_merge/ledger_merge.py index 18e5a1ac85..7cd6d04c35 100644 --- a/erpnext/accounts/doctype/ledger_merge/ledger_merge.py +++ b/erpnext/accounts/doctype/ledger_merge/ledger_merge.py @@ -4,22 +4,20 @@ import frappe from frappe import _ from frappe.model.document import Document +from frappe.utils.background_jobs import is_job_queued 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: + if not is_job_queued(self.name): enqueue( start_merge, queue="default", diff --git a/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.py b/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.py index ed35d1e094..7d6ef3c3bf 100644 --- a/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.py +++ b/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.py @@ -25,7 +25,7 @@ class ModeofPayment(Document): def validate_accounts(self): for entry in self.accounts: """Error when Company of Ledger account doesn't match with Company Selected""" - if frappe.db.get_value("Account", entry.default_account, "company") != entry.company: + if frappe.get_cached_value("Account", entry.default_account, "company") != entry.company: frappe.throw( _("Account {0} does not match with Company {1} in Mode of Account: {2}").format( entry.default_account, entry.company, self.name diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js index 7eb5c4234d..88867d11bb 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js @@ -20,15 +20,14 @@ frappe.ui.form.on('Opening Invoice Creation Tool', { frm.dashboard.reset(); frm.doc.import_in_progress = true; } - if (data.user != frappe.session.user) return; if (data.count == data.total) { - setTimeout((title) => { + setTimeout(() => { frm.doc.import_in_progress = false; frm.clear_table("invoices"); frm.refresh_fields(); frm.page.clear_indicator(); - frm.dashboard.hide_progress(title); - frappe.msgprint(__("Opening {0} Invoice created", [frm.doc.invoice_type])); + frm.dashboard.hide_progress(); + frappe.msgprint(__("Opening {0} Invoices created", [frm.doc.invoice_type])); }, 1500, data.title); return; } @@ -51,13 +50,6 @@ frappe.ui.form.on('Opening Invoice Creation Tool', { method: "make_invoices", freeze: 1, freeze_message: __("Creating {0} Invoice", [frm.doc.invoice_type]), - callback: function(r) { - if (r.message.length == 1) { - frappe.msgprint(__("{0} Invoice created successfully.", [frm.doc.invoice_type])); - } else if (r.message.length < 50) { - frappe.msgprint(__("{0} Invoices created successfully.", [frm.doc.invoice_type])); - } - } }); }); 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 99377421c5..47c2ceb6e4 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 @@ -6,7 +6,7 @@ import frappe from frappe import _, scrub from frappe.model.document import Document from frappe.utils import flt, nowdate -from frappe.utils.background_jobs import enqueue +from frappe.utils.background_jobs import enqueue, is_job_queued from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_accounting_dimensions, @@ -207,14 +207,12 @@ class OpeningInvoiceCreationTool(Document): if len(invoices) < 50: return start_import(invoices) else: - from frappe.core.page.background_jobs.background_jobs import get_info from frappe.utils.scheduler import is_scheduler_inactive if is_scheduler_inactive() and not frappe.flags.in_test: frappe.throw(_("Scheduler is inactive. Cannot import data."), title=_("Scheduler Inactive")) - enqueued_jobs = [d.get("job_name") for d in get_info()] - if self.name not in enqueued_jobs: + if not is_job_queued(self.name): enqueue( start_import, queue="default", @@ -257,17 +255,15 @@ def start_import(invoices): def publish(index, total, doctype): - if total < 50: - return frappe.publish_realtime( "opening_invoice_creation_progress", dict( title=_("Opening Invoice Creation In Progress"), message=_("Creating {} out of {} {}").format(index + 1, total, doctype), - user=frappe.session.user, count=index + 1, total=total, ), + user=frappe.session.user, ) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 0f53079403..91374ae217 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -7,7 +7,7 @@ cur_frm.cscript.tax_table = "Advance Taxes and Charges"; frappe.ui.form.on('Payment Entry', { onload: function(frm) { - frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice']; + frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', "Repost Payment Ledger"]; if(frm.doc.__islocal) { if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null); @@ -1091,7 +1091,7 @@ frappe.ui.form.on('Payment Entry', { $.each(tax_fields, function(i, fieldname) { tax[fieldname] = 0.0; }); - frm.doc.paid_amount_after_tax = frm.doc.paid_amount; + frm.doc.paid_amount_after_tax = frm.doc.base_paid_amount; }); }, @@ -1182,7 +1182,7 @@ frappe.ui.form.on('Payment Entry', { } cumulated_tax_fraction += tax.tax_fraction_for_current_item; - frm.doc.paid_amount_after_tax = flt(frm.doc.paid_amount/(1+cumulated_tax_fraction)) + frm.doc.paid_amount_after_tax = flt(frm.doc.base_paid_amount/(1+cumulated_tax_fraction)) }); }, @@ -1214,6 +1214,7 @@ frappe.ui.form.on('Payment Entry', { frm.doc.total_taxes_and_charges = 0.0; frm.doc.base_total_taxes_and_charges = 0.0; + let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency; let actual_tax_dict = {}; // maintain actual tax rate based on idx @@ -1234,8 +1235,8 @@ frappe.ui.form.on('Payment Entry', { } } - tax.tax_amount = current_tax_amount; - tax.base_tax_amount = tax.tax_amount * frm.doc.source_exchange_rate; + // tax accounts are only in company currency + tax.base_tax_amount = current_tax_amount; current_tax_amount *= (tax.add_deduct_tax == "Deduct") ? -1.0 : 1.0; if(i==0) { @@ -1244,9 +1245,29 @@ frappe.ui.form.on('Payment Entry', { tax.total = flt(frm.doc["taxes"][i-1].total + current_tax_amount, precision("total", tax)); } - tax.base_total = tax.total * frm.doc.source_exchange_rate; - frm.doc.total_taxes_and_charges += current_tax_amount; - frm.doc.base_total_taxes_and_charges += current_tax_amount * frm.doc.source_exchange_rate; + // tac accounts are only in company currency + tax.base_total = tax.total + + // calculate total taxes and base total taxes + if(frm.doc.payment_type == "Pay") { + // tax accounts only have company currency + if(tax.currency != frm.doc.paid_to_account_currency) { + //total_taxes_and_charges has the target currency. so using target conversion rate + frm.doc.total_taxes_and_charges += flt(current_tax_amount / frm.doc.target_exchange_rate); + + } else { + frm.doc.total_taxes_and_charges += current_tax_amount; + } + } else if(frm.doc.payment_type == "Receive") { + if(tax.currency != frm.doc.paid_from_account_currency) { + //total_taxes_and_charges has the target currency. so using source conversion rate + frm.doc.total_taxes_and_charges += flt(current_tax_amount / frm.doc.source_exchange_rate); + } else { + frm.doc.total_taxes_and_charges += current_tax_amount; + } + } + + frm.doc.base_total_taxes_and_charges += tax.base_tax_amount; frm.refresh_field('taxes'); frm.refresh_field('total_taxes_and_charges'); diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.json b/erpnext/accounts/doctype/payment_entry/payment_entry.json index 3fc1adff2d..3927ecae43 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.json +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.json @@ -239,7 +239,7 @@ "depends_on": "paid_from", "fieldname": "paid_from_account_currency", "fieldtype": "Link", - "label": "Account Currency", + "label": "Account Currency (From)", "options": "Currency", "print_hide": 1, "read_only": 1, @@ -249,7 +249,7 @@ "depends_on": "paid_from", "fieldname": "paid_from_account_balance", "fieldtype": "Currency", - "label": "Account Balance", + "label": "Account Balance (From)", "options": "paid_from_account_currency", "print_hide": 1, "read_only": 1 @@ -272,7 +272,7 @@ "depends_on": "paid_to", "fieldname": "paid_to_account_currency", "fieldtype": "Link", - "label": "Account Currency", + "label": "Account Currency (To)", "options": "Currency", "print_hide": 1, "read_only": 1, @@ -282,7 +282,7 @@ "depends_on": "paid_to", "fieldname": "paid_to_account_balance", "fieldtype": "Currency", - "label": "Account Balance", + "label": "Account Balance (To)", "options": "paid_to_account_currency", "print_hide": 1, "read_only": 1 @@ -304,7 +304,8 @@ { "fieldname": "source_exchange_rate", "fieldtype": "Float", - "label": "Exchange Rate", + "label": "Source Exchange Rate", + "precision": "9", "print_hide": 1, "reqd": 1 }, @@ -333,7 +334,8 @@ { "fieldname": "target_exchange_rate", "fieldtype": "Float", - "label": "Exchange Rate", + "label": "Target Exchange Rate", + "precision": "9", "print_hide": 1, "reqd": 1 }, @@ -631,14 +633,14 @@ "depends_on": "eval:doc.party_type == 'Supplier'", "fieldname": "purchase_taxes_and_charges_template", "fieldtype": "Link", - "label": "Taxes and Charges Template", + "label": "Purchase Taxes and Charges Template", "options": "Purchase Taxes and Charges Template" }, { "depends_on": "eval: doc.party_type == 'Customer'", "fieldname": "sales_taxes_and_charges_template", "fieldtype": "Link", - "label": "Taxes and Charges Template", + "label": "Sales Taxes and Charges Template", "options": "Sales Taxes and Charges Template" }, { @@ -731,7 +733,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2022-02-23 20:08:39.559814", + "modified": "2023-02-14 04:52:30.478523", "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 af5a5e249d..cd5b6d5ce2 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -62,7 +62,6 @@ class PaymentEntry(AccountsController): self.set_missing_values() self.validate_payment_type() self.validate_party_details() - self.validate_bank_accounts() self.set_exchange_rate() self.validate_mandatory() self.validate_reference_documents() @@ -93,7 +92,13 @@ class PaymentEntry(AccountsController): self.set_status() def on_cancel(self): - self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry") + self.ignore_linked_doctypes = ( + "GL Entry", + "Stock Ledger Entry", + "Payment Ledger Entry", + "Repost Payment Ledger", + "Repost Payment Ledger Items", + ) self.make_gl_entries(cancel=1) self.update_outstanding_amounts() self.update_advance_paid() @@ -181,7 +186,11 @@ class PaymentEntry(AccountsController): frappe.throw(_("Party is mandatory")) _party_name = "title" if self.party_type == "Shareholder" else self.party_type.lower() + "_name" - self.party_name = frappe.db.get_value(self.party_type, self.party, _party_name) + + if frappe.db.has_column(self.party_type, _party_name): + self.party_name = frappe.db.get_value(self.party_type, self.party, _party_name) + else: + self.party_name = frappe.db.get_value(self.party_type, self.party, "name") if self.party: if not self.party_balance: @@ -239,29 +248,12 @@ class PaymentEntry(AccountsController): if not frappe.db.exists(self.party_type, self.party): frappe.throw(_("Invalid {0}: {1}").format(self.party_type, self.party)) - if self.party_account and self.party_type in ("Customer", "Supplier"): - self.validate_account_type( - self.party_account, [erpnext.get_party_account_type(self.party_type)] - ) - - def validate_bank_accounts(self): - if self.payment_type in ("Pay", "Internal Transfer"): - self.validate_account_type(self.paid_from, ["Bank", "Cash"]) - - if self.payment_type in ("Receive", "Internal Transfer"): - self.validate_account_type(self.paid_to, ["Bank", "Cash"]) - - def validate_account_type(self, account, account_types): - account_type = frappe.db.get_value("Account", account, "account_type") - # if account_type not in account_types: - # frappe.throw(_("Account Type for {0} must be {1}").format(account, comma_or(account_types))) - def set_exchange_rate(self, ref_doc=None): self.set_source_exchange_rate(ref_doc) self.set_target_exchange_rate(ref_doc) def set_source_exchange_rate(self, ref_doc=None): - if self.paid_from and not self.source_exchange_rate: + if self.paid_from: if self.paid_from_account_currency == self.company_currency: self.source_exchange_rate = 1 else: @@ -295,6 +287,9 @@ class PaymentEntry(AccountsController): def validate_reference_documents(self): valid_reference_doctypes = self.get_valid_reference_doctypes() + if not valid_reference_doctypes: + return + for d in self.get("references"): if not d.allocated_amount: continue @@ -362,7 +357,7 @@ class PaymentEntry(AccountsController): if not d.allocated_amount: continue - if d.reference_doctype in ("Sales Invoice", "Purchase Invoice", "Fees"): + if d.reference_doctype in ("Sales Invoice", "Purchase Invoice"): outstanding_amount, is_return = frappe.get_cached_value( d.reference_doctype, d.reference_name, ["outstanding_amount", "is_return"] ) @@ -633,7 +628,7 @@ class PaymentEntry(AccountsController): self.payment_type == "Receive" and self.base_total_allocated_amount < self.base_received_amount + total_deductions and self.total_allocated_amount - < self.paid_amount + (total_deductions / self.source_exchange_rate) + < flt(self.paid_amount) + (total_deductions / self.source_exchange_rate) ): self.unallocated_amount = ( self.base_received_amount + total_deductions - self.base_total_allocated_amount @@ -643,7 +638,7 @@ class PaymentEntry(AccountsController): self.payment_type == "Pay" and self.base_total_allocated_amount < (self.base_paid_amount - total_deductions) and self.total_allocated_amount - < self.received_amount + (total_deductions / self.target_exchange_rate) + < flt(self.received_amount) + (total_deductions / self.target_exchange_rate) ): self.unallocated_amount = ( self.base_paid_amount - (total_deductions + self.base_total_allocated_amount) @@ -695,35 +690,34 @@ class PaymentEntry(AccountsController): ) def validate_payment_against_negative_invoice(self): - if (self.payment_type == "Pay" and self.party_type == "Customer") or ( - self.payment_type == "Receive" and self.party_type == "Supplier" + if (self.payment_type != "Pay" or self.party_type != "Customer") and ( + self.payment_type != "Receive" or self.party_type != "Supplier" ): + return - total_negative_outstanding = sum( - abs(flt(d.outstanding_amount)) for d in self.get("references") if flt(d.outstanding_amount) < 0 + total_negative_outstanding = sum( + abs(flt(d.outstanding_amount)) for d in self.get("references") if flt(d.outstanding_amount) < 0 + ) + + paid_amount = self.paid_amount if self.payment_type == "Receive" else self.received_amount + additional_charges = sum(flt(d.amount) for d in self.deductions) + + if not total_negative_outstanding: + if self.party_type == "Customer": + msg = _("Cannot pay to Customer without any negative outstanding invoice") + else: + msg = _("Cannot receive from Supplier without any negative outstanding invoice") + + frappe.throw(msg, InvalidPaymentEntry) + + elif paid_amount - additional_charges > total_negative_outstanding: + frappe.throw( + _("Paid Amount cannot be greater than total negative outstanding amount {0}").format( + total_negative_outstanding + ), + InvalidPaymentEntry, ) - paid_amount = self.paid_amount if self.payment_type == "Receive" else self.received_amount - additional_charges = sum([flt(d.amount) for d in self.deductions]) - - 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")), - self.party_type, - ), - InvalidPaymentEntry, - ) - - elif paid_amount - additional_charges > total_negative_outstanding: - frappe.throw( - _("Paid Amount cannot be greater than total negative outstanding amount {0}").format( - total_negative_outstanding - ), - InvalidPaymentEntry, - ) - def set_title(self): if frappe.flags.in_import and self.title: # do not set title dynamically if title exists during data import. @@ -736,7 +730,7 @@ class PaymentEntry(AccountsController): def validate_transaction_reference(self): bank_account = self.paid_to if self.payment_type == "Receive" else self.paid_from - bank_account_type = frappe.db.get_value("Account", bank_account, "account_type") + bank_account_type = frappe.get_cached_value("Account", bank_account, "account_type") if bank_account_type == "Bank": if not self.reference_no or not self.reference_date: @@ -933,6 +927,13 @@ class PaymentEntry(AccountsController): ) if not d.included_in_paid_amount: + if get_account_currency(payment_account) != self.company_currency: + if self.payment_type == "Receive": + exchange_rate = self.target_exchange_rate + elif self.payment_type in ["Pay", "Internal Transfer"]: + exchange_rate = self.source_exchange_rate + base_tax_amount = flt((tax_amount / exchange_rate), self.precision("paid_amount")) + gl_entries.append( self.get_gl_dict( { @@ -981,7 +982,9 @@ class PaymentEntry(AccountsController): if self.payment_type in ("Receive", "Pay") and self.party: for d in self.get("references"): if d.allocated_amount and d.reference_doctype in frappe.get_hooks("advance_payment_doctypes"): - frappe.get_doc(d.reference_doctype, d.reference_name).set_total_advance_paid() + frappe.get_doc( + d.reference_doctype, d.reference_name, for_update=True + ).set_total_advance_paid() def on_recurring(self, reference_doc, auto_repeat_doc): self.reference_no = reference_doc.name @@ -1026,7 +1029,7 @@ class PaymentEntry(AccountsController): for fieldname in tax_fields: tax.set(fieldname, 0.0) - self.paid_amount_after_tax = self.paid_amount + self.paid_amount_after_tax = self.base_paid_amount def determine_exclusive_rate(self): if not any(cint(tax.included_in_paid_amount) for tax in self.get("taxes")): @@ -1045,7 +1048,7 @@ class PaymentEntry(AccountsController): cumulated_tax_fraction += tax.tax_fraction_for_current_item - self.paid_amount_after_tax = flt(self.paid_amount / (1 + cumulated_tax_fraction)) + self.paid_amount_after_tax = flt(self.base_paid_amount / (1 + cumulated_tax_fraction)) def calculate_taxes(self): self.total_taxes_and_charges = 0.0 @@ -1068,7 +1071,7 @@ class PaymentEntry(AccountsController): current_tax_amount += actual_tax_dict[tax.idx] tax.tax_amount = current_tax_amount - tax.base_tax_amount = tax.tax_amount * self.source_exchange_rate + tax.base_tax_amount = current_tax_amount if tax.add_deduct_tax == "Deduct": current_tax_amount *= -1.0 @@ -1082,14 +1085,20 @@ class PaymentEntry(AccountsController): self.get("taxes")[i - 1].total + current_tax_amount, self.precision("total", tax) ) - tax.base_total = tax.total * self.source_exchange_rate + tax.base_total = tax.total if self.payment_type == "Pay": - self.base_total_taxes_and_charges += flt(current_tax_amount / self.source_exchange_rate) - self.total_taxes_and_charges += flt(current_tax_amount / self.target_exchange_rate) - else: - self.base_total_taxes_and_charges += flt(current_tax_amount / self.target_exchange_rate) - self.total_taxes_and_charges += flt(current_tax_amount / self.source_exchange_rate) + if tax.currency != self.paid_to_account_currency: + self.total_taxes_and_charges += flt(current_tax_amount / self.target_exchange_rate) + else: + self.total_taxes_and_charges += current_tax_amount + elif self.payment_type == "Receive": + if tax.currency != self.paid_from_account_currency: + self.total_taxes_and_charges += flt(current_tax_amount / self.source_exchange_rate) + else: + self.total_taxes_and_charges += current_tax_amount + + self.base_total_taxes_and_charges += tax.base_tax_amount if self.get("taxes"): self.paid_amount_after_tax = self.get("taxes")[-1].base_total @@ -1184,6 +1193,8 @@ def get_outstanding_reference_documents(args): ple = qb.DocType("Payment Ledger Entry") common_filter = [] + accounting_dimensions_filter = [] + posting_and_due_date = [] # confirm that Supplier is not blocked if args.get("party_type") == "Supplier": @@ -1200,7 +1211,7 @@ def get_outstanding_reference_documents(args): party_account_currency = get_account_currency(args.get("party_account")) company_currency = frappe.get_cached_value("Company", args.get("company"), "default_currency") - # Get positive outstanding sales /purchase invoices/ Fees + # Get positive outstanding sales /purchase invoices condition = "" if args.get("voucher_type") and args.get("voucher_no"): condition = " and voucher_type={0} and voucher_no={1}".format( @@ -1212,7 +1223,7 @@ def get_outstanding_reference_documents(args): # Add cost center condition if args.get("cost_center"): condition += " and cost_center='%s'" % args.get("cost_center") - common_filter.append(ple.cost_center == args.get("cost_center")) + accounting_dimensions_filter.append(ple.cost_center == args.get("cost_center")) date_fields_dict = { "posting_date": ["from_posting_date", "to_posting_date"], @@ -1224,7 +1235,7 @@ def get_outstanding_reference_documents(args): condition += " and {0} between '{1}' and '{2}'".format( fieldname, args.get(date_fields[0]), args.get(date_fields[1]) ) - common_filter.append(ple[fieldname][args.get(date_fields[0]) : args.get(date_fields[1])]) + posting_and_due_date.append(ple[fieldname][args.get(date_fields[0]) : args.get(date_fields[1])]) if args.get("company"): condition += " and company = {0}".format(frappe.db.escape(args.get("company"))) @@ -1235,8 +1246,10 @@ def get_outstanding_reference_documents(args): args.get("party"), args.get("party_account"), common_filter=common_filter, + posting_date=posting_and_due_date, min_outstanding=args.get("outstanding_amt_greater_than"), max_outstanding=args.get("outstanding_amt_less_than"), + accounting_dimensions=accounting_dimensions_filter, ) outstanding_invoices = split_invoices_based_on_payment_terms(outstanding_invoices) @@ -1297,7 +1310,7 @@ def split_invoices_based_on_payment_terms(outstanding_invoices): d.voucher_type, d.voucher_no, "payment_terms_template" ) if payment_term_template: - allocate_payment_based_on_payment_terms = frappe.db.get_value( + allocate_payment_based_on_payment_terms = frappe.get_cached_value( "Payment Terms Template", payment_term_template, "allocate_payment_based_on_payment_terms" ) if allocate_payment_based_on_payment_terms: @@ -1529,7 +1542,7 @@ def get_account_details(account, date, cost_center=None): { "account_currency": get_account_currency(account), "account_balance": account_balance, - "account_type": frappe.db.get_value("Account", account, "account_type"), + "account_type": frappe.get_cached_value("Account", account, "account_type"), } ) @@ -1595,10 +1608,11 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre elif reference_doctype != "Journal Entry": if not total_amount: if party_account_currency == company_currency: - total_amount = ref_doc.base_grand_total + # for handling cases that don't have multi-currency (base field) + total_amount = ref_doc.get("grand_total") or ref_doc.get("base_grand_total") exchange_rate = 1 else: - total_amount = ref_doc.grand_total + total_amount = ref_doc.get("grand_total") if not exchange_rate: # Get the exchange rate from the original ref doc # or get it based on the posting date of the ref doc. @@ -1609,7 +1623,7 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre if reference_doctype in ("Sales Invoice", "Purchase Invoice"): outstanding_amount = ref_doc.get("outstanding_amount") else: - outstanding_amount = flt(total_amount) - flt(ref_doc.advance_paid) + outstanding_amount = flt(total_amount) - flt(ref_doc.get("advance_paid")) else: # Get the exchange rate based on the posting date of the ref doc. @@ -1627,16 +1641,23 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre @frappe.whitelist() -def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=None): +def get_payment_entry( + dt, dn, party_amount=None, bank_account=None, bank_amount=None, party_type=None, payment_type=None +): reference_doc = None doc = frappe.get_doc(dt, dn) - if dt in ("Sales Order", "Purchase Order") and flt(doc.per_billed, 2) > 0: + if dt in ("Sales Order", "Purchase Order") and flt(doc.per_billed, 2) >= 99.99: frappe.throw(_("Can only make payment against unbilled {0}").format(dt)) - party_type = set_party_type(dt) + if not party_type: + party_type = set_party_type(dt) + party_account = set_party_account(dt, dn, doc, party_type) party_account_currency = set_party_account_currency(dt, party_account, doc) - payment_type = set_payment_type(dt, doc) + + if not payment_type: + payment_type = set_payment_type(dt, doc) + grand_total, outstanding_amount = set_grand_total_and_outstanding_amount( party_amount, dt, party_account_currency, doc ) @@ -1690,9 +1711,9 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount= if doc.doctype == "Purchase Invoice" and doc.invoice_is_blocked(): frappe.msgprint(_("{0} is on hold till {1}").format(doc.name, doc.release_date)) else: - if doc.doctype in ("Sales Invoice", "Purchase Invoice") and frappe.get_value( + if doc.doctype in ("Sales Invoice", "Purchase Invoice") and frappe.get_cached_value( "Payment Terms Template", - {"name": doc.payment_terms_template}, + doc.payment_terms_template, "allocate_payment_based_on_payment_terms", ): @@ -1743,6 +1764,8 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount= pe.setup_party_account_field() pe.set_missing_values() + update_accounting_dimensions(pe, doc) + if party_account and bank: pe.set_exchange_rate(ref_doc=reference_doc) pe.set_amounts() @@ -1760,6 +1783,18 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount= return pe +def update_accounting_dimensions(pe, doc): + """ + Updates accounting dimensions in Payment Entry based on the accounting dimensions in the reference document + """ + from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( + get_accounting_dimensions, + ) + + for dimension in get_accounting_dimensions(): + pe.set(dimension, doc.get(dimension)) + + def get_bank_cash_account(doc, bank_account): bank = get_default_bank_cash_account( doc.company, "Bank", mode_of_payment=doc.get("mode_of_payment"), account=bank_account @@ -1786,8 +1821,6 @@ def set_party_account(dt, dn, doc, party_type): party_account = get_party_account_based_on_invoice_discounting(dn) or doc.debit_to elif dt == "Purchase Invoice": party_account = doc.credit_to - elif dt == "Fees": - party_account = doc.receivable_account else: party_account = get_party_account(party_type, doc.get(party_type.lower()), doc.company) return party_account @@ -1803,8 +1836,7 @@ def set_party_account_currency(dt, party_account, doc): def set_payment_type(dt, doc): if ( - dt == "Sales Order" - or (dt in ("Sales Invoice", "Fees", "Dunning") and doc.outstanding_amount > 0) + dt == "Sales Order" or (dt in ("Sales Invoice", "Dunning") and doc.outstanding_amount > 0) ) or (dt == "Purchase Invoice" and doc.outstanding_amount < 0): payment_type = "Receive" else: @@ -1822,18 +1854,15 @@ def set_grand_total_and_outstanding_amount(party_amount, dt, party_account_curre else: grand_total = doc.rounded_total or doc.grand_total outstanding_amount = doc.outstanding_amount - elif dt == "Fees": - grand_total = doc.grand_total - outstanding_amount = doc.outstanding_amount elif dt == "Dunning": grand_total = doc.grand_total outstanding_amount = doc.grand_total else: if party_account_currency == doc.company_currency: - grand_total = flt(doc.get("base_rounded_total") or doc.base_grand_total) + grand_total = flt(doc.get("base_rounded_total") or doc.get("base_grand_total")) else: - grand_total = flt(doc.get("rounded_total") or doc.grand_total) - outstanding_amount = grand_total - flt(doc.advance_paid) + grand_total = flt(doc.get("rounded_total") or doc.get("grand_total")) + outstanding_amount = doc.get("outstanding_amount") or (grand_total - flt(doc.advance_paid)) return grand_total, outstanding_amount diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index 02627eb007..123b5dfd51 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -4,6 +4,7 @@ import unittest import frappe +from frappe import qb from frappe.tests.utils import FrappeTestCase from frappe.utils import flt, nowdate @@ -722,6 +723,46 @@ class TestPaymentEntry(FrappeTestCase): flt(payment_entry.total_taxes_and_charges, 2), flt(10 / payment_entry.target_exchange_rate, 2) ) + def test_gl_of_multi_currency_payment_with_taxes(self): + payment_entry = create_payment_entry( + party="_Test Supplier USD", paid_to="_Test Payable USD - _TC", save=True + ) + payment_entry.append( + "taxes", + { + "account_head": "_Test Account Service Tax - _TC", + "charge_type": "Actual", + "tax_amount": 100, + "add_deduct_tax": "Add", + "description": "Test", + }, + ) + payment_entry.target_exchange_rate = 80 + payment_entry.received_amount = 12.5 + payment_entry = payment_entry.submit() + gle = qb.DocType("GL Entry") + gl_entries = ( + qb.from_(gle) + .select( + gle.account, + gle.debit, + gle.credit, + gle.debit_in_account_currency, + gle.credit_in_account_currency, + ) + .orderby(gle.account) + .where(gle.voucher_no == payment_entry.name) + .run() + ) + + expected_gl_entries = ( + ("_Test Account Service Tax - _TC", 100.0, 0.0, 100.0, 0.0), + ("_Test Bank - _TC", 0.0, 1100.0, 0.0, 1100.0), + ("_Test Payable USD - _TC", 1000.0, 0.0, 12.5, 0), + ) + + self.assertEqual(gl_entries, expected_gl_entries) + def test_payment_entry_against_onhold_purchase_invoice(self): pi = make_purchase_invoice() diff --git a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json index 8961167f01..3003c68196 100644 --- a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json +++ b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json @@ -25,7 +25,8 @@ "in_list_view": 1, "label": "Type", "options": "DocType", - "reqd": 1 + "reqd": 1, + "search_index": 1 }, { "columns": 2, @@ -35,7 +36,8 @@ "in_list_view": 1, "label": "Name", "options": "reference_doctype", - "reqd": 1 + "reqd": 1, + "search_index": 1 }, { "fieldname": "due_date", @@ -104,7 +106,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-09-26 17:06:55.597389", + "modified": "2022-12-12 12:31:44.919895", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Entry Reference", @@ -113,5 +115,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account.js b/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account.js index 8f09bc3691..aff067eab8 100644 --- a/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account.js +++ b/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account.js @@ -3,6 +3,7 @@ frappe.ui.form.on('Payment Gateway Account', { refresh(frm) { + erpnext.utils.check_payments_app(); if(!frm.doc.__islocal) { frm.set_df_property('payment_gateway', 'read_only', 1); } diff --git a/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account.py b/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account.py index ab47b6151c..791de2570a 100644 --- a/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account.py +++ b/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account.py @@ -11,7 +11,7 @@ class PaymentGatewayAccount(Document): self.name = self.payment_gateway + " - " + self.currency def validate(self): - self.currency = frappe.db.get_value("Account", self.payment_account, "account_currency") + self.currency = frappe.get_cached_value("Account", self.payment_account, "account_currency") self.update_default_payment_gateway() self.set_as_default_if_not_set() diff --git a/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json index 4596b00fc1..22842cec0f 100644 --- a/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json +++ b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json @@ -22,7 +22,8 @@ "amount", "account_currency", "amount_in_account_currency", - "delinked" + "delinked", + "remarks" ], "fields": [ { @@ -136,12 +137,17 @@ "fieldtype": "Link", "label": "Finance Book", "options": "Finance Book" + }, + { + "fieldname": "remarks", + "fieldtype": "Text", + "label": "Remarks" } ], "in_create": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2022-07-11 09:13:54.379168", + "modified": "2022-08-22 15:32:56.629430", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Ledger Entry", diff --git a/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.py b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.py index bcbcb670fa..58691ab8d4 100644 --- a/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.py +++ b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.py @@ -97,7 +97,7 @@ class PaymentLedgerEntry(Document): ) def validate_dimensions_for_pl_and_bs(self): - account_type = frappe.db.get_value("Account", self.account, "report_type") + account_type = frappe.get_cached_value("Account", self.account, "report_type") for dimension in get_checks_for_pl_and_bs_accounts(): if ( diff --git a/erpnext/accounts/doctype/payment_ledger_entry/test_payment_ledger_entry.py b/erpnext/accounts/doctype/payment_ledger_entry/test_payment_ledger_entry.py index a71b19e092..fc6dbba7e7 100644 --- a/erpnext/accounts/doctype/payment_ledger_entry/test_payment_ledger_entry.py +++ b/erpnext/accounts/doctype/payment_ledger_entry/test_payment_ledger_entry.py @@ -3,12 +3,13 @@ import frappe from frappe import qb -from frappe.tests.utils import FrappeTestCase +from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import nowdate from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice +from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order from erpnext.stock.doctype.item.test_item import create_item @@ -127,6 +128,25 @@ class TestPaymentLedgerEntry(FrappeTestCase): payment.posting_date = posting_date return payment + def create_sales_order( + self, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False + ): + so = make_sales_order( + company=self.company, + transaction_date=posting_date, + customer=self.customer, + item_code=self.item, + cost_center=self.cost_center, + warehouse=self.warehouse, + debit_to=self.debit_to, + currency="INR", + qty=qty, + rate=100, + do_not_save=do_not_save, + do_not_submit=do_not_submit, + ) + return so + def clear_old_entries(self): doctype_list = [ "GL Entry", @@ -406,3 +426,89 @@ class TestPaymentLedgerEntry(FrappeTestCase): ] self.assertEqual(pl_entries_for_crnote[0], expected_values[0]) self.assertEqual(pl_entries_for_crnote[1], expected_values[1]) + + @change_settings( + "Accounts Settings", + {"unlink_payment_on_cancellation_of_invoice": 1, "delete_linked_ledger_entries": 1}, + ) + def test_multi_payment_unlink_on_invoice_cancellation(self): + transaction_date = nowdate() + amount = 100 + si = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date) + + for amt in [40, 40, 20]: + # payment 1 + pe = get_payment_entry(si.doctype, si.name) + pe.paid_amount = amt + pe.get("references")[0].allocated_amount = amt + pe = pe.save().submit() + + si.reload() + si.cancel() + + entries = frappe.db.get_list( + "Payment Ledger Entry", + filters={"against_voucher_type": si.doctype, "against_voucher_no": si.name, "delinked": 0}, + ) + self.assertEqual(entries, []) + + # with references removed, deletion should be possible + si.delete() + self.assertRaises(frappe.DoesNotExistError, frappe.get_doc, si.doctype, si.name) + + @change_settings( + "Accounts Settings", + {"unlink_payment_on_cancellation_of_invoice": 1, "delete_linked_ledger_entries": 1}, + ) + def test_multi_je_unlink_on_invoice_cancellation(self): + transaction_date = nowdate() + amount = 100 + si = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date) + + # multiple JE's against invoice + for amt in [40, 40, 20]: + je1 = self.create_journal_entry( + self.income_account, self.debit_to, amt, posting_date=transaction_date + ) + je1.get("accounts")[1].party_type = "Customer" + je1.get("accounts")[1].party = self.customer + je1.get("accounts")[1].reference_type = si.doctype + je1.get("accounts")[1].reference_name = si.name + je1 = je1.save().submit() + + si.reload() + si.cancel() + + entries = frappe.db.get_list( + "Payment Ledger Entry", + filters={"against_voucher_type": si.doctype, "against_voucher_no": si.name, "delinked": 0}, + ) + self.assertEqual(entries, []) + + # with references removed, deletion should be possible + si.delete() + self.assertRaises(frappe.DoesNotExistError, frappe.get_doc, si.doctype, si.name) + + @change_settings( + "Accounts Settings", + {"unlink_payment_on_cancellation_of_invoice": 1, "delete_linked_ledger_entries": 1}, + ) + def test_advance_payment_unlink_on_order_cancellation(self): + transaction_date = nowdate() + amount = 100 + so = self.create_sales_order(qty=1, rate=amount, posting_date=transaction_date).save().submit() + + pe = get_payment_entry(so.doctype, so.name).save().submit() + + so.reload() + so.cancel() + + entries = frappe.db.get_list( + "Payment Ledger Entry", + filters={"against_voucher_type": so.doctype, "against_voucher_no": so.name, "delinked": 0}, + ) + self.assertEqual(entries, []) + + # with references removed, deletion should be possible + so.delete() + self.assertRaises(frappe.DoesNotExistError, frappe.get_doc, so.doctype, so.name) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js index 0b334ae076..d986f32066 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js @@ -170,7 +170,7 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo } reconcile() { - var show_dialog = this.frm.doc.allocation.filter(d => d.difference_amount && !d.difference_account); + var show_dialog = this.frm.doc.allocation.filter(d => d.difference_amount); if (show_dialog && show_dialog.length) { @@ -179,8 +179,12 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo title: __("Select Difference Account"), fields: [ { - fieldname: "allocation", fieldtype: "Table", label: __("Allocation"), - data: this.data, in_place_edit: true, + fieldname: "allocation", + fieldtype: "Table", + label: __("Allocation"), + data: this.data, + in_place_edit: true, + cannot_add_rows: true, get_data: () => { return this.data; }, @@ -218,6 +222,10 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo read_only: 1 }] }, + { + fieldtype: 'HTML', + options: " New Journal Entry will be posted for the difference amount " + } ], primary_action: () => { const args = dialog.get_values()["allocation"]; @@ -234,7 +242,7 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo }); this.frm.doc.allocation.forEach(d => { - if (d.difference_amount && !d.difference_account) { + if (d.difference_amount) { dialog.fields_dict.allocation.df.data.push({ 'docname': d.name, 'reference_name': d.reference_name, diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 5ed34d34a3..e3d9c26b2d 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -22,6 +22,8 @@ class PaymentReconciliation(Document): def __init__(self, *args, **kwargs): super(PaymentReconciliation, self).__init__(*args, **kwargs) self.common_filter_conditions = [] + self.accounting_dimension_filter_conditions = [] + self.ple_posting_date_filter = [] @frappe.whitelist() def get_unreconciled_entries(self): @@ -67,6 +69,10 @@ class PaymentReconciliation(Document): def get_jv_entries(self): condition = self.get_conditions() + + if self.get("cost_center"): + condition += f" and t2.cost_center = '{self.cost_center}' " + dr_or_cr = ( "credit_in_account_currency" if erpnext.get_party_account_type(self.party_type) == "Receivable" @@ -77,12 +83,13 @@ class PaymentReconciliation(Document): "t2.against_account like %(bank_cash_account)s" if self.bank_cash_account else "1=1" ) + # nosemgrep journal_entries = frappe.db.sql( """ select "Journal Entry" as reference_type, t1.name as reference_name, t1.posting_date, t1.remark as remarks, t2.name as reference_row, - {dr_or_cr} as amount, t2.is_advance, + {dr_or_cr} as amount, t2.is_advance, t2.exchange_rate, t2.account_currency as currency from `tabJournal Entry` t1, `tabJournal Entry Account` t2 @@ -150,6 +157,7 @@ class PaymentReconciliation(Document): return_outstanding = ple_query.get_voucher_outstandings( vouchers=return_invoices, common_filter=self.common_filter_conditions, + posting_date=self.ple_posting_date_filter, min_outstanding=-(self.minimum_payment_amount) if self.minimum_payment_amount else None, max_outstanding=-(self.maximum_payment_amount) if self.maximum_payment_amount else None, get_payments=True, @@ -187,8 +195,10 @@ class PaymentReconciliation(Document): self.party, self.receivable_payable_account, common_filter=self.common_filter_conditions, + posting_date=self.ple_posting_date_filter, min_outstanding=self.minimum_invoice_amount if self.minimum_invoice_amount else None, max_outstanding=self.maximum_invoice_amount if self.maximum_invoice_amount else None, + accounting_dimensions=self.accounting_dimension_filter_conditions, ) if self.invoice_limit: @@ -209,9 +219,26 @@ class PaymentReconciliation(Document): inv.currency = entry.get("currency") inv.outstanding_amount = flt(entry.get("outstanding_amount")) + def get_difference_amount(self, payment_entry, invoice, allocated_amount): + difference_amount = 0 + if invoice.get("exchange_rate") and payment_entry.get("exchange_rate", 1) != invoice.get( + "exchange_rate", 1 + ): + allocated_amount_in_ref_rate = payment_entry.get("exchange_rate", 1) * allocated_amount + allocated_amount_in_inv_rate = invoice.get("exchange_rate", 1) * allocated_amount + difference_amount = allocated_amount_in_ref_rate - allocated_amount_in_inv_rate + + return difference_amount + @frappe.whitelist() def allocate_entries(self, args): self.validate_entries() + + invoice_exchange_map = self.get_invoice_exchange_map(args.get("invoices"), args.get("payments")) + default_exchange_gain_loss_account = frappe.get_cached_value( + "Company", self.company, "exchange_gain_loss_account" + ) + entries = [] for pay in args.get("payments"): pay.update({"unreconciled_amount": pay.get("amount")}) @@ -224,12 +251,22 @@ class PaymentReconciliation(Document): res = self.get_allocated_entry(pay, inv, pay["amount"]) inv["outstanding_amount"] = flt(inv.get("outstanding_amount")) - flt(pay.get("amount")) pay["amount"] = 0 + + inv["exchange_rate"] = invoice_exchange_map.get(inv.get("invoice_number")) + if pay.get("reference_type") in ["Sales Invoice", "Purchase Invoice"]: + pay["exchange_rate"] = invoice_exchange_map.get(pay.get("reference_name")) + + res.difference_amount = self.get_difference_amount(pay, inv, res["allocated_amount"]) + res.difference_account = default_exchange_gain_loss_account + res.exchange_rate = inv.get("exchange_rate") + if pay.get("amount") == 0: entries.append(res) break elif inv.get("outstanding_amount") == 0: entries.append(res) continue + else: break @@ -251,6 +288,7 @@ class PaymentReconciliation(Document): "amount": pay.get("amount"), "allocated_amount": allocated_amount, "difference_amount": pay.get("difference_amount"), + "currency": inv.get("currency"), } ) @@ -273,7 +311,11 @@ class PaymentReconciliation(Document): else: reconciled_entry = entry_list - reconciled_entry.append(self.get_payment_details(row, dr_or_cr)) + payment_details = self.get_payment_details(row, dr_or_cr) + reconciled_entry.append(payment_details) + + if payment_details.difference_amount: + self.make_difference_entry(payment_details) if entry_list: reconcile_against_document(entry_list) @@ -284,6 +326,57 @@ class PaymentReconciliation(Document): msgprint(_("Successfully Reconciled")) self.get_unreconciled_entries() + def make_difference_entry(self, row): + journal_entry = frappe.new_doc("Journal Entry") + journal_entry.voucher_type = "Exchange Gain Or Loss" + journal_entry.company = self.company + journal_entry.posting_date = nowdate() + journal_entry.multi_currency = 1 + + party_account_currency = frappe.get_cached_value( + "Account", self.receivable_payable_account, "account_currency" + ) + difference_account_currency = frappe.get_cached_value( + "Account", row.difference_account, "account_currency" + ) + + # Account Currency has balance + dr_or_cr = "debit" if self.party_type == "Customer" else "credit" + reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit" + + journal_account = frappe._dict( + { + "account": self.receivable_payable_account, + "party_type": self.party_type, + "party": self.party, + "account_currency": party_account_currency, + "exchange_rate": 0, + "cost_center": erpnext.get_default_cost_center(self.company), + "reference_type": row.against_voucher_type, + "reference_name": row.against_voucher, + dr_or_cr: flt(row.difference_amount), + dr_or_cr + "_in_account_currency": 0, + } + ) + + journal_entry.append("accounts", journal_account) + + journal_account = frappe._dict( + { + "account": row.difference_account, + "account_currency": difference_account_currency, + "exchange_rate": 1, + "cost_center": erpnext.get_default_cost_center(self.company), + reverse_dr_or_cr + "_in_account_currency": flt(row.difference_amount), + reverse_dr_or_cr: flt(row.difference_amount), + } + ) + + journal_entry.append("accounts", journal_account) + + journal_entry.save() + journal_entry.submit() + def get_payment_details(self, row, dr_or_cr): return frappe._dict( { @@ -293,6 +386,7 @@ class PaymentReconciliation(Document): "against_voucher_type": row.get("invoice_type"), "against_voucher": row.get("invoice_number"), "account": self.receivable_payable_account, + "exchange_rate": row.get("exchange_rate"), "party_type": self.party_type, "party": self.party, "is_advance": row.get("is_advance"), @@ -317,6 +411,49 @@ class PaymentReconciliation(Document): if not self.get("payments"): frappe.throw(_("No records found in the Payments table")) + def get_invoice_exchange_map(self, invoices, payments): + sales_invoices = [ + d.get("invoice_number") for d in invoices if d.get("invoice_type") == "Sales Invoice" + ] + + sales_invoices.extend( + [d.get("reference_name") for d in payments if d.get("reference_type") == "Sales Invoice"] + ) + purchase_invoices = [ + d.get("invoice_number") for d in invoices if d.get("invoice_type") == "Purchase Invoice" + ] + purchase_invoices.extend( + [d.get("reference_name") for d in payments if d.get("reference_type") == "Purchase Invoice"] + ) + + invoice_exchange_map = frappe._dict() + + if sales_invoices: + sales_invoice_map = frappe._dict( + frappe.db.get_all( + "Sales Invoice", + filters={"name": ("in", sales_invoices)}, + fields=["name", "conversion_rate"], + as_list=1, + ) + ) + + invoice_exchange_map.update(sales_invoice_map) + + if purchase_invoices: + purchase_invoice_map = frappe._dict( + frappe.db.get_all( + "Purchase Invoice", + filters={"name": ("in", purchase_invoices)}, + fields=["name", "conversion_rate"], + as_list=1, + ) + ) + + invoice_exchange_map.update(purchase_invoice_map) + + return invoice_exchange_map + def validate_allocation(self): unreconciled_invoices = frappe._dict() @@ -350,24 +487,26 @@ class PaymentReconciliation(Document): def build_qb_filter_conditions(self, get_invoices=False, get_return_invoices=False): self.common_filter_conditions.clear() + self.accounting_dimension_filter_conditions.clear() + self.ple_posting_date_filter.clear() ple = qb.DocType("Payment Ledger Entry") self.common_filter_conditions.append(ple.company == self.company) if self.get("cost_center") and (get_invoices or get_return_invoices): - self.common_filter_conditions.append(ple.cost_center == self.cost_center) + self.accounting_dimension_filter_conditions.append(ple.cost_center == self.cost_center) if get_invoices: if self.from_invoice_date: - self.common_filter_conditions.append(ple.posting_date.gte(self.from_invoice_date)) + self.ple_posting_date_filter.append(ple.posting_date.gte(self.from_invoice_date)) if self.to_invoice_date: - self.common_filter_conditions.append(ple.posting_date.lte(self.to_invoice_date)) + self.ple_posting_date_filter.append(ple.posting_date.lte(self.to_invoice_date)) elif get_return_invoices: if self.from_payment_date: - self.common_filter_conditions.append(ple.posting_date.gte(self.from_payment_date)) + self.ple_posting_date_filter.append(ple.posting_date.gte(self.from_payment_date)) if self.to_payment_date: - self.common_filter_conditions.append(ple.posting_date.lte(self.to_payment_date)) + self.ple_posting_date_filter.append(ple.posting_date.lte(self.to_payment_date)) def get_conditions(self, get_payments=False): condition = " and company = '{0}' ".format(self.company) diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py index 625382a3e9..f9dda0593b 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py @@ -6,8 +6,10 @@ import unittest import frappe from frappe import qb from frappe.tests.utils import FrappeTestCase -from frappe.utils import add_days, nowdate +from frappe.utils import add_days, flt, nowdate +from erpnext import get_default_cost_center +from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.party import get_party_account @@ -20,6 +22,7 @@ class TestPaymentReconciliation(FrappeTestCase): self.create_item() self.create_customer() self.create_account() + self.create_cost_center() self.clear_old_entries() def tearDown(self): @@ -72,33 +75,11 @@ class TestPaymentReconciliation(FrappeTestCase): self.item = item if isinstance(item, str) else item.item_code def create_customer(self): - if frappe.db.exists("Customer", "_Test PR Customer"): - self.customer = "_Test PR Customer" - else: - customer = frappe.new_doc("Customer") - customer.customer_name = "_Test PR Customer" - customer.type = "Individual" - customer.save() - self.customer = customer.name - - if frappe.db.exists("Customer", "_Test PR Customer 2"): - self.customer2 = "_Test PR Customer 2" - else: - customer = frappe.new_doc("Customer") - customer.customer_name = "_Test PR Customer 2" - customer.type = "Individual" - customer.save() - self.customer2 = customer.name - - if frappe.db.exists("Customer", "_Test PR Customer 3"): - self.customer3 = "_Test PR Customer 3" - else: - customer = frappe.new_doc("Customer") - customer.customer_name = "_Test PR Customer 3" - customer.type = "Individual" - customer.default_currency = "EUR" - customer.save() - self.customer3 = customer.name + self.customer = make_customer("_Test PR Customer") + self.customer2 = make_customer("_Test PR Customer 2") + self.customer3 = make_customer("_Test PR Customer 3", "EUR") + self.customer4 = make_customer("_Test PR Customer 4", "EUR") + self.customer5 = make_customer("_Test PR Customer 5", "EUR") def create_account(self): account_name = "Debtors EUR" @@ -216,6 +197,22 @@ class TestPaymentReconciliation(FrappeTestCase): ) return je + def create_cost_center(self): + # Setup cost center + cc_name = "Sub" + + self.main_cc = frappe.get_doc("Cost Center", get_default_cost_center(self.company)) + + cc_exists = frappe.db.get_list("Cost Center", filters={"cost_center_name": cc_name}) + if cc_exists: + self.sub_cc = frappe.get_doc("Cost Center", cc_exists[0].name) + else: + sub_cc = frappe.new_doc("Cost Center") + sub_cc.cost_center_name = "Sub" + sub_cc.parent_cost_center = self.main_cc.parent_cost_center + sub_cc.company = self.main_cc.company + self.sub_cc = sub_cc.save() + def test_filter_min_max(self): # check filter condition minimum and maximum amount self.create_sales_invoice(qty=1, rate=300) @@ -283,6 +280,41 @@ class TestPaymentReconciliation(FrappeTestCase): self.assertEqual(len(pr.get("invoices")), 2) self.assertEqual(len(pr.get("payments")), 2) + def test_filter_posting_date_case2(self): + """ + Posting date should not affect outstanding amount calculation + """ + + from_date = add_days(nowdate(), -30) + to_date = nowdate() + self.create_payment_entry(amount=25, posting_date=from_date).submit() + self.create_sales_invoice(rate=25, qty=1, posting_date=to_date) + + pr = self.create_payment_reconciliation() + pr.from_invoice_date = pr.from_payment_date = from_date + pr.to_invoice_date = pr.to_payment_date = to_date + pr.get_unreconciled_entries() + + self.assertEqual(len(pr.invoices), 1) + self.assertEqual(len(pr.payments), 1) + + invoices = [x.as_dict() for x in pr.invoices] + payments = [x.as_dict() for x in pr.payments] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + pr.reconcile() + + pr.get_unreconciled_entries() + + self.assertEqual(len(pr.invoices), 0) + self.assertEqual(len(pr.payments), 0) + + pr.from_invoice_date = pr.from_payment_date = to_date + pr.to_invoice_date = pr.to_payment_date = to_date + + pr.get_unreconciled_entries() + + self.assertEqual(len(pr.invoices), 0) + def test_filter_invoice_limit(self): # check filter condition - invoice limit transaction_date = nowdate() @@ -441,6 +473,11 @@ class TestPaymentReconciliation(FrappeTestCase): invoices = [x.as_dict() for x in pr.get("invoices")] payments = [x.as_dict() for x in pr.get("payments")] pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + + # Cr Note and Invoice are of the same currency. There shouldn't any difference amount. + for row in pr.allocation: + self.assertEqual(flt(row.get("difference_amount")), 0.0) + pr.reconcile() pr.get_unreconciled_entries() @@ -474,6 +511,11 @@ class TestPaymentReconciliation(FrappeTestCase): payments = [x.as_dict() for x in pr.get("payments")] pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) pr.allocation[0].allocated_amount = allocated_amount + + # Cr Note and Invoice are of the same currency. There shouldn't any difference amount. + for row in pr.allocation: + self.assertEqual(flt(row.get("difference_amount")), 0.0) + pr.reconcile() # assert outstanding @@ -543,3 +585,255 @@ class TestPaymentReconciliation(FrappeTestCase): self.assertEqual(len(pr.payments), 1) self.assertEqual(pr.payments[0].amount, amount) self.assertEqual(pr.payments[0].currency, "EUR") + + def test_difference_amount_via_journal_entry(self): + # Make Sale Invoice + si = self.create_sales_invoice( + qty=1, rate=100, posting_date=nowdate(), do_not_save=True, do_not_submit=True + ) + si.customer = self.customer4 + si.currency = "EUR" + si.conversion_rate = 85 + si.debit_to = self.debtors_eur + si.save().submit() + + # Make payment using Journal Entry + je1 = self.create_journal_entry("HDFC - _PR", self.debtors_eur, 100, nowdate()) + je1.multi_currency = 1 + je1.accounts[0].exchange_rate = 1 + je1.accounts[0].credit_in_account_currency = 0 + je1.accounts[0].credit = 0 + je1.accounts[0].debit_in_account_currency = 8000 + je1.accounts[0].debit = 8000 + je1.accounts[1].party_type = "Customer" + je1.accounts[1].party = self.customer4 + je1.accounts[1].exchange_rate = 80 + je1.accounts[1].credit_in_account_currency = 100 + je1.accounts[1].credit = 8000 + je1.accounts[1].debit_in_account_currency = 0 + je1.accounts[1].debit = 0 + je1.save() + je1.submit() + + je2 = self.create_journal_entry("HDFC - _PR", self.debtors_eur, 200, nowdate()) + je2.multi_currency = 1 + je2.accounts[0].exchange_rate = 1 + je2.accounts[0].credit_in_account_currency = 0 + je2.accounts[0].credit = 0 + je2.accounts[0].debit_in_account_currency = 16000 + je2.accounts[0].debit = 16000 + je2.accounts[1].party_type = "Customer" + je2.accounts[1].party = self.customer4 + je2.accounts[1].exchange_rate = 80 + je2.accounts[1].credit_in_account_currency = 200 + je1.accounts[1].credit = 16000 + je1.accounts[1].debit_in_account_currency = 0 + je1.accounts[1].debit = 0 + je2.save() + je2.submit() + + pr = self.create_payment_reconciliation() + pr.party = self.customer4 + pr.receivable_payable_account = self.debtors_eur + pr.get_unreconciled_entries() + + self.assertEqual(len(pr.invoices), 1) + self.assertEqual(len(pr.payments), 2) + + # Test exact payment allocation + invoices = [x.as_dict() for x in pr.invoices] + payments = [pr.payments[0].as_dict()] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + + self.assertEqual(pr.allocation[0].allocated_amount, 100) + self.assertEqual(pr.allocation[0].difference_amount, -500) + + # Test partial payment allocation (with excess payment entry) + pr.set("allocation", []) + pr.get_unreconciled_entries() + invoices = [x.as_dict() for x in pr.invoices] + payments = [pr.payments[1].as_dict()] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + pr.allocation[0].difference_account = "Exchange Gain/Loss - _PR" + + self.assertEqual(pr.allocation[0].allocated_amount, 100) + self.assertEqual(pr.allocation[0].difference_amount, -500) + + # Check if difference journal entry gets generated for difference amount after reconciliation + pr.reconcile() + total_debit_amount = frappe.db.get_all( + "Journal Entry Account", + {"account": self.debtors_eur, "docstatus": 1, "reference_name": si.name}, + "sum(debit) as amount", + group_by="reference_name", + )[0].amount + + self.assertEqual(flt(total_debit_amount, 2), -500) + + def test_difference_amount_via_payment_entry(self): + # Make Sale Invoice + si = self.create_sales_invoice( + qty=1, rate=100, posting_date=nowdate(), do_not_save=True, do_not_submit=True + ) + si.customer = self.customer5 + si.currency = "EUR" + si.conversion_rate = 85 + si.debit_to = self.debtors_eur + si.save().submit() + + # Make payment using Payment Entry + pe1 = create_payment_entry( + company=self.company, + payment_type="Receive", + party_type="Customer", + party=self.customer5, + paid_from=self.debtors_eur, + paid_to=self.bank, + paid_amount=100, + ) + + pe1.source_exchange_rate = 80 + pe1.received_amount = 8000 + pe1.save() + pe1.submit() + + pe2 = create_payment_entry( + company=self.company, + payment_type="Receive", + party_type="Customer", + party=self.customer5, + paid_from=self.debtors_eur, + paid_to=self.bank, + paid_amount=200, + ) + + pe2.source_exchange_rate = 80 + pe2.received_amount = 16000 + pe2.save() + pe2.submit() + + pr = self.create_payment_reconciliation() + pr.party = self.customer5 + pr.receivable_payable_account = self.debtors_eur + pr.get_unreconciled_entries() + + self.assertEqual(len(pr.invoices), 1) + self.assertEqual(len(pr.payments), 2) + + invoices = [x.as_dict() for x in pr.invoices] + payments = [pr.payments[0].as_dict()] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + + self.assertEqual(pr.allocation[0].allocated_amount, 100) + self.assertEqual(pr.allocation[0].difference_amount, -500) + + pr.set("allocation", []) + pr.get_unreconciled_entries() + invoices = [x.as_dict() for x in pr.invoices] + payments = [pr.payments[1].as_dict()] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + + self.assertEqual(pr.allocation[0].allocated_amount, 100) + self.assertEqual(pr.allocation[0].difference_amount, -500) + + def test_differing_cost_center_on_invoice_and_payment(self): + """ + Cost Center filter should not affect outstanding amount calculation + """ + + si = self.create_sales_invoice(qty=1, rate=100, do_not_submit=True) + si.cost_center = self.main_cc.name + si.submit() + pr = get_payment_entry(si.doctype, si.name) + pr.cost_center = self.sub_cc.name + pr = pr.save().submit() + + pr = self.create_payment_reconciliation() + pr.cost_center = self.main_cc.name + + pr.get_unreconciled_entries() + + # check PR tool output + self.assertEqual(len(pr.get("invoices")), 0) + self.assertEqual(len(pr.get("payments")), 0) + + def test_cost_center_filter_on_vouchers(self): + """ + Test Cost Center filter is applied on Invoices, Payment Entries and Journals + """ + transaction_date = nowdate() + rate = 100 + + # 'Main - PR' Cost Center + si1 = self.create_sales_invoice( + qty=1, rate=rate, posting_date=transaction_date, do_not_submit=True + ) + si1.cost_center = self.main_cc.name + si1.submit() + + pe1 = self.create_payment_entry(posting_date=transaction_date, amount=rate) + pe1.cost_center = self.main_cc.name + pe1 = pe1.save().submit() + + je1 = self.create_journal_entry(self.bank, self.debit_to, 100, transaction_date) + je1.accounts[0].cost_center = self.main_cc.name + je1.accounts[1].cost_center = self.main_cc.name + je1.accounts[1].party_type = "Customer" + je1.accounts[1].party = self.customer + je1 = je1.save().submit() + + # 'Sub - PR' Cost Center + si2 = self.create_sales_invoice( + qty=1, rate=rate, posting_date=transaction_date, do_not_submit=True + ) + si2.cost_center = self.sub_cc.name + si2.submit() + + pe2 = self.create_payment_entry(posting_date=transaction_date, amount=rate) + pe2.cost_center = self.sub_cc.name + pe2 = pe2.save().submit() + + je2 = self.create_journal_entry(self.bank, self.debit_to, 100, transaction_date) + je2.accounts[0].cost_center = self.sub_cc.name + je2.accounts[1].cost_center = self.sub_cc.name + je2.accounts[1].party_type = "Customer" + je2.accounts[1].party = self.customer + je2 = je2.save().submit() + + pr = self.create_payment_reconciliation() + pr.cost_center = self.main_cc.name + + pr.get_unreconciled_entries() + + # check PR tool output + self.assertEqual(len(pr.get("invoices")), 1) + self.assertEqual(pr.get("invoices")[0].get("invoice_number"), si1.name) + self.assertEqual(len(pr.get("payments")), 2) + payment_vouchers = [x.get("reference_name") for x in pr.get("payments")] + self.assertCountEqual(payment_vouchers, [pe1.name, je1.name]) + + # Change cost center + pr.cost_center = self.sub_cc.name + + pr.get_unreconciled_entries() + + # check PR tool output + self.assertEqual(len(pr.get("invoices")), 1) + self.assertEqual(pr.get("invoices")[0].get("invoice_number"), si2.name) + self.assertEqual(len(pr.get("payments")), 2) + payment_vouchers = [x.get("reference_name") for x in pr.get("payments")] + self.assertCountEqual(payment_vouchers, [je2.name, pe2.name]) + + +def make_customer(customer_name, currency=None): + if not frappe.db.exists("Customer", customer_name): + customer = frappe.new_doc("Customer") + customer.customer_name = customer_name + customer.type = "Individual" + + if currency: + customer.default_currency = currency + customer.save() + return customer.name + else: + return customer_name diff --git a/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json b/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json index 6a21692c6a..0f7e47acfe 100644 --- a/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json +++ b/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json @@ -20,7 +20,9 @@ "section_break_5", "difference_amount", "column_break_7", - "difference_account" + "difference_account", + "exchange_rate", + "currency" ], "fields": [ { @@ -37,7 +39,7 @@ "fieldtype": "Currency", "in_list_view": 1, "label": "Allocated Amount", - "options": "Currency", + "options": "currency", "reqd": 1 }, { @@ -112,7 +114,7 @@ "fieldtype": "Currency", "hidden": 1, "label": "Unreconciled Amount", - "options": "Currency", + "options": "currency", "read_only": 1 }, { @@ -120,7 +122,7 @@ "fieldtype": "Currency", "hidden": 1, "label": "Amount", - "options": "Currency", + "options": "currency", "read_only": 1 }, { @@ -129,11 +131,24 @@ "hidden": 1, "label": "Reference Row", "read_only": 1 + }, + { + "fieldname": "currency", + "fieldtype": "Link", + "hidden": 1, + "label": "Currency", + "options": "Currency" + }, + { + "fieldname": "exchange_rate", + "fieldtype": "Float", + "label": "Exchange Rate", + "read_only": 1 } ], "istable": 1, "links": [], - "modified": "2021-10-06 11:48:59.616562", + "modified": "2022-12-24 21:01:14.882747", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Reconciliation Allocation", @@ -141,5 +156,6 @@ "permissions": [], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/accounts/doctype/payment_reconciliation_invoice/payment_reconciliation_invoice.json b/erpnext/accounts/doctype/payment_reconciliation_invoice/payment_reconciliation_invoice.json index 00c9e1240c..c4dbd7e844 100644 --- a/erpnext/accounts/doctype/payment_reconciliation_invoice/payment_reconciliation_invoice.json +++ b/erpnext/accounts/doctype/payment_reconciliation_invoice/payment_reconciliation_invoice.json @@ -11,7 +11,8 @@ "col_break1", "amount", "outstanding_amount", - "currency" + "currency", + "exchange_rate" ], "fields": [ { @@ -62,11 +63,17 @@ "hidden": 1, "label": "Currency", "options": "Currency" + }, + { + "fieldname": "exchange_rate", + "fieldtype": "Float", + "hidden": 1, + "label": "Exchange Rate" } ], "istable": 1, "links": [], - "modified": "2021-08-24 22:42:40.923179", + "modified": "2022-11-08 18:18:02.502149", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Reconciliation Invoice", @@ -75,5 +82,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/accounts/doctype/payment_reconciliation_payment/payment_reconciliation_payment.json b/erpnext/accounts/doctype/payment_reconciliation_payment/payment_reconciliation_payment.json index add07e870d..d300ea97ab 100644 --- a/erpnext/accounts/doctype/payment_reconciliation_payment/payment_reconciliation_payment.json +++ b/erpnext/accounts/doctype/payment_reconciliation_payment/payment_reconciliation_payment.json @@ -15,7 +15,8 @@ "difference_amount", "sec_break1", "remark", - "currency" + "currency", + "exchange_rate" ], "fields": [ { @@ -91,11 +92,17 @@ "label": "Difference Amount", "options": "currency", "read_only": 1 + }, + { + "fieldname": "exchange_rate", + "fieldtype": "Float", + "hidden": 1, + "label": "Exchange Rate" } ], "istable": 1, "links": [], - "modified": "2021-08-30 10:51:48.140062", + "modified": "2022-11-08 18:18:36.268760", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Reconciliation Payment", @@ -103,5 +110,6 @@ "permissions": [], "quick_entry": 1, "sort_field": "modified", - "sort_order": "DESC" + "sort_order": "DESC", + "states": [] } \ No newline at end of file diff --git a/erpnext/accounts/doctype/payment_request/payment_request.js b/erpnext/accounts/doctype/payment_request/payment_request.js index 901ef1987b..e913912028 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.js +++ b/erpnext/accounts/doctype/payment_request/payment_request.js @@ -42,7 +42,7 @@ frappe.ui.form.on("Payment Request", "refresh", function(frm) { }); } - if(!frm.doc.payment_gateway_account && frm.doc.status == "Initiated") { + if((!frm.doc.payment_gateway_account || frm.doc.payment_request_type == "Outward") && frm.doc.status == "Initiated") { frm.add_custom_button(__('Create Payment Entry'), function(){ frappe.call({ method: "erpnext.accounts.doctype.payment_request.payment_request.make_payment_entry", diff --git a/erpnext/accounts/doctype/payment_request/payment_request.json b/erpnext/accounts/doctype/payment_request/payment_request.json index 2ee356aaf4..381f3fb531 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.json +++ b/erpnext/accounts/doctype/payment_request/payment_request.json @@ -32,6 +32,10 @@ "iban", "branch_code", "swift_number", + "accounting_dimensions_section", + "cost_center", + "dimension_col_break", + "project", "recipient_and_message", "print_format", "email_to", @@ -186,8 +190,10 @@ { "fetch_from": "bank_account.bank", "fieldname": "bank", - "fieldtype": "Read Only", - "label": "Bank" + "fieldtype": "Link", + "label": "Bank", + "options": "Bank", + "read_only": 1 }, { "fetch_from": "bank_account.bank_account_no", @@ -360,16 +366,39 @@ "label": "Payment Channel", "options": "\nEmail\nPhone", "read_only": 1 + }, + { + "collapsible": 1, + "fieldname": "accounting_dimensions_section", + "fieldtype": "Section Break", + "label": "Accounting Dimensions" + }, + { + "fieldname": "cost_center", + "fieldtype": "Link", + "label": "Cost Center", + "options": "Cost Center" + }, + { + "fieldname": "dimension_col_break", + "fieldtype": "Column Break" + }, + { + "fieldname": "project", + "fieldtype": "Link", + "label": "Project", + "options": "Project" } ], "in_create": 1, "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-09-18 12:24:14.178853", + "modified": "2022-12-21 16:56:40.115737", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Request", + "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ { @@ -401,5 +430,6 @@ } ], "sort_field": "modified", - "sort_order": "DESC" + "sort_order": "DESC", + "states": [] } \ No newline at end of file diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index 29c497854c..2f43914c45 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -9,8 +9,10 @@ from frappe import _ from frappe.model.document import Document from frappe.utils import flt, get_url, nowdate from frappe.utils.background_jobs import enqueue -from payments.utils import get_payment_gateway_controller +from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( + get_accounting_dimensions, +) from erpnext.accounts.doctype.payment_entry.payment_entry import ( get_company_defaults, get_payment_entry, @@ -19,6 +21,14 @@ from erpnext.accounts.doctype.subscription_plan.subscription_plan import get_pla from erpnext.accounts.party import get_party_account, get_party_bank_account from erpnext.accounts.utils import get_account_currency from erpnext.erpnext_integrations.stripe_integration import create_stripe_subscription +from erpnext.utilities import payment_app_import_guard + + +def _get_payment_gateway_controller(*args, **kwargs): + with payment_app_import_guard(): + from payments.utils import get_payment_gateway_controller + + return get_payment_gateway_controller(*args, **kwargs) class PaymentRequest(Document): @@ -35,25 +45,24 @@ class PaymentRequest(Document): frappe.throw(_("To create a Payment Request reference document is required")) def validate_payment_request_amount(self): - existing_payment_request_amount = get_existing_payment_request_amount( - self.reference_doctype, self.reference_name + existing_payment_request_amount = flt( + get_existing_payment_request_amount(self.reference_doctype, self.reference_name) ) - if existing_payment_request_amount: - ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name) - if hasattr(ref_doc, "order_type") and getattr(ref_doc, "order_type") != "Shopping Cart": - ref_amount = get_amount(ref_doc, self.payment_account) + ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name) + if not hasattr(ref_doc, "order_type") or getattr(ref_doc, "order_type") != "Shopping Cart": + ref_amount = get_amount(ref_doc, self.payment_account) - if existing_payment_request_amount + flt(self.grand_total) > ref_amount: - frappe.throw( - _("Total Payment Request amount cannot be greater than {0} amount").format( - self.reference_doctype - ) + if existing_payment_request_amount + flt(self.grand_total) > ref_amount: + frappe.throw( + _("Total Payment Request amount cannot be greater than {0} amount").format( + self.reference_doctype ) + ) def validate_currency(self): ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name) - if self.payment_account and ref_doc.currency != frappe.db.get_value( + if self.payment_account and ref_doc.currency != frappe.get_cached_value( "Account", self.payment_account, "account_currency" ): frappe.throw(_("Transaction currency must be same as Payment Gateway currency")) @@ -107,7 +116,7 @@ class PaymentRequest(Document): self.request_phone_payment() def request_phone_payment(self): - controller = get_payment_gateway_controller(self.payment_gateway) + controller = _get_payment_gateway_controller(self.payment_gateway) request_amount = self.get_request_amount() payment_record = dict( @@ -156,7 +165,7 @@ class PaymentRequest(Document): def payment_gateway_validation(self): try: - controller = get_payment_gateway_controller(self.payment_gateway) + controller = _get_payment_gateway_controller(self.payment_gateway) if hasattr(controller, "on_payment_request_submission"): return controller.on_payment_request_submission(self) else: @@ -189,7 +198,7 @@ class PaymentRequest(Document): ) data.update({"company": frappe.defaults.get_defaults().company}) - controller = get_payment_gateway_controller(self.payment_gateway) + controller = _get_payment_gateway_controller(self.payment_gateway) controller.validate_transaction_currency(self.currency) if hasattr(controller, "validate_minimum_transaction_amount"): @@ -254,6 +263,7 @@ class PaymentRequest(Document): payment_entry.update( { + "mode_of_payment": self.mode_of_payment, "reference_no": self.name, "reference_date": nowdate(), "remarks": "Payment Entry against {0} {1} via Payment Request {2}".format( @@ -262,6 +272,17 @@ class PaymentRequest(Document): } ) + # Update dimensions + payment_entry.update( + { + "cost_center": self.get("cost_center"), + "project": self.get("project"), + } + ) + + for dimension in get_accounting_dimensions(): + payment_entry.update({dimension: self.get(dimension)}) + if payment_entry.difference_amount: company_details = get_company_defaults(ref_doc.company) @@ -403,25 +424,22 @@ def make_payment_request(**args): else "" ) - existing_payment_request = None - if args.order_type == "Shopping Cart": - existing_payment_request = frappe.db.get_value( - "Payment Request", - {"reference_doctype": args.dt, "reference_name": args.dn, "docstatus": ("!=", 2)}, - ) + draft_payment_request = frappe.db.get_value( + "Payment Request", + {"reference_doctype": args.dt, "reference_name": args.dn, "docstatus": 0}, + ) - if existing_payment_request: + existing_payment_request_amount = get_existing_payment_request_amount(args.dt, args.dn) + + if existing_payment_request_amount: + grand_total -= existing_payment_request_amount + + if draft_payment_request: frappe.db.set_value( - "Payment Request", existing_payment_request, "grand_total", grand_total, update_modified=False + "Payment Request", draft_payment_request, "grand_total", grand_total, update_modified=False ) - pr = frappe.get_doc("Payment Request", existing_payment_request) + pr = frappe.get_doc("Payment Request", draft_payment_request) else: - if args.order_type != "Shopping Cart": - existing_payment_request_amount = get_existing_payment_request_amount(args.dt, args.dn) - - if existing_payment_request_amount: - grand_total -= existing_payment_request_amount - pr = frappe.new_doc("Payment Request") pr.update( { @@ -444,6 +462,17 @@ def make_payment_request(**args): } ) + # Update dimensions + pr.update( + { + "cost_center": ref_doc.get("cost_center"), + "project": ref_doc.get("project"), + } + ) + + for dimension in get_accounting_dimensions(): + pr.update({dimension: ref_doc.get(dimension)}) + if args.order_type == "Shopping Cart" or args.mute_email: pr.flags.mute_email = True @@ -466,7 +495,7 @@ def get_amount(ref_doc, payment_account=None): """get amount based on doctype""" dt = ref_doc.doctype if dt in ["Sales Order", "Purchase Order"]: - grand_total = flt(ref_doc.grand_total) - flt(ref_doc.advance_paid) + grand_total = flt(ref_doc.rounded_total) - flt(ref_doc.advance_paid) elif dt in ["Sales Invoice", "Purchase Invoice"]: if ref_doc.party_account_currency == ref_doc.currency: diff --git a/erpnext/accounts/doctype/payment_schedule/payment_schedule.json b/erpnext/accounts/doctype/payment_schedule/payment_schedule.json index 6ed7a3154e..dde9980ce5 100644 --- a/erpnext/accounts/doctype/payment_schedule/payment_schedule.json +++ b/erpnext/accounts/doctype/payment_schedule/payment_schedule.json @@ -39,6 +39,7 @@ { "columns": 2, "fetch_from": "payment_term.description", + "fetch_if_empty": 1, "fieldname": "description", "fieldtype": "Small Text", "in_list_view": 1, @@ -159,7 +160,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-04-28 05:41:35.084233", + "modified": "2022-09-16 13:57:06.382859", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Schedule", @@ -168,5 +169,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file 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 866a94d04b..ca98bee5c1 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py +++ b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py @@ -43,7 +43,7 @@ class PeriodClosingVoucher(AccountsController): make_reverse_gl_entries(voucher_type="Period Closing Voucher", voucher_no=self.name) def validate_account_head(self): - closing_account_type = frappe.db.get_value("Account", self.closing_account_head, "root_type") + closing_account_type = frappe.get_cached_value("Account", self.closing_account_head, "root_type") if closing_account_type not in ["Liability", "Equity"]: frappe.throw( diff --git a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js index 98f3420d87..e6d9fe2b54 100644 --- a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js +++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js @@ -25,7 +25,7 @@ frappe.ui.form.on('POS Closing Entry', { frappe.realtime.on('closing_process_complete', async function(data) { await frm.reload_doc(); - if (frm.doc.status == 'Failed' && frm.doc.error_message && data.user == frappe.session.user) { + if (frm.doc.status == 'Failed' && frm.doc.error_message) { frappe.msgprint({ title: __('POS Closing Failed'), message: frm.doc.error_message, @@ -36,6 +36,15 @@ frappe.ui.form.on('POS Closing Entry', { }); set_html_data(frm); + + if (frm.doc.docstatus == 1) { + if (!frm.doc.posting_date) { + frm.set_value("posting_date", frappe.datetime.nowdate()); + } + if (!frm.doc.posting_time) { + frm.set_value("posting_time", frappe.datetime.now_time()); + } + } }, refresh: function(frm) { diff --git a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.json b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.json index d6e35c6a50..9d15e6cf35 100644 --- a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.json +++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.json @@ -11,6 +11,7 @@ "period_end_date", "column_break_3", "posting_date", + "posting_time", "pos_opening_entry", "status", "section_break_5", @@ -51,7 +52,6 @@ "fieldtype": "Datetime", "in_list_view": 1, "label": "Period End Date", - "read_only": 1, "reqd": 1 }, { @@ -219,6 +219,13 @@ "fieldtype": "Small Text", "label": "Error", "read_only": 1 + }, + { + "fieldname": "posting_time", + "fieldtype": "Time", + "label": "Posting Time", + "no_copy": 1, + "reqd": 1 } ], "is_submittable": 1, @@ -228,10 +235,11 @@ "link_fieldname": "pos_closing_entry" } ], - "modified": "2021-10-20 16:19:25.340565", + "modified": "2022-08-01 11:37:14.991228", "modified_by": "Administrator", "module": "Accounts", "name": "POS Closing Entry", + "naming_rule": "Expression (old style)", "owner": "Administrator", "permissions": [ { @@ -278,5 +286,6 @@ ], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py index 49aab0d0bb..115b415eed 100644 --- a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py +++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py @@ -15,11 +15,30 @@ from erpnext.controllers.status_updater import StatusUpdater class POSClosingEntry(StatusUpdater): def validate(self): + self.posting_date = self.posting_date or frappe.utils.nowdate() + self.posting_time = self.posting_time or frappe.utils.nowtime() + if frappe.db.get_value("POS Opening Entry", self.pos_opening_entry, "status") != "Open": frappe.throw(_("Selected POS Opening Entry should be open."), title=_("Invalid Opening Entry")) + self.validate_duplicate_pos_invoices() self.validate_pos_invoices() + def validate_duplicate_pos_invoices(self): + pos_occurences = {} + for idx, inv in enumerate(self.pos_transactions, 1): + pos_occurences.setdefault(inv.pos_invoice, []).append(idx) + + error_list = [] + for key, value in pos_occurences.items(): + if len(value) > 1: + error_list.append( + _("{} is added multiple times on rows: {}".format(frappe.bold(key), frappe.bold(value))) + ) + + if error_list: + frappe.throw(error_list, title=_("Duplicate POS Invoices found"), as_list=True) + def validate_pos_invoices(self): invalid_rows = [] for d in self.pos_transactions: diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.js b/erpnext/accounts/doctype/pos_invoice/pos_invoice.js index 15c292211c..56b857992c 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.js +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.js @@ -5,6 +5,8 @@ frappe.provide("erpnext.accounts"); erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnext.selling.SellingController { + settings = {}; + setup(doc) { this.setup_posting_date_time_check(); super.setup(doc); @@ -12,21 +14,37 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex company() { erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype); + this.frm.set_value("set_warehouse", ""); + this.frm.set_value("taxes_and_charges", ""); } onload(doc) { super.onload(); this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice Merge Log', 'POS Closing Entry']; + if(doc.__islocal && doc.is_pos && frappe.get_route_str() !== 'point-of-sale') { this.frm.script_manager.trigger("is_pos"); this.frm.refresh_fields(); } + this.frm.set_query("set_warehouse", function(doc) { + return { + filters: { + company: doc.company ? doc.company : '', + } + } + }); + erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype); } + onload_post_render(frm) { + this.pos_profile(frm); + } + refresh(doc) { super.refresh(); + if (doc.docstatus == 1 && !doc.is_return) { this.frm.add_custom_button(__('Return'), this.make_sales_return, __('Create')); this.frm.page.set_inner_btn_group_as_primary(__('Create')); @@ -36,6 +54,18 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex this.frm.return_print_format = "Sales Invoice Return"; this.frm.set_value('consolidated_invoice', ''); } + + this.frm.set_query("customer", (function () { + const customer_groups = this.settings?.customer_groups; + + if (!customer_groups?.length) return {}; + + return { + filters: { + customer_group: ["in", customer_groups], + } + } + }).bind(this)); } is_pos() { @@ -88,6 +118,25 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex }); } + pos_profile(frm) { + if (!frm.pos_profile || frm.pos_profile == '') { + this.update_customer_groups_settings([]); + return; + } + + frappe.call({ + method: "erpnext.selling.page.point_of_sale.point_of_sale.get_pos_profile_data", + args: { "pos_profile": frm.pos_profile }, + callback: ({ message: profile }) => { + this.update_customer_groups_settings(profile?.customer_groups); + }, + }); + } + + update_customer_groups_settings(customer_groups) { + this.settings.customer_groups = customer_groups?.map((group) => group.name) + } + amount(){ this.write_off_outstanding_amount_automatically() } diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json index b126d57400..eedaaaf338 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json @@ -343,7 +343,8 @@ "no_copy": 1, "options": "POS Invoice", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "default": "0", @@ -1553,7 +1554,7 @@ "icon": "fa fa-file-text", "is_submittable": 1, "links": [], - "modified": "2022-03-22 13:00:24.166684", + "modified": "2022-09-30 03:49:50.455199", "modified_by": "Administrator", "module": "Accounts", "name": "POS Invoice", diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index 6e3a0766f1..a1239d64a0 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -239,14 +239,14 @@ class POSInvoice(SalesInvoice): frappe.bold(d.warehouse), frappe.bold(d.qty), ) - if flt(available_stock) <= 0: + if is_stock_item and flt(available_stock) <= 0: frappe.throw( _("Row #{}: Item Code: {} is not available under warehouse {}.").format( d.idx, item_code, warehouse ), title=_("Item Unavailable"), ) - elif flt(available_stock) < flt(d.qty): + elif is_stock_item and flt(available_stock) < flt(d.qty): frappe.throw( _( "Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. Available quantity {}." @@ -335,7 +335,8 @@ class POSInvoice(SalesInvoice): if ( self.change_amount and self.account_for_change_amount - and frappe.db.get_value("Account", self.account_for_change_amount, "company") != self.company + and frappe.get_cached_value("Account", self.account_for_change_amount, "company") + != self.company ): frappe.throw( _("The selected change account {} doesn't belongs to Company {}.").format( @@ -486,7 +487,7 @@ class POSInvoice(SalesInvoice): customer_price_list, customer_group, customer_currency = frappe.db.get_value( "Customer", self.customer, ["default_price_list", "customer_group", "default_currency"] ) - customer_group_price_list = frappe.db.get_value( + customer_group_price_list = frappe.get_cached_value( "Customer Group", customer_group, "default_price_list" ) selling_price_list = ( @@ -532,8 +533,8 @@ class POSInvoice(SalesInvoice): if not self.debit_to: self.debit_to = get_party_account("Customer", self.customer, self.company) - self.party_account_currency = frappe.db.get_value( - "Account", self.debit_to, "account_currency", cache=True + self.party_account_currency = frappe.get_cached_value( + "Account", self.debit_to, "account_currency" ) if not self.due_date and self.customer: self.due_date = get_due_date(self.posting_date, "Customer", self.customer, self.company) @@ -632,11 +633,12 @@ def get_stock_availability(item_code, warehouse): pos_sales_qty = get_pos_reserved_qty(item_code, warehouse) return bin_qty - pos_sales_qty, is_stock_item else: - is_stock_item = False + is_stock_item = True if frappe.db.exists("Product Bundle", item_code): return get_bundle_availability(item_code, warehouse), is_stock_item else: - # Is a service item + is_stock_item = False + # Is a service item or non_stock item return 0, is_stock_item @@ -650,7 +652,9 @@ def get_bundle_availability(bundle_item_code, warehouse): available_qty = item_bin_qty - item_pos_reserved_qty max_available_bundles = available_qty / item.qty - if bundle_bin_qty > max_available_bundles: + if bundle_bin_qty > max_available_bundles and frappe.get_value( + "Item", item.item_code, "is_stock_item" + ): bundle_bin_qty = max_available_bundles pos_sales_qty = get_pos_reserved_qty(bundle_item_code, warehouse) @@ -671,7 +675,7 @@ def get_bin_qty(item_code, warehouse): def get_pos_reserved_qty(item_code, warehouse): reserved_qty = frappe.db.sql( - """select sum(p_item.qty) as qty + """select sum(p_item.stock_qty) as qty from `tabPOS Invoice` p, `tabPOS Invoice Item` p_item where p.name = p_item.parent and ifnull(p.consolidated_invoice, '') = '' diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py index 70f128e0e3..3132fdd259 100644 --- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py @@ -495,6 +495,67 @@ class TestPOSInvoice(unittest.TestCase): self.assertRaises(frappe.ValidationError, pos.submit) + def test_value_error_on_serial_no_validation(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item + + se = make_serialized_item( + company="_Test Company", + target_warehouse="Stores - _TC", + cost_center="Main - _TC", + expense_account="Cost of Goods Sold - _TC", + ) + serial_nos = se.get("items")[0].serial_no + + # make a pos invoice + pos = create_pos_invoice( + company="_Test Company", + debit_to="Debtors - _TC", + account_for_change_amount="Cash - _TC", + warehouse="Stores - _TC", + income_account="Sales - _TC", + expense_account="Cost of Goods Sold - _TC", + cost_center="Main - _TC", + item=se.get("items")[0].item_code, + rate=1000, + qty=1, + do_not_save=1, + ) + pos.get("items")[0].has_serial_no = 1 + pos.get("items")[0].serial_no = serial_nos.split("\n")[0] + pos.set("payments", []) + pos.append( + "payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1000, "default": 1} + ) + pos = pos.save().submit() + + # make a return + pos_return = make_sales_return(pos.name) + pos_return.paid_amount = pos_return.grand_total + pos_return.save() + pos_return.submit() + + # set docstatus to 2 for pos to trigger this issue + frappe.db.set_value("POS Invoice", pos.name, "docstatus", 2) + + pos2 = create_pos_invoice( + company="_Test Company", + debit_to="Debtors - _TC", + account_for_change_amount="Cash - _TC", + warehouse="Stores - _TC", + income_account="Sales - _TC", + expense_account="Cost of Goods Sold - _TC", + cost_center="Main - _TC", + item=se.get("items")[0].item_code, + rate=1000, + qty=1, + do_not_save=1, + ) + + pos2.get("items")[0].has_serial_no = 1 + pos2.get("items")[0].serial_no = serial_nos.split("\n")[0] + # Value error should not be triggered on validation + pos2.save() + def test_loyalty_points(self): from erpnext.accounts.doctype.loyalty_program.loyalty_program import ( get_loyalty_program_details_with_points, 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 3f85668ede..4bb18655b4 100644 --- a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json +++ b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json @@ -8,6 +8,7 @@ "engine": "InnoDB", "field_order": [ "barcode", + "has_item_scanned", "item_code", "col_break1", "item_name", @@ -808,11 +809,19 @@ "fieldtype": "Check", "label": "Grant Commission", "read_only": 1 + }, + { + "default": "0", + "depends_on": "barcode", + "fieldname": "has_item_scanned", + "fieldtype": "Check", + "label": "Has Item Scanned", + "read_only": 1 } ], "istable": 1, "links": [], - "modified": "2021-10-05 12:23:47.506290", + "modified": "2022-11-02 12:52:39.125295", "modified_by": "Administrator", "module": "Accounts", "name": "POS Invoice Item", @@ -820,5 +829,6 @@ "owner": "Administrator", "permissions": [], "sort_field": "modified", - "sort_order": "DESC" + "sort_order": "DESC", + "states": [] } \ No newline at end of file diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.json b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.json index d762087078..a059455647 100644 --- a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.json +++ b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.json @@ -6,6 +6,7 @@ "engine": "InnoDB", "field_order": [ "posting_date", + "posting_time", "merge_invoices_based_on", "column_break_3", "pos_closing_entry", @@ -105,12 +106,19 @@ "label": "Customer Group", "mandatory_depends_on": "eval:doc.merge_invoices_based_on == 'Customer Group'", "options": "Customer Group" + }, + { + "fieldname": "posting_time", + "fieldtype": "Time", + "label": "Posting Time", + "no_copy": 1, + "reqd": 1 } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-09-14 11:17:19.001142", + "modified": "2022-08-01 11:36:42.456429", "modified_by": "Administrator", "module": "Accounts", "name": "POS Invoice Merge Log", @@ -173,5 +181,6 @@ ], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py index 5003a1d6a8..b1e22087db 100644 --- a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py +++ b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py @@ -6,11 +6,10 @@ import json import frappe from frappe import _ -from frappe.core.page.background_jobs.background_jobs import get_info from frappe.model.document import Document from frappe.model.mapper import map_child_doc, map_doc -from frappe.utils import cint, flt, getdate, nowdate -from frappe.utils.background_jobs import enqueue +from frappe.utils import cint, flt, get_time, getdate, nowdate, nowtime +from frappe.utils.background_jobs import enqueue, is_job_queued from frappe.utils.scheduler import is_scheduler_inactive @@ -18,6 +17,22 @@ class POSInvoiceMergeLog(Document): def validate(self): self.validate_customer() self.validate_pos_invoice_status() + self.validate_duplicate_pos_invoices() + + def validate_duplicate_pos_invoices(self): + pos_occurences = {} + for idx, inv in enumerate(self.pos_invoices, 1): + pos_occurences.setdefault(inv.pos_invoice, []).append(idx) + + error_list = [] + for key, value in pos_occurences.items(): + if len(value) > 1: + error_list.append( + _("{} is added multiple times on rows: {}".format(frappe.bold(key), frappe.bold(value))) + ) + + if error_list: + frappe.throw(error_list, title=_("Duplicate POS Invoices found"), as_list=True) def validate_customer(self): if self.merge_invoices_based_on == "Customer Group": @@ -99,6 +114,7 @@ class POSInvoiceMergeLog(Document): sales_invoice.is_consolidated = 1 sales_invoice.set_posting_time = 1 sales_invoice.posting_date = getdate(self.posting_date) + sales_invoice.posting_time = get_time(self.posting_time) sales_invoice.save() sales_invoice.submit() @@ -115,6 +131,7 @@ class POSInvoiceMergeLog(Document): credit_note.is_consolidated = 1 credit_note.set_posting_time = 1 credit_note.posting_date = getdate(self.posting_date) + credit_note.posting_time = get_time(self.posting_time) # TODO: return could be against multiple sales invoice which could also have been consolidated? # credit_note.return_against = self.consolidated_invoice credit_note.save() @@ -402,6 +419,9 @@ def create_merge_logs(invoice_by_customer, closing_entry=None): merge_log.posting_date = ( getdate(closing_entry.get("posting_date")) if closing_entry else nowdate() ) + merge_log.posting_time = ( + get_time(closing_entry.get("posting_time")) if closing_entry else nowtime() + ) merge_log.customer = customer merge_log.pos_closing_entry = closing_entry.get("name") if closing_entry else None @@ -421,12 +441,14 @@ def create_merge_logs(invoice_by_customer, closing_entry=None): if closing_entry: closing_entry.set_status(update=True, status="Failed") + if type(error_message) == list: + error_message = frappe.json.dumps(error_message) closing_entry.db_set("error_message", error_message) raise finally: frappe.db.commit() - frappe.publish_realtime("closing_process_complete", {"user": frappe.session.user}) + frappe.publish_realtime("closing_process_complete", user=frappe.session.user) def cancel_merge_logs(merge_logs, closing_entry=None): @@ -453,7 +475,7 @@ def cancel_merge_logs(merge_logs, closing_entry=None): finally: frappe.db.commit() - frappe.publish_realtime("closing_process_complete", {"user": frappe.session.user}) + frappe.publish_realtime("closing_process_complete", user=frappe.session.user) def enqueue_job(job, **kwargs): @@ -462,7 +484,7 @@ def enqueue_job(job, **kwargs): closing_entry = kwargs.get("closing_entry") or {} job_name = closing_entry.get("name") - if not job_already_enqueued(job_name): + if not is_job_queued(job_name): enqueue( job, **kwargs, @@ -486,12 +508,6 @@ def check_scheduler_status(): frappe.throw(_("Scheduler is inactive. Cannot enqueue job."), title=_("Scheduler Inactive")) -def job_already_enqueued(job_name): - enqueued_jobs = [d.get("job_name") for d in get_info()] - if job_name in enqueued_jobs: - return True - - def safe_load_json(message): try: json_message = json.loads(message).get("message") diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.json b/erpnext/accounts/doctype/pos_profile/pos_profile.json index d5f7ee4f21..994b6776e3 100644 --- a/erpnext/accounts/doctype/pos_profile/pos_profile.json +++ b/erpnext/accounts/doctype/pos_profile/pos_profile.json @@ -43,6 +43,7 @@ "currency", "write_off_account", "write_off_cost_center", + "write_off_limit", "account_for_change_amount", "disable_rounded_total", "column_break_23", @@ -360,6 +361,14 @@ "fieldtype": "Check", "label": "Validate Stock on Save" }, + { + "default": "1", + "description": "Auto write off precision loss while consolidation", + "fieldname": "write_off_limit", + "fieldtype": "Currency", + "label": "Write Off Limit", + "reqd": 1 + }, { "default": "0", "description": "If enabled, the consolidated invoices will have rounded total disabled", @@ -393,7 +402,7 @@ "link_fieldname": "pos_profile" } ], - "modified": "2022-07-21 11:16:46.911173", + "modified": "2022-08-10 12:57:06.241439", "modified_by": "Administrator", "module": "Accounts", "name": "POS Profile", diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json index 99c5b34fa3..a63039e0e3 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json @@ -52,7 +52,10 @@ "free_item_rate", "column_break_42", "free_item_uom", + "round_free_qty", "is_recursive", + "recurse_for", + "apply_recursion_over", "section_break_23", "valid_from", "valid_upto", @@ -176,7 +179,7 @@ }, { "collapsible": 1, - "depends_on": "eval:doc.apply_on != 'Transaction'", + "depends_on": "eval:doc.apply_on != 'Transaction' && !doc.mixed_conditions", "fieldname": "section_break_18", "fieldtype": "Section Break", "label": "Discount on Other Item" @@ -297,12 +300,12 @@ { "fieldname": "min_qty", "fieldtype": "Float", - "label": "Min Qty" + "label": "Min Qty (As Per Stock UOM)" }, { "fieldname": "max_qty", "fieldtype": "Float", - "label": "Max Qty" + "label": "Max Qty (As Per Stock UOM)" }, { "fieldname": "column_break_21", @@ -469,7 +472,7 @@ "description": "If rate is zero them item will be treated as \"Free Item\"", "fieldname": "free_item_rate", "fieldtype": "Currency", - "label": "Rate" + "label": "Free Item Rate" }, { "collapsible": 1, @@ -481,7 +484,7 @@ "description": "System will notify to increase or decrease quantity or amount ", "fieldname": "threshold_percentage", "fieldtype": "Percent", - "label": "Threshold for Suggestion" + "label": "Threshold for Suggestion (In Percentage)" }, { "description": "Higher the number, higher the priority", @@ -578,15 +581,38 @@ "fieldtype": "Select", "label": "Naming Series", "options": "PRLE-.####" + }, + { + "default": "0", + "fieldname": "round_free_qty", + "fieldtype": "Check", + "label": "Round Free Qty" + }, + { + "depends_on": "is_recursive", + "description": "Give free item for every N quantity", + "fieldname": "recurse_for", + "fieldtype": "Float", + "label": "Recurse Every (As Per Transaction UOM)", + "mandatory_depends_on": "is_recursive" + }, + { + "default": "0", + "depends_on": "is_recursive", + "description": "Qty for which recursion isn't applicable.", + "fieldname": "apply_recursion_over", + "fieldtype": "Float", + "label": "Apply Recursion Over (As Per Transaction UOM)" } ], "icon": "fa fa-gift", "idx": 1, "links": [], - "modified": "2021-08-06 15:10:04.219321", + "modified": "2023-02-14 04:53:34.887358", "modified_by": "Administrator", "module": "Accounts", "name": "Pricing Rule", + "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ { @@ -642,5 +668,6 @@ "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "title_field": "title" -} +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py index 98e0a9b215..2943500cf4 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -10,7 +10,7 @@ import re import frappe from frappe import _, throw from frappe.model.document import Document -from frappe.utils import cint, flt, getdate +from frappe.utils import cint, flt apply_on_dict = {"Item Code": "items", "Item Group": "item_groups", "Brand": "brands"} @@ -24,6 +24,7 @@ class PricingRule(Document): self.validate_applicable_for_selling_or_buying() self.validate_min_max_amt() self.validate_min_max_qty() + self.validate_recursion() self.cleanup_fields_value() self.validate_rate_or_discount() self.validate_max_discount() @@ -109,6 +110,18 @@ class PricingRule(Document): if self.min_amt and self.max_amt and flt(self.min_amt) > flt(self.max_amt): throw(_("Min Amt can not be greater than Max Amt")) + def validate_recursion(self): + if self.price_or_product_discount != "Product": + return + if self.free_item or self.same_item: + if flt(self.recurse_for) <= 0: + self.recurse_for = 1 + if self.is_recursive: + if flt(self.apply_recursion_over) > flt(self.min_qty): + throw(_("Min Qty should be greater than Recurse Over Qty")) + if flt(self.apply_recursion_over) < 0: + throw(_("Recurse Over Qty cannot be less than 0")) + def cleanup_fields_value(self): for logic_field in ["apply_on", "applicable_for", "rate_or_discount"]: fieldname = frappe.scrub(self.get(logic_field) or "") @@ -171,8 +184,7 @@ class PricingRule(Document): if self.is_cumulative and not (self.valid_from and self.valid_upto): frappe.throw(_("Valid from and valid upto fields are mandatory for the cumulative")) - if self.valid_from and self.valid_upto and getdate(self.valid_from) > getdate(self.valid_upto): - frappe.throw(_("Valid from date must be less than valid upto date")) + self.validate_from_to_dates("valid_from", "valid_upto") def validate_condition(self): if ( @@ -243,7 +255,7 @@ def apply_pricing_rule(args, doc=None): for item in item_list: args_copy = copy.deepcopy(args) args_copy.update(item) - data = get_pricing_rule_for_item(args_copy, item.get("price_list_rate"), doc=doc) + data = get_pricing_rule_for_item(args_copy, doc=doc) out.append(data) if ( @@ -268,7 +280,19 @@ def get_serial_no_for_item(args): return item_details -def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=False): +def update_pricing_rule_uom(pricing_rule, args): + child_doc = {"Item Code": "items", "Item Group": "item_groups", "Brand": "brands"}.get( + pricing_rule.apply_on + ) + + apply_on_field = frappe.scrub(pricing_rule.apply_on) + + for row in pricing_rule.get(child_doc): + if row.get(apply_on_field) == args.get(apply_on_field): + pricing_rule.uom = row.uom + + +def get_pricing_rule_for_item(args, doc=None, for_validate=False): from erpnext.accounts.doctype.pricing_rule.utils import ( get_applied_pricing_rules, get_pricing_rule_items, @@ -324,7 +348,8 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa if isinstance(pricing_rule, str): pricing_rule = frappe.get_cached_doc("Pricing Rule", pricing_rule) - pricing_rule.apply_rule_on_other_items = get_pricing_rule_items(pricing_rule) + update_pricing_rule_uom(pricing_rule, args) + pricing_rule.apply_rule_on_other_items = get_pricing_rule_items(pricing_rule) or [] if pricing_rule.get("suggestion"): continue @@ -337,7 +362,6 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa if pricing_rule.mixed_conditions or pricing_rule.apply_rule_on_other: item_details.update( { - "apply_rule_on_other_items": json.dumps(pricing_rule.apply_rule_on_other_items), "price_or_product_discount": pricing_rule.price_or_product_discount, "apply_rule_on": ( frappe.scrub(pricing_rule.apply_rule_on_other) @@ -347,6 +371,9 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa } ) + if pricing_rule.apply_rule_on_other_items: + item_details["apply_rule_on_other_items"] = json.dumps(pricing_rule.apply_rule_on_other_items) + if pricing_rule.coupon_code_based == 1 and args.coupon_code == None: return item_details @@ -438,12 +465,15 @@ def apply_price_discount_rule(pricing_rule, item_details, args): if pricing_rule.currency == args.currency: pricing_rule_rate = pricing_rule.rate + # TODO https://github.com/frappe/erpnext/pull/23636 solve this in some other way. if pricing_rule_rate: + is_blank_uom = pricing_rule.get("uom") != args.get("uom") # Override already set price list rate (from item price) # if pricing_rule_rate > 0 item_details.update( { - "price_list_rate": pricing_rule_rate * args.get("conversion_factor", 1), + "price_list_rate": pricing_rule_rate + * (args.get("conversion_factor", 1) if is_blank_uom else 1), } ) item_details.update({"discount_percentage": 0.0}) @@ -492,7 +522,7 @@ def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None, ra ) if pricing_rule.get("mixed_conditions") or pricing_rule.get("apply_rule_on_other"): - items = get_pricing_rule_items(pricing_rule) + items = get_pricing_rule_items(pricing_rule, other_items=True) item_details.apply_on = ( frappe.scrub(pricing_rule.apply_rule_on_other) if pricing_rule.apply_rule_on_other diff --git a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py index 3bd0cd2e83..5bb366a770 100644 --- a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py @@ -595,6 +595,247 @@ class TestPricingRule(unittest.TestCase): frappe.get_doc("Item Price", {"item_code": "Water Flask"}).delete() item.delete() + def test_item_price_with_blank_uom_pricing_rule(self): + properties = { + "item_code": "Item Blank UOM", + "stock_uom": "Nos", + "sales_uom": "Box", + "uoms": [dict(uom="Box", conversion_factor=10)], + } + item = make_item(properties=properties) + + make_item_price("Item Blank UOM", "_Test Price List", 100) + + pricing_rule_record = { + "doctype": "Pricing Rule", + "title": "_Test Item Blank UOM Rule", + "apply_on": "Item Code", + "items": [ + { + "item_code": "Item Blank UOM", + } + ], + "selling": 1, + "currency": "INR", + "rate_or_discount": "Rate", + "rate": 101, + "company": "_Test Company", + } + rule = frappe.get_doc(pricing_rule_record) + rule.insert() + + si = create_sales_invoice( + do_not_save=True, item_code="Item Blank UOM", uom="Box", conversion_factor=10 + ) + si.selling_price_list = "_Test Price List" + si.save() + + # If UOM is blank consider it as stock UOM and apply pricing_rule on all UOM. + # rate is 101, Selling UOM is Box that have conversion_factor of 10 so 101 * 10 = 1010 + self.assertEqual(si.items[0].price_list_rate, 1010) + self.assertEqual(si.items[0].rate, 1010) + + si.delete() + + si = create_sales_invoice(do_not_save=True, item_code="Item Blank UOM", uom="Nos") + si.selling_price_list = "_Test Price List" + si.save() + + # UOM is blank so consider it as stock UOM and apply pricing_rule on all UOM. + # rate is 101, Selling UOM is Nos that have conversion_factor of 1 so 101 * 1 = 101 + self.assertEqual(si.items[0].price_list_rate, 101) + self.assertEqual(si.items[0].rate, 101) + + si.delete() + rule.delete() + frappe.get_doc("Item Price", {"item_code": "Item Blank UOM"}).delete() + + item.delete() + + def test_item_price_with_selling_uom_pricing_rule(self): + properties = { + "item_code": "Item UOM other than Stock", + "stock_uom": "Nos", + "sales_uom": "Box", + "uoms": [dict(uom="Box", conversion_factor=10)], + } + item = make_item(properties=properties) + + make_item_price("Item UOM other than Stock", "_Test Price List", 100) + + pricing_rule_record = { + "doctype": "Pricing Rule", + "title": "_Test Item UOM other than Stock Rule", + "apply_on": "Item Code", + "items": [ + { + "item_code": "Item UOM other than Stock", + "uom": "Box", + } + ], + "selling": 1, + "currency": "INR", + "rate_or_discount": "Rate", + "rate": 101, + "company": "_Test Company", + } + rule = frappe.get_doc(pricing_rule_record) + rule.insert() + + si = create_sales_invoice( + do_not_save=True, item_code="Item UOM other than Stock", uom="Box", conversion_factor=10 + ) + si.selling_price_list = "_Test Price List" + si.save() + + # UOM is Box so apply pricing_rule only on Box UOM. + # Selling UOM is Box and as both UOM are same no need to multiply by conversion_factor. + self.assertEqual(si.items[0].price_list_rate, 101) + self.assertEqual(si.items[0].rate, 101) + + si.delete() + + si = create_sales_invoice(do_not_save=True, item_code="Item UOM other than Stock", uom="Nos") + si.selling_price_list = "_Test Price List" + si.save() + + # UOM is Box so pricing_rule won't apply as selling_uom is Nos. + # As Pricing Rule is not applied price of 100 will be fetched from Item Price List. + self.assertEqual(si.items[0].price_list_rate, 100) + self.assertEqual(si.items[0].rate, 100) + + si.delete() + rule.delete() + frappe.get_doc("Item Price", {"item_code": "Item UOM other than Stock"}).delete() + + item.delete() + + def test_item_group_price_with_blank_uom_pricing_rule(self): + group = frappe.get_doc(doctype="Item Group", item_group_name="_Test Pricing Rule Item Group") + group.save() + properties = { + "item_code": "Item with Group Blank UOM", + "item_group": "_Test Pricing Rule Item Group", + "stock_uom": "Nos", + "sales_uom": "Box", + "uoms": [dict(uom="Box", conversion_factor=10)], + } + item = make_item(properties=properties) + + make_item_price("Item with Group Blank UOM", "_Test Price List", 100) + + pricing_rule_record = { + "doctype": "Pricing Rule", + "title": "_Test Item with Group Blank UOM Rule", + "apply_on": "Item Group", + "item_groups": [ + { + "item_group": "_Test Pricing Rule Item Group", + } + ], + "selling": 1, + "currency": "INR", + "rate_or_discount": "Rate", + "rate": 101, + "company": "_Test Company", + } + rule = frappe.get_doc(pricing_rule_record) + rule.insert() + + si = create_sales_invoice( + do_not_save=True, item_code="Item with Group Blank UOM", uom="Box", conversion_factor=10 + ) + si.selling_price_list = "_Test Price List" + si.save() + + # If UOM is blank consider it as stock UOM and apply pricing_rule on all UOM. + # rate is 101, Selling UOM is Box that have conversion_factor of 10 so 101 * 10 = 1010 + self.assertEqual(si.items[0].price_list_rate, 1010) + self.assertEqual(si.items[0].rate, 1010) + + si.delete() + + si = create_sales_invoice(do_not_save=True, item_code="Item with Group Blank UOM", uom="Nos") + si.selling_price_list = "_Test Price List" + si.save() + + # UOM is blank so consider it as stock UOM and apply pricing_rule on all UOM. + # rate is 101, Selling UOM is Nos that have conversion_factor of 1 so 101 * 1 = 101 + self.assertEqual(si.items[0].price_list_rate, 101) + self.assertEqual(si.items[0].rate, 101) + + si.delete() + rule.delete() + frappe.get_doc("Item Price", {"item_code": "Item with Group Blank UOM"}).delete() + item.delete() + group.delete() + + def test_item_group_price_with_selling_uom_pricing_rule(self): + group = frappe.get_doc(doctype="Item Group", item_group_name="_Test Pricing Rule Item Group UOM") + group.save() + properties = { + "item_code": "Item with Group UOM other than Stock", + "item_group": "_Test Pricing Rule Item Group UOM", + "stock_uom": "Nos", + "sales_uom": "Box", + "uoms": [dict(uom="Box", conversion_factor=10)], + } + item = make_item(properties=properties) + + make_item_price("Item with Group UOM other than Stock", "_Test Price List", 100) + + pricing_rule_record = { + "doctype": "Pricing Rule", + "title": "_Test Item with Group UOM other than Stock Rule", + "apply_on": "Item Group", + "item_groups": [ + { + "item_group": "_Test Pricing Rule Item Group UOM", + "uom": "Box", + } + ], + "selling": 1, + "currency": "INR", + "rate_or_discount": "Rate", + "rate": 101, + "company": "_Test Company", + } + rule = frappe.get_doc(pricing_rule_record) + rule.insert() + + si = create_sales_invoice( + do_not_save=True, + item_code="Item with Group UOM other than Stock", + uom="Box", + conversion_factor=10, + ) + si.selling_price_list = "_Test Price List" + si.save() + + # UOM is Box so apply pricing_rule only on Box UOM. + # Selling UOM is Box and as both UOM are same no need to multiply by conversion_factor. + self.assertEqual(si.items[0].price_list_rate, 101) + self.assertEqual(si.items[0].rate, 101) + + si.delete() + + si = create_sales_invoice( + do_not_save=True, item_code="Item with Group UOM other than Stock", uom="Nos" + ) + si.selling_price_list = "_Test Price List" + si.save() + + # UOM is Box so pricing_rule won't apply as selling_uom is Nos. + # As Pricing Rule is not applied price of 100 will be fetched from Item Price List. + self.assertEqual(si.items[0].price_list_rate, 100) + self.assertEqual(si.items[0].rate, 100) + + si.delete() + rule.delete() + frappe.get_doc("Item Price", {"item_code": "Item with Group UOM other than Stock"}).delete() + item.delete() + group.delete() + def test_pricing_rule_for_different_currency(self): make_item("Test Sanitizer Item") @@ -766,6 +1007,107 @@ class TestPricingRule(unittest.TestCase): frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule with Min Qty - 1") frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule with Min Qty - 2") + def test_pricing_rule_for_other_items_cond_with_amount(self): + item = make_item("Water Flask New") + other_item = make_item("Other Water Flask New") + make_item_price(item.name, "_Test Price List", 100) + make_item_price(other_item.name, "_Test Price List", 100) + + pricing_rule_record = { + "doctype": "Pricing Rule", + "title": "_Test Water Flask Rule", + "apply_on": "Item Code", + "apply_rule_on_other": "Item Code", + "price_or_product_discount": "Price", + "rate_or_discount": "Discount Percentage", + "other_item_code": other_item.name, + "items": [ + { + "item_code": item.name, + } + ], + "selling": 1, + "currency": "INR", + "min_amt": 200, + "discount_percentage": 10, + "company": "_Test Company", + } + rule = frappe.get_doc(pricing_rule_record) + rule.insert() + + si = create_sales_invoice(do_not_save=True, item_code=item.name) + si.append( + "items", + { + "item_code": other_item.name, + "item_name": other_item.item_name, + "description": other_item.description, + "stock_uom": other_item.stock_uom, + "uom": other_item.stock_uom, + "cost_center": si.items[0].cost_center, + "expense_account": si.items[0].expense_account, + "warehouse": si.items[0].warehouse, + "conversion_factor": 1, + "qty": 1, + }, + ) + si.selling_price_list = "_Test Price List" + si.save() + + self.assertEqual(si.items[0].discount_percentage, 0) + self.assertEqual(si.items[1].discount_percentage, 0) + + si.items[0].qty = 2 + si.save() + + self.assertEqual(si.items[0].discount_percentage, 0) + self.assertEqual(si.items[0].stock_qty, 2) + self.assertEqual(si.items[0].amount, 200) + self.assertEqual(si.items[0].price_list_rate, 100) + self.assertEqual(si.items[1].discount_percentage, 10) + + si.delete() + rule.delete() + + def test_pricing_rule_for_product_free_item_rounded_qty_and_recursion(self): + frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule") + test_record = { + "doctype": "Pricing Rule", + "title": "_Test Pricing Rule", + "apply_on": "Item Code", + "currency": "USD", + "items": [ + { + "item_code": "_Test Item", + } + ], + "selling": 1, + "rate": 0, + "min_qty": 3, + "max_qty": 7, + "price_or_product_discount": "Product", + "same_item": 1, + "free_qty": 1, + "round_free_qty": 1, + "is_recursive": 1, + "recurse_for": 2, + "company": "_Test Company", + } + frappe.get_doc(test_record.copy()).insert() + + # With pricing rule + so = make_sales_order(item_code="_Test Item", qty=5) + so.load_from_db() + self.assertEqual(so.items[1].is_free_item, 1) + self.assertEqual(so.items[1].item_code, "_Test Item") + self.assertEqual(so.items[1].qty, 2) + + so = make_sales_order(item_code="_Test Item", qty=7) + so.load_from_db() + self.assertEqual(so.items[1].is_free_item, 1) + self.assertEqual(so.items[1].item_code, "_Test Item") + self.assertEqual(so.items[1].qty, 4) + test_dependencies = ["Campaign"] @@ -781,7 +1123,7 @@ def make_pricing_rule(**args): "apply_on": args.apply_on or "Item Code", "applicable_for": args.applicable_for, "selling": args.selling or 0, - "currency": "USD", + "currency": "INR", "apply_discount_on_rate": args.apply_discount_on_rate or 0, "buying": args.buying or 0, "min_qty": args.min_qty or 0.0, diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py index 70926cfbd7..57feaa03eb 100644 --- a/erpnext/accounts/doctype/pricing_rule/utils.py +++ b/erpnext/accounts/doctype/pricing_rule/utils.py @@ -111,6 +111,12 @@ def _get_pricing_rules(apply_on, args, values): ) if apply_on_field == "item_code": + if args.get("uom", None): + item_conditions += ( + " and ({child_doc}.uom='{item_uom}' or IFNULL({child_doc}.uom, '')='')".format( + child_doc=child_doc, item_uom=args.get("uom") + ) + ) if "variant_of" not in args: args.variant_of = frappe.get_cached_value("Item", args.item_code, "variant_of") @@ -121,6 +127,12 @@ def _get_pricing_rules(apply_on, args, values): values["variant_of"] = args.variant_of elif apply_on_field == "item_group": item_conditions = _get_tree_conditions(args, "Item Group", child_doc, False) + if args.get("uom", None): + item_conditions += ( + " and ({child_doc}.uom='{item_uom}' or IFNULL({child_doc}.uom, '')='')".format( + child_doc=child_doc, item_uom=args.get("uom") + ) + ) conditions += get_other_conditions(conditions, values, args) warehouse_conditions = _get_tree_conditions(args, "Warehouse", "`tabPricing Rule`") @@ -238,6 +250,22 @@ def get_other_conditions(conditions, values, args): and ifnull(`tabPricing Rule`.valid_upto, '2500-12-31')""" values["transaction_date"] = args.get("transaction_date") + if args.get("doctype") in [ + "Quotation", + "Quotation Item", + "Sales Order", + "Sales Order Item", + "Delivery Note", + "Delivery Note Item", + "Sales Invoice", + "Sales Invoice Item", + "POS Invoice", + "POS Invoice Item", + ]: + conditions += """ and ifnull(`tabPricing Rule`.selling, 0) = 1""" + else: + conditions += """ and ifnull(`tabPricing Rule`.buying, 0) = 1""" + return conditions @@ -252,12 +280,6 @@ def filter_pricing_rules(args, pricing_rules, doc=None): stock_qty = flt(args.get("stock_qty")) amount = flt(args.get("price_list_rate")) * flt(args.get("qty")) - if pricing_rules[0].apply_rule_on_other: - field = frappe.scrub(pricing_rules[0].apply_rule_on_other) - - if field and pricing_rules[0].get("other_" + field) != args.get(field): - return - pr_doc = frappe.get_cached_doc("Pricing Rule", pricing_rules[0].name) if pricing_rules[0].mixed_conditions and doc: @@ -274,7 +296,7 @@ def filter_pricing_rules(args, pricing_rules, doc=None): amount += data[1] if pricing_rules[0].apply_rule_on_other and not pricing_rules[0].mixed_conditions and doc: - pricing_rules = get_qty_and_rate_for_other_item(doc, pr_doc, pricing_rules) or [] + pricing_rules = get_qty_and_rate_for_other_item(doc, pr_doc, pricing_rules, args) or [] else: pricing_rules = filter_pricing_rules_for_qty_amount(stock_qty, amount, pricing_rules, args) @@ -352,16 +374,14 @@ def validate_quantity_and_amount_for_suggestion(args, qty, amount, item_code, tr if fieldname: msg = _( "If you {0} {1} quantities of the item {2}, the scheme {3} will be applied on the item." - ).format( - type_of_transaction, args.get(fieldname), bold(item_code), bold(args.rule_description) - ) + ).format(type_of_transaction, args.get(fieldname), bold(item_code), bold(args.title)) if fieldname in ["min_amt", "max_amt"]: msg = _("If you {0} {1} worth item {2}, the scheme {3} will be applied on the item.").format( type_of_transaction, fmt_money(args.get(fieldname), currency=args.get("currency")), bold(item_code), - bold(args.rule_description), + bold(args.title), ) frappe.msgprint(msg) @@ -454,17 +474,29 @@ def get_qty_and_rate_for_mixed_conditions(doc, pr_doc, args): return sum_qty, sum_amt, items -def get_qty_and_rate_for_other_item(doc, pr_doc, pricing_rules): - items = get_pricing_rule_items(pr_doc) +def get_qty_and_rate_for_other_item(doc, pr_doc, pricing_rules, row_item): + other_items = get_pricing_rule_items(pr_doc, other_items=True) + pricing_rule_apply_on = apply_on_table.get(pr_doc.get("apply_on")) + apply_on = frappe.scrub(pr_doc.get("apply_on")) + + items = [] + for d in pr_doc.get(pricing_rule_apply_on): + if apply_on == "item_group": + items.extend(get_child_item_groups(d.get(apply_on))) + else: + items.append(d.get(apply_on)) for row in doc.items: - if row.get(frappe.scrub(pr_doc.apply_rule_on_other)) in items: - pricing_rules = filter_pricing_rules_for_qty_amount( - row.get("stock_qty"), row.get("amount"), pricing_rules, row - ) + if row.get(apply_on) in items: + if not row.get("qty"): + continue + + stock_qty = row.get("qty") * (row.get("conversion_factor") or 1.0) + amount = stock_qty * (row.get("price_list_rate") or row.get("rate")) + pricing_rules = filter_pricing_rules_for_qty_amount(stock_qty, amount, pricing_rules, row) if pricing_rules and pricing_rules[0]: - pricing_rules[0].apply_rule_on_other_items = items + pricing_rules[0].apply_rule_on_other_items = other_items return pricing_rules @@ -617,9 +649,13 @@ def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None): qty = pricing_rule.free_qty or 1 if pricing_rule.is_recursive: - transaction_qty = args.get("qty") if args else doc.total_qty + transaction_qty = ( + args.get("qty") if args else doc.total_qty + ) - pricing_rule.apply_recursion_over if transaction_qty: - qty = flt(transaction_qty) * qty + qty = flt(transaction_qty) * qty / pricing_rule.recurse_for + if pricing_rule.round_free_qty: + qty = round(qty) free_item_data_args = { "item_code": free_item, @@ -649,30 +685,40 @@ def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None): item_details.free_item_data.append(free_item_data_args) -def apply_pricing_rule_for_free_items(doc, pricing_rule_args, set_missing_values=False): +def apply_pricing_rule_for_free_items(doc, pricing_rule_args): if pricing_rule_args: - items = tuple((d.item_code, d.pricing_rules) for d in doc.items if d.is_free_item) + args = {(d["item_code"], d["pricing_rules"]): d for d in pricing_rule_args} - for args in pricing_rule_args: - if not items or (args.get("item_code"), args.get("pricing_rules")) not in items: - doc.append("items", args) + for item in doc.items: + if not item.is_free_item: + continue + + free_item_data = args.get((item.item_code, item.pricing_rules)) + if free_item_data: + free_item_data.pop("item_name") + free_item_data.pop("description") + item.update(free_item_data) + args.pop((item.item_code, item.pricing_rules)) + + for free_item in args.values(): + doc.append("items", free_item) -def get_pricing_rule_items(pr_doc): +def get_pricing_rule_items(pr_doc, other_items=False) -> list: apply_on_data = [] apply_on = frappe.scrub(pr_doc.get("apply_on")) pricing_rule_apply_on = apply_on_table.get(pr_doc.get("apply_on")) - for d in pr_doc.get(pricing_rule_apply_on): - if apply_on == "item_group": - apply_on_data.extend(get_child_item_groups(d.get(apply_on))) - else: - apply_on_data.append(d.get(apply_on)) - - if pr_doc.apply_rule_on_other: + if pr_doc.apply_rule_on_other and other_items: apply_on = frappe.scrub(pr_doc.apply_rule_on_other) apply_on_data.append(pr_doc.get("other_" + apply_on)) + else: + for d in pr_doc.get(pricing_rule_apply_on): + if apply_on == "item_group": + apply_on_data.extend(get_child_item_groups(d.get(apply_on))) + else: + apply_on_data.append(d.get(apply_on)) return list(set(apply_on_data)) diff --git a/erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.py b/erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.py index 8ec726b36c..1f88849b26 100644 --- a/erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.py +++ b/erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.py @@ -34,4 +34,4 @@ class ProcessDeferredAccounting(Document): filters={"against_voucher_type": self.doctype, "against_voucher": self.name}, ) - make_gl_entries(gl_entries=gl_entries, cancel=1) + make_gl_entries(gl_map=gl_entries, cancel=1) diff --git a/erpnext/accounts/doctype/process_deferred_accounting/test_process_deferred_accounting.py b/erpnext/accounts/doctype/process_deferred_accounting/test_process_deferred_accounting.py index 164ba6aa34..5a0aeb7284 100644 --- a/erpnext/accounts/doctype/process_deferred_accounting/test_process_deferred_accounting.py +++ b/erpnext/accounts/doctype/process_deferred_accounting/test_process_deferred_accounting.py @@ -57,3 +57,16 @@ class TestProcessDeferredAccounting(unittest.TestCase): ] check_gl_entries(self, si.name, expected_gle, "2019-01-10") + + def test_pda_submission_and_cancellation(self): + pda = frappe.get_doc( + dict( + doctype="Process Deferred Accounting", + posting_date="2019-01-01", + start_date="2019-01-01", + end_date="2019-01-31", + type="Income", + ) + ) + pda.submit() + pda.cancel() diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html index 82705a9cea..3920d4cf09 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html @@ -25,7 +25,7 @@
- +
@@ -49,7 +49,6 @@
{% endif %} - {{ _("Against") }}: {{ row.against }}
{{ _("Remarks") }}: {{ row.remarks }} {% if row.bill_no %}
{{ _("Supplier Invoice No") }}: {{ row.bill_no }} diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js index 7dd77fbb3c..7dd5ef36f2 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js @@ -9,6 +9,7 @@ frappe.ui.form.on('Process Statement Of Accounts', { refresh: function(frm){ if(!frm.doc.__islocal) { frm.add_custom_button(__('Send Emails'), function(){ + if (frm.is_dirty()) frappe.throw(__("Please save before proceeding.")) frappe.call({ method: "erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.send_emails", args: { @@ -25,7 +26,8 @@ frappe.ui.form.on('Process Statement Of Accounts', { }); }); frm.add_custom_button(__('Download'), function(){ - var url = frappe.urllib.get_full_url( + if (frm.is_dirty()) frappe.throw(__("Please save before proceeding.")) + let url = frappe.urllib.get_full_url( '/api/method/erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.download_statements?' + 'document_name='+encodeURIComponent(frm.doc.name)) $.ajax({ diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json index a26267ba5e..16602d317a 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json @@ -23,10 +23,12 @@ "fetch_customers", "column_break_6", "primary_mandatory", + "show_net_values_in_party_account", "column_break_17", "customers", "preferences", "orientation", + "include_break", "include_ageing", "ageing_based_on", "section_break_14", @@ -284,10 +286,22 @@ "fieldtype": "Link", "label": "Terms and Conditions", "options": "Terms and Conditions" + }, + { + "default": "1", + "fieldname": "include_break", + "fieldtype": "Check", + "label": "Page Break After Each SoA" + }, + { + "default": "0", + "fieldname": "show_net_values_in_party_account", + "fieldtype": "Check", + "label": "Show Net Values in Party Account" } ], "links": [], - "modified": "2021-09-06 21:00:45.732505", + "modified": "2022-11-10 17:44:17.165991", "modified_by": "Administrator", "module": "Accounts", "name": "Process Statement Of Accounts", @@ -321,5 +335,6 @@ ], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py index 01f716daa2..a48c0272ff 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py @@ -6,6 +6,7 @@ import copy import frappe from frappe import _ +from frappe.desk.reportview import get_match_cond from frappe.model.document import Document from frappe.utils import add_days, add_months, format_date, getdate, today from frappe.utils.jinja import validate_template @@ -94,6 +95,7 @@ def get_report_pdf(doc, consolidated=True): "show_opening_entries": 0, "include_default_book_entries": 0, "tax_id": tax_id if tax_id else None, + "show_net_values_in_party_account": doc.show_net_values_in_party_account, } ) col, res = get_soa(filters) @@ -128,7 +130,8 @@ def get_report_pdf(doc, consolidated=True): if not bool(statement_dict): return False elif consolidated: - result = "".join(list(statement_dict.values())) + delimiter = '
' if doc.include_break else "" + result = delimiter.join(list(statement_dict.values())) return get_pdf(result, {"orientation": doc.orientation}) else: for customer, statement_html in statement_dict.items(): @@ -240,8 +243,6 @@ def fetch_customers(customer_collection, collection_name, primary_mandatory): if int(primary_mandatory): if primary_email == "": continue - elif (billing_email == "") and (primary_email == ""): - continue customer_list.append( {"name": customer.name, "primary_email": primary_email, "billing_email": billing_email} @@ -273,8 +274,12 @@ def get_customer_emails(customer_name, primary_mandatory, billing_and_primary=Tr link.link_doctype='Customer' and link.link_name=%s and contact.is_billing_contact=1 + {mcond} ORDER BY - contact.creation desc""", + contact.creation desc + """.format( + mcond=get_match_cond("Contact") + ), customer_name, ) @@ -313,6 +318,8 @@ def send_emails(document_name, from_scheduler=False): attachments = [{"fname": customer + ".pdf", "fcontent": report_pdf}] recipients, cc = get_recipients_and_cc(customer, doc) + if not recipients: + continue context = get_context(customer, doc) subject = frappe.render_template(doc.subject, context) message = frappe.render_template(doc.body, context) diff --git a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py index fac9be7bdb..4d28d10660 100644 --- a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py +++ b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py @@ -34,8 +34,8 @@ pricing_rule_fields = [ other_fields = [ "min_qty", "max_qty", - "min_amt", - "max_amt", + "min_amount", + "max_amount", "priority", "warehouse", "threshold_percentage", @@ -246,7 +246,11 @@ def prepare_pricing_rule( def set_args(args, pr, doc, child_doc, discount_fields, child_doc_fields): pr.update(args) for field in other_fields + discount_fields: - pr.set(field, child_doc_fields.get(field)) + target_field = field + if target_field in ["min_amount", "max_amount"]: + target_field = "min_amt" if field == "min_amount" else "max_amt" + + pr.set(target_field, child_doc_fields.get(field)) pr.promotional_scheme_id = child_doc_fields.name pr.promotional_scheme = doc.name diff --git a/erpnext/accounts/doctype/promotional_scheme/test_promotional_scheme.py b/erpnext/accounts/doctype/promotional_scheme/test_promotional_scheme.py index b3b9d7b208..9e576fb877 100644 --- a/erpnext/accounts/doctype/promotional_scheme/test_promotional_scheme.py +++ b/erpnext/accounts/doctype/promotional_scheme/test_promotional_scheme.py @@ -90,6 +90,23 @@ class TestPromotionalScheme(unittest.TestCase): price_rules = frappe.get_all("Pricing Rule", filters={"promotional_scheme": ps.name}) self.assertEqual(price_rules, []) + def test_min_max_amount_configuration(self): + ps = make_promotional_scheme() + ps.price_discount_slabs[0].min_amount = 10 + ps.price_discount_slabs[0].max_amount = 1000 + ps.save() + + price_rules_data = frappe.db.get_value( + "Pricing Rule", {"promotional_scheme": ps.name}, ["min_amt", "max_amt"], as_dict=1 + ) + + self.assertEqual(price_rules_data.min_amt, 10) + self.assertEqual(price_rules_data.max_amt, 1000) + + frappe.delete_doc("Promotional Scheme", ps.name) + price_rules = frappe.get_all("Pricing Rule", filters={"promotional_scheme": ps.name}) + self.assertEqual(price_rules, []) + def make_promotional_scheme(**args): args = frappe._dict(args) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index ec861a2787..e2b4a1ad5b 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -31,7 +31,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying. super.onload(); // Ignore linked advances - this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry']; + this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry', 'Purchase Invoice', "Repost Payment Ledger"]; if(!this.frm.doc.__islocal) { // show credit_to in print format @@ -81,7 +81,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying. } if(doc.docstatus == 1 && doc.outstanding_amount != 0 - && !(doc.is_return && doc.return_against)) { + && !(doc.is_return && doc.return_against) && !doc.on_hold) { this.frm.add_custom_button(__('Payment'), this.make_payment_entry, __('Create')); cur_frm.page.set_inner_btn_group_as_primary(__('Create')); } @@ -99,7 +99,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying. } } - if (doc.outstanding_amount > 0 && !cint(doc.is_return)) { + if (doc.outstanding_amount > 0 && !cint(doc.is_return) && !doc.on_hold) { cur_frm.add_custom_button(__('Payment Request'), function() { me.make_payment_request() }, __('Create')); @@ -569,6 +569,10 @@ frappe.ui.form.on("Purchase Invoice", { erpnext.queries.setup_queries(frm, "Warehouse", function() { return erpnext.queries.warehouse(frm.doc); }); + + if (frm.is_new()) { + frm.clear_table("tax_withheld_vouchers"); + } }, is_subcontracted: function(frm) { diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index 534b879e78..54caf6f8b0 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -12,39 +12,27 @@ "supplier", "supplier_name", "tax_id", - "due_date", - "tax_withholding_category", - "column_break1", "company", + "column_break_6", "posting_date", "posting_time", "set_posting_time", + "due_date", + "column_break1", "is_paid", "is_return", + "return_against", "apply_tds", + "tax_withholding_category", "amended_from", - "accounting_dimensions_section", - "cost_center", - "dimension_col_break", - "project", "supplier_invoice_details", "bill_no", "column_break_15", "bill_date", - "returns", - "return_against", - "section_addresses", - "supplier_address", - "address_display", - "contact_person", - "contact_display", - "contact_mobile", - "contact_email", - "col_break_address", - "shipping_address", - "shipping_address_display", - "billing_address", - "billing_address_display", + "accounting_dimensions_section", + "cost_center", + "dimension_col_break", + "project", "currency_and_price_list", "currency", "conversion_rate", @@ -54,37 +42,37 @@ "plc_conversion_rate", "ignore_pricing_rule", "sec_warehouse", - "set_warehouse", - "rejected_warehouse", - "col_break_warehouse", - "set_from_warehouse", - "supplier_warehouse", - "is_subcontracted", - "items_section", - "update_stock", "scan_barcode", + "col_break_warehouse", + "update_stock", + "set_warehouse", + "set_from_warehouse", + "is_subcontracted", + "rejected_warehouse", + "supplier_warehouse", + "items_section", "items", - "pricing_rule_details", - "pricing_rules", - "raw_materials_supplied", - "supplied_items", "section_break_26", "total_qty", + "total_net_weight", + "column_break_50", "base_total", "base_net_total", "column_break_28", - "total_net_weight", "total", "net_total", + "tax_withholding_net_total", + "base_tax_withholding_net_total", "taxes_section", "tax_category", - "column_break_49", - "shipping_rule", - "section_break_51", "taxes_and_charges", + "column_break_58", + "shipping_rule", + "column_break_49", + "incoterm", + "named_place", + "section_break_51", "taxes", - "sec_tax_breakup", - "other_charges_calculation", "totals", "base_taxes_and_charges_added", "base_taxes_and_charges_deducted", @@ -93,13 +81,6 @@ "taxes_and_charges_added", "taxes_and_charges_deducted", "total_taxes_and_charges", - "section_break_44", - "apply_discount_on", - "base_discount_amount", - "additional_discount_account", - "column_break_46", - "additional_discount_percentage", - "discount_amount", "section_break_49", "base_grand_total", "base_rounding_adjustment", @@ -113,24 +94,57 @@ "total_advance", "outstanding_amount", "disable_rounded_total", + "section_break_44", + "apply_discount_on", + "base_discount_amount", + "column_break_46", + "additional_discount_percentage", + "discount_amount", + "tax_withheld_vouchers_section", + "tax_withheld_vouchers", + "sec_tax_breakup", + "other_charges_calculation", + "pricing_rule_details", + "pricing_rules", + "raw_materials_supplied", + "supplied_items", + "payments_tab", "payments_section", "mode_of_payment", - "cash_bank_account", + "base_paid_amount", "clearance_date", "col_br_payments", + "cash_bank_account", "paid_amount", - "base_paid_amount", + "advances_section", + "allocate_advances_automatically", + "get_advances", + "advances", + "advance_tax", "write_off", "write_off_amount", "base_write_off_amount", "column_break_61", "write_off_account", "write_off_cost_center", - "advances_section", - "allocate_advances_automatically", - "get_advances", - "advances", - "advance_tax", + "address_and_contact_tab", + "section_addresses", + "supplier_address", + "address_display", + "col_break_address", + "contact_person", + "contact_display", + "contact_mobile", + "contact_email", + "company_shipping_address_section", + "shipping_address", + "column_break_126", + "shipping_address_display", + "company_billing_address_section", + "billing_address", + "column_break_130", + "billing_address_display", + "terms_tab", "payment_schedule_section", "payment_terms_template", "ignore_default_payment_terms_template", @@ -138,23 +152,11 @@ "terms_section_break", "tc_name", "terms", - "printing_settings", - "letter_head", - "select_print_heading", - "column_break_112", - "group_same_items", - "language", - "sb_14", - "on_hold", - "release_date", - "cb_17", - "hold_comment", - "more_info", + "more_info_tab", + "status_section", "status", - "inter_company_invoice_reference", - "represents_company", - "column_break_147", - "is_internal_supplier", + "column_break_177", + "per_received", "accounting_details_section", "credit_to", "party_account_currency", @@ -162,15 +164,32 @@ "against_expense_account", "column_break_63", "unrealized_profit_loss_account", - "remarks", "subscription_section", - "from_date", - "to_date", - "column_break_114", "auto_repeat", "update_auto_repeat_reference", - "per_received", - "is_old_subcontracting_flow" + "column_break_114", + "from_date", + "to_date", + "printing_settings", + "letter_head", + "group_same_items", + "column_break_112", + "select_print_heading", + "language", + "sb_14", + "on_hold", + "release_date", + "cb_17", + "hold_comment", + "additional_info_section", + "is_internal_supplier", + "represents_company", + "column_break_147", + "inter_company_invoice_reference", + "is_old_subcontracting_flow", + "remarks", + "connections_tab", + "column_break_38" ], "fields": [ { @@ -353,7 +372,7 @@ "collapsible_depends_on": "bill_no", "fieldname": "supplier_invoice_details", "fieldtype": "Section Break", - "label": "Supplier Invoice Details" + "label": "Supplier Invoice" }, { "fieldname": "bill_no", @@ -376,12 +395,6 @@ "oldfieldtype": "Date", "print_hide": 1 }, - { - "depends_on": "return_against", - "fieldname": "returns", - "fieldtype": "Section Break", - "label": "Returns" - }, { "depends_on": "return_against", "fieldname": "return_against", @@ -393,10 +406,9 @@ "read_only": 1 }, { - "collapsible": 1, "fieldname": "section_addresses", "fieldtype": "Section Break", - "label": "Address and Contact" + "label": "Supplier Address" }, { "fieldname": "supplier_address", @@ -512,17 +524,17 @@ "fieldname": "ignore_pricing_rule", "fieldtype": "Check", "label": "Ignore Pricing Rule", - "no_copy": 1, "permlevel": 1, "print_hide": 1 }, { "fieldname": "sec_warehouse", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "hide_border": 1, + "label": "Items" }, { "depends_on": "update_stock", - "description": "Sets 'Accepted Warehouse' in each row of the items table.", "fieldname": "set_warehouse", "fieldtype": "Link", "label": "Set Accepted Warehouse", @@ -531,7 +543,6 @@ }, { "depends_on": "update_stock", - "description": "Warehouse where you are maintaining stock of rejected items", "fieldname": "rejected_warehouse", "fieldtype": "Link", "label": "Rejected Warehouse", @@ -554,6 +565,7 @@ { "fieldname": "items_section", "fieldtype": "Section Break", + "hide_border": 1, "oldfieldtype": "Section Break", "options": "fa fa-shopping-cart" }, @@ -581,6 +593,7 @@ "reqd": 1 }, { + "collapsible": 1, "fieldname": "pricing_rule_details", "fieldtype": "Section Break", "label": "Pricing Rules" @@ -593,6 +606,7 @@ "read_only": 1 }, { + "collapsible": 1, "collapsible_depends_on": "supplied_items", "fieldname": "raw_materials_supplied", "fieldtype": "Section Break", @@ -656,6 +670,7 @@ "read_only": 1 }, { + "depends_on": "total_net_weight", "fieldname": "total_net_weight", "fieldtype": "Float", "label": "Total Net Weight", @@ -665,6 +680,8 @@ { "fieldname": "taxes_section", "fieldtype": "Section Break", + "hide_border": 1, + "label": "Taxes and Charges", "oldfieldtype": "Section Break", "options": "fa fa-money" }, @@ -688,7 +705,8 @@ }, { "fieldname": "section_break_51", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "hide_border": 1 }, { "fieldname": "taxes_and_charges", @@ -792,7 +810,6 @@ }, { "collapsible": 1, - "collapsible_depends_on": "discount_amount", "fieldname": "section_break_44", "fieldtype": "Section Break", "label": "Additional Discount" @@ -832,7 +849,8 @@ }, { "fieldname": "section_break_49", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "label": "Totals" }, { "fieldname": "base_grand_total", @@ -1003,8 +1021,6 @@ }, { "collapsible": 1, - "collapsible_depends_on": "write_off_amount", - "depends_on": "grand_total", "fieldname": "write_off", "fieldtype": "Section Break", "label": "Write Off" @@ -1081,7 +1097,6 @@ "print_hide": 1 }, { - "collapsible": 1, "collapsible_depends_on": "eval:(!doc.is_return)", "fieldname": "payment_schedule_section", "fieldtype": "Section Break", @@ -1102,8 +1117,6 @@ "print_hide": 1 }, { - "collapsible": 1, - "collapsible_depends_on": "terms", "fieldname": "terms_section_break", "fieldtype": "Section Break", "label": "Terms and Conditions", @@ -1119,13 +1132,13 @@ { "fieldname": "terms", "fieldtype": "Text Editor", - "label": "Terms and Conditions1" + "label": "Terms and Conditions" }, { "collapsible": 1, "fieldname": "printing_settings", "fieldtype": "Section Break", - "label": "Printing Settings" + "label": "Print Settings" }, { "allow_on_submit": 1, @@ -1166,15 +1179,6 @@ "print_hide": 1, "read_only": 1 }, - { - "collapsible": 1, - "fieldname": "more_info", - "fieldtype": "Section Break", - "label": "More Information", - "oldfieldtype": "Section Break", - "options": "fa fa-file-text", - "print_hide": 1 - }, { "default": "0", "fetch_from": "supplier.is_internal_supplier", @@ -1260,7 +1264,7 @@ "collapsible": 1, "fieldname": "subscription_section", "fieldtype": "Section Break", - "label": "Subscription Section", + "label": "Subscription", "print_hide": 1 }, { @@ -1339,7 +1343,7 @@ }, { "depends_on": "eval:doc.is_internal_supplier", - "description": "Unrealized Profit / Loss account for intra-company transfers", + "description": "Unrealized Profit/Loss account for intra-company transfers", "fieldname": "unrealized_profit_loss_account", "fieldtype": "Link", "label": "Unrealized Profit / Loss Account", @@ -1356,7 +1360,6 @@ }, { "depends_on": "eval:doc.update_stock && doc.is_internal_supplier", - "description": "Sets 'From Warehouse' in each row of the items table.", "fieldname": "set_from_warehouse", "fieldtype": "Link", "label": "Set From Warehouse", @@ -1367,7 +1370,7 @@ "width": "50px" }, { - "depends_on": "eval:doc.is_subcontracted", + "depends_on": "eval:doc.is_subcontracted", "fieldname": "supplier_warehouse", "fieldtype": "Link", "label": "Supplier Warehouse", @@ -1386,12 +1389,6 @@ "print_hide": 1, "read_only": 1 }, - { - "fieldname": "additional_discount_account", - "fieldtype": "Link", - "label": "Additional Discount Account", - "options": "Account" - }, { "default": "0", "fieldname": "ignore_default_payment_terms_template", @@ -1426,13 +1423,140 @@ "hidden": 1, "label": "Is Old Subcontracting Flow", "read_only": 1 - } + }, + { + "default": "0", + "depends_on": "apply_tds", + "fieldname": "tax_withholding_net_total", + "fieldtype": "Currency", + "hidden": 1, + "label": "Tax Withholding Net Total", + "no_copy": 1, + "options": "currency", + "read_only": 1 + }, + { + "depends_on": "apply_tds", + "fieldname": "base_tax_withholding_net_total", + "fieldtype": "Currency", + "hidden": 1, + "label": "Base Tax Withholding Net Total", + "no_copy": 1, + "options": "Company:company:default_currency", + "print_hide": 1, + "read_only": 1 + }, + { + "collapsible_depends_on": "tax_withheld_vouchers", + "fieldname": "tax_withheld_vouchers_section", + "fieldtype": "Section Break", + "label": "Tax Withheld Vouchers" + }, + { + "fieldname": "tax_withheld_vouchers", + "fieldtype": "Table", + "label": "Tax Withheld Vouchers", + "no_copy": 1, + "options": "Tax Withheld Vouchers", + "read_only": 1 + }, + { + "fieldname": "payments_tab", + "fieldtype": "Tab Break", + "label": "Payments" + }, + { + "fieldname": "address_and_contact_tab", + "fieldtype": "Tab Break", + "label": "Address & Contact" + }, + { + "fieldname": "terms_tab", + "fieldtype": "Tab Break", + "label": "Terms" + }, + { + "fieldname": "more_info_tab", + "fieldtype": "Tab Break", + "label": "More Info" + }, + { + "fieldname": "connections_tab", + "fieldtype": "Tab Break", + "label": "Connections", + "show_dashboard": 1 + }, + { + "fieldname": "column_break_6", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_38", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_50", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_58", + "fieldtype": "Column Break" + }, + { + "fieldname": "company_shipping_address_section", + "fieldtype": "Section Break", + "label": "Company Shipping Address" + }, + { + "fieldname": "column_break_126", + "fieldtype": "Column Break" + }, + { + "fieldname": "company_billing_address_section", + "fieldtype": "Section Break", + "label": "Company Billing Address" + }, + { + "fieldname": "column_break_130", + "fieldtype": "Column Break" + }, + { + "collapsible": 1, + "fieldname": "status_section", + "fieldtype": "Section Break", + "label": "Status" + }, + { + "fieldname": "column_break_177", + "fieldtype": "Column Break" + }, + { + "collapsible": 1, + "fieldname": "additional_info_section", + "fieldtype": "Section Break", + "label": "Additional Info", + "oldfieldtype": "Section Break", + "options": "fa fa-file-text", + "print_hide": 1 + }, + { + "fieldname": "incoterm", + "fieldtype": "Link", + "label": "Incoterm", + "options": "Incoterm" + }, + { + "depends_on": "incoterm", + "fieldname": "named_place", + "fieldtype": "Data", + "label": "Named Place" + } ], "icon": "fa fa-file-text", "idx": 204, "is_submittable": 1, "links": [], - "modified": "2022-06-15 15:40:58.527065", + "modified": "2023-01-28 19:18:56.586321", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", @@ -1492,6 +1616,7 @@ "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "timeline_field": "supplier", "title_field": "title", "track_changes": 1 diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index de3927e45b..21addab240 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -5,6 +5,7 @@ import frappe from frappe import _, throw from frappe.model.mapper import get_mapped_doc +from frappe.query_builder.functions import Sum from frappe.utils import cint, cstr, flt, formatdate, get_link_to_form, getdate, nowdate import erpnext @@ -71,6 +72,9 @@ class PurchaseInvoice(BuyingController): supplier_tds = frappe.db.get_value("Supplier", self.supplier, "tax_withholding_category") self.set_onload("supplier_tds", supplier_tds) + if self.is_new(): + self.set("tax_withheld_vouchers", []) + def before_save(self): if not self.on_hold: self.release_date = "" @@ -150,8 +154,8 @@ class PurchaseInvoice(BuyingController): def set_missing_values(self, for_validate=False): if not self.credit_to: self.credit_to = get_party_account("Supplier", self.supplier, self.company) - self.party_account_currency = frappe.db.get_value( - "Account", self.credit_to, "account_currency", cache=True + self.party_account_currency = frappe.get_cached_value( + "Account", self.credit_to, "account_currency" ) if not self.due_date: self.due_date = get_due_date( @@ -172,7 +176,7 @@ class PurchaseInvoice(BuyingController): if not self.credit_to: self.raise_missing_debit_credit_account_error("Supplier", self.supplier) - account = frappe.db.get_value( + account = frappe.get_cached_value( "Account", self.credit_to, ["account_type", "report_type", "account_currency"], as_dict=True ) @@ -228,7 +232,9 @@ class PurchaseInvoice(BuyingController): ) if ( - cint(frappe.db.get_single_value("Buying Settings", "maintain_same_rate")) and not self.is_return + cint(frappe.db.get_single_value("Buying Settings", "maintain_same_rate")) + and not self.is_return + and not self.is_internal_supplier ): self.validate_rate_with_reference_doc( [ @@ -575,7 +581,6 @@ class PurchaseInvoice(BuyingController): self.make_supplier_gl_entry(gl_entries) self.make_item_gl_entries(gl_entries) - self.make_discount_gl_entries(gl_entries) if self.check_asset_cwip_enabled(): self.get_asset_gl_entry(gl_entries) @@ -604,7 +609,7 @@ class PurchaseInvoice(BuyingController): def make_supplier_gl_entry(self, gl_entries): # Checked both rounding_adjustment and rounded_total - # because rounded_total had value even before introcution of posting GLE based on rounded total + # because rounded_total had value even before introduction of posting GLE based on rounded total grand_total = ( self.rounded_total if (self.rounding_adjustment and self.rounded_total) else self.grand_total ) @@ -670,11 +675,8 @@ class PurchaseInvoice(BuyingController): exchange_rate_map, net_rate_map = get_purchase_document_details(self) - enable_discount_accounting = cint( - frappe.db.get_single_value("Buying Settings", "enable_discount_accounting") - ) provisional_accounting_for_non_stock_items = cint( - frappe.db.get_value( + frappe.get_cached_value( "Company", self.company, "enable_provisional_accounting_for_non_stock_items" ) ) @@ -709,6 +711,10 @@ class PurchaseInvoice(BuyingController): ) ) + credit_amount = item.base_net_amount + if self.is_internal_supplier and item.valuation_rate: + credit_amount = flt(item.valuation_rate * item.stock_qty) + # Intentionally passed negative debit amount to avoid incorrect GL Entry validation gl_entries.append( self.get_gl_dict( @@ -718,7 +724,7 @@ class PurchaseInvoice(BuyingController): "cost_center": item.cost_center, "project": item.project or self.project, "remarks": self.get("remarks") or _("Accounting Entry for Stock"), - "debit": -1 * flt(item.base_net_amount, item.precision("base_net_amount")), + "debit": -1 * flt(credit_amount, item.precision("base_net_amount")), }, warehouse_account[item.from_warehouse]["account_currency"], item=item, @@ -806,10 +812,7 @@ class PurchaseInvoice(BuyingController): else item.deferred_expense_account ) - if not item.is_fixed_asset: - dummy, amount = self.get_amount_and_base_amount(item, enable_discount_accounting) - else: - amount = flt(item.base_net_amount + item.item_tax_amount, item.precision("base_net_amount")) + dummy, amount = self.get_amount_and_base_amount(item, None) if provisional_accounting_for_non_stock_items: if item.purchase_receipt: @@ -981,7 +984,7 @@ class PurchaseInvoice(BuyingController): asset_amount = flt(item.net_amount) + flt(item.item_tax_amount / self.conversion_rate) base_asset_amount = flt(item.base_net_amount + item.item_tax_amount) - item_exp_acc_type = frappe.db.get_value("Account", item.expense_account, "account_type") + item_exp_acc_type = frappe.get_cached_value("Account", item.expense_account, "account_type") if not item.expense_account or item_exp_acc_type not in [ "Asset Received But Not Billed", "Fixed Asset", @@ -1160,12 +1163,9 @@ class PurchaseInvoice(BuyingController): def make_tax_gl_entries(self, gl_entries): # tax table gl entries valuation_tax = {} - enable_discount_accounting = cint( - frappe.db.get_single_value("Buying Settings", "enable_discount_accounting") - ) for tax in self.get("taxes"): - amount, base_amount = self.get_tax_amounts(tax, enable_discount_accounting) + amount, base_amount = self.get_tax_amounts(tax, None) if tax.category in ("Total", "Valuation and Total") and flt(base_amount): account_currency = get_account_currency(tax.account_head) @@ -1250,15 +1250,6 @@ class PurchaseInvoice(BuyingController): ) ) - @property - def enable_discount_accounting(self): - if not hasattr(self, "_enable_discount_accounting"): - self._enable_discount_accounting = cint( - frappe.db.get_single_value("Buying Settings", "enable_discount_accounting") - ) - - return self._enable_discount_accounting - def make_internal_transfer_gl_entries(self, gl_entries): if self.is_internal_transfer() and flt(self.base_total_taxes_and_charges): account_currency = get_account_currency(self.unrealized_profit_loss_account) @@ -1419,14 +1410,17 @@ class PurchaseInvoice(BuyingController): self.repost_future_sle_and_gle() self.update_project() - frappe.db.set(self, "status", "Cancelled") + self.db_set("status", "Cancelled") 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", + "Repost Payment Ledger", + "Repost Payment Ledger Items", "Payment Ledger Entry", + "Tax Withheld Vouchers", ) self.update_advance_tax_references(cancel=1) @@ -1471,24 +1465,25 @@ class PurchaseInvoice(BuyingController): def update_billing_status_in_pr(self, update_modified=True): updated_pr = [] + po_details = [] + + pr_details_billed_amt = self.get_pr_details_billed_amt() + for d in self.get("items"): if d.pr_detail: - billed_amt = frappe.db.sql( - """select sum(amount) from `tabPurchase Invoice Item` - where pr_detail=%s and docstatus=1""", - d.pr_detail, - ) - billed_amt = billed_amt and billed_amt[0][0] or 0 frappe.db.set_value( "Purchase Receipt Item", d.pr_detail, "billed_amt", - billed_amt, + flt(pr_details_billed_amt.get(d.pr_detail)), update_modified=update_modified, ) updated_pr.append(d.purchase_receipt) elif d.po_detail: - updated_pr += update_billed_amount_based_on_po(d.po_detail, update_modified) + po_details.append(d.po_detail) + + if po_details: + updated_pr += update_billed_amount_based_on_po(po_details, update_modified) for pr in set(updated_pr): from erpnext.stock.doctype.purchase_receipt.purchase_receipt import update_billing_percentage @@ -1496,6 +1491,24 @@ class PurchaseInvoice(BuyingController): pr_doc = frappe.get_doc("Purchase Receipt", pr) update_billing_percentage(pr_doc, update_modified=update_modified) + def get_pr_details_billed_amt(self): + # Get billed amount based on purchase receipt item reference (pr_detail) in purchase invoice + + pr_details_billed_amt = {} + pr_details = [d.get("pr_detail") for d in self.get("items") if d.get("pr_detail")] + if pr_details: + doctype = frappe.qb.DocType("Purchase Invoice Item") + query = ( + frappe.qb.from_(doctype) + .select(doctype.pr_detail, Sum(doctype.amount)) + .where(doctype.pr_detail.isin(pr_details) & doctype.docstatus == 1) + .groupby(doctype.pr_detail) + ) + + pr_details_billed_amt = frappe._dict(query.run(as_list=1)) + + return pr_details_billed_amt + def on_recurring(self, reference_doc, auto_repeat_doc): self.due_date = None @@ -1520,7 +1533,7 @@ class PurchaseInvoice(BuyingController): if not self.tax_withholding_category: return - tax_withholding_details, advance_taxes = get_party_tax_withholding_details( + tax_withholding_details, advance_taxes, voucher_wise_amount = get_party_tax_withholding_details( self, self.tax_withholding_category ) @@ -1549,6 +1562,19 @@ class PurchaseInvoice(BuyingController): for d in to_remove: self.remove(d) + ## Add pending vouchers on which tax was withheld + self.set("tax_withheld_vouchers", []) + + for voucher_no, voucher_details in voucher_wise_amount.items(): + self.append( + "tax_withheld_vouchers", + { + "voucher_name": voucher_no, + "voucher_type": voucher_details.get("voucher_type"), + "taxable_amount": voucher_details.get("amount"), + }, + ) + # calculate totals again after applying TDS self.calculate_taxes_and_totals() @@ -1791,4 +1817,6 @@ def make_purchase_receipt(source_name, target_doc=None): target_doc, ) + doc.set_onload("ignore_price_list", True) + return doc diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_list.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_list.js index 82d00308db..e1c37c6001 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_list.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_list.js @@ -63,7 +63,7 @@ frappe.listview_settings["Purchase Invoice"] = { }); listview.page.add_action_item(__("Payment"), ()=>{ - erpnext.bulk_transaction_processing.create(listview, "Purchase Invoice", "Payment"); + erpnext.bulk_transaction_processing.create(listview, "Purchase Invoice", "Payment Entry"); }); } }; diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index e55d3a70af..f901257ccf 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -338,59 +338,6 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin): self.assertEqual(discrepancy_caused_by_exchange_rate_diff, amount) - @change_settings("Buying Settings", {"enable_discount_accounting": 1}) - def test_purchase_invoice_with_discount_accounting_enabled(self): - - discount_account = create_account( - account_name="Discount Account", - parent_account="Indirect Expenses - _TC", - company="_Test Company", - ) - pi = make_purchase_invoice(discount_account=discount_account, rate=45) - - expected_gle = [ - ["_Test Account Cost for Goods Sold - _TC", 250.0, 0.0, nowdate()], - ["Creditors - _TC", 0.0, 225.0, nowdate()], - ["Discount Account - _TC", 0.0, 25.0, nowdate()], - ] - - check_gl_entries(self, pi.name, expected_gle, nowdate()) - - @change_settings("Buying Settings", {"enable_discount_accounting": 1}) - def test_additional_discount_for_purchase_invoice_with_discount_accounting_enabled(self): - - additional_discount_account = create_account( - account_name="Discount Account", - parent_account="Indirect Expenses - _TC", - company="_Test Company", - ) - - pi = make_purchase_invoice(do_not_save=1, parent_cost_center="Main - _TC") - pi.apply_discount_on = "Grand Total" - pi.additional_discount_account = additional_discount_account - pi.additional_discount_percentage = 10 - pi.disable_rounded_total = 1 - pi.append( - "taxes", - { - "charge_type": "On Net Total", - "account_head": "_Test Account VAT - _TC", - "cost_center": "Main - _TC", - "description": "Test", - "rate": 10, - }, - ) - pi.submit() - - expected_gle = [ - ["_Test Account Cost for Goods Sold - _TC", 250.0, 0.0, nowdate()], - ["_Test Account VAT - _TC", 25.0, 0.0, nowdate()], - ["Creditors - _TC", 0.0, 247.5, nowdate()], - ["Discount Account - _TC", 0.0, 27.5, nowdate()], - ] - - check_gl_entries(self, pi.name, expected_gle, nowdate()) - def test_purchase_invoice_change_naming_series(self): pi = frappe.copy_doc(test_records[1]) pi.insert() @@ -1596,6 +1543,37 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin): pi.save() self.assertEqual(pi.items[0].conversion_factor, 1000) + def test_batch_expiry_for_purchase_invoice(self): + from erpnext.controllers.sales_and_purchase_return import make_return_doc + + item = self.make_item( + "_Test Batch Item For Return Check", + { + "is_purchase_item": 1, + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "TBIRC.#####", + }, + ) + + pi = make_purchase_invoice( + qty=1, + item_code=item.name, + update_stock=True, + ) + + pi.load_from_db() + batch_no = pi.items[0].batch_no + self.assertTrue(batch_no) + + frappe.db.set_value("Batch", batch_no, "expiry_date", add_days(nowdate(), -1)) + + return_pi = make_return_doc(pi.doctype, pi.name) + return_pi.save().submit() + + self.assertTrue(return_pi.docstatus == 1) + def check_gl_entries(doc, voucher_no, expected_gle, posting_date): gl_entries = frappe.db.sql( 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 7fa2fe2a66..1fa7e7f3fc 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -49,6 +49,7 @@ "pricing_rules", "stock_uom_rate", "is_free_item", + "apply_tds", "section_break_22", "net_rate", "net_amount", @@ -74,7 +75,6 @@ "manufacturer_part_no", "accounting", "expense_account", - "discount_account", "col_break5", "is_fixed_asset", "asset_location", @@ -215,6 +215,7 @@ "reqd": 1 }, { + "default": "1", "depends_on": "eval:doc.uom != doc.stock_uom", "fieldname": "conversion_factor", "fieldtype": "Float", @@ -712,6 +713,7 @@ "label": "Valuation Rate", "no_copy": 1, "options": "Company:company:default_currency", + "precision": "6", "print_hide": 1, "read_only": 1 }, @@ -820,6 +822,7 @@ }, { "collapsible": 1, + "collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount", "fieldname": "section_break_26", "fieldtype": "Section Break", "label": "Discount and Margin" @@ -860,24 +863,24 @@ "print_hide": 1, "read_only": 1 }, - { - "fieldname": "discount_account", - "fieldtype": "Link", - "label": "Discount Account", - "options": "Account" - }, { "fieldname": "product_bundle", "fieldtype": "Link", "label": "Product Bundle", "options": "Product Bundle", "read_only": 1 + }, + { + "default": "1", + "fieldname": "apply_tds", + "fieldtype": "Check", + "label": "Apply TDS" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2022-06-17 05:31:10.520171", + "modified": "2022-11-29 13:01:20.438217", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice Item", diff --git a/erpnext/erpnext_integrations/doctype/taxjar_nexus/__init__.py b/erpnext/accounts/doctype/repost_payment_ledger/__init__.py similarity index 100% rename from erpnext/erpnext_integrations/doctype/taxjar_nexus/__init__.py rename to erpnext/accounts/doctype/repost_payment_ledger/__init__.py diff --git a/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.js b/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.js new file mode 100644 index 0000000000..6801408c7b --- /dev/null +++ b/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.js @@ -0,0 +1,53 @@ +// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Repost Payment Ledger', { + setup: function(frm) { + frm.set_query("voucher_type", () => { + return { + filters: { + name: ['in', ['Purchase Invoice', 'Sales Invoice', 'Payment Entry', 'Journal Entry']] + } + }; + }); + + frm.fields_dict['repost_vouchers'].grid.get_field('voucher_type').get_query = function(doc) { + return { + filters: { + name: ['in', ['Purchase Invoice', 'Sales Invoice', 'Payment Entry', 'Journal Entry']] + } + } + } + + frm.fields_dict['repost_vouchers'].grid.get_field('voucher_no').get_query = function(doc) { + if (doc.company) { + return { + filters: { + company: doc.company, + docstatus: 1 + } + } + } + } + + }, + refresh: function(frm) { + + if (frm.doc.docstatus==1 && ['Queued', 'Failed'].find(x => x == frm.doc.repost_status)) { + frm.set_intro(__("Use 'Repost in background' button to trigger background job. Job can only be triggered when document is in Queued or Failed status.")); + var btn_label = __("Repost in background") + + frm.add_custom_button(btn_label, () => { + frappe.call({ + method: 'erpnext.accounts.doctype.repost_payment_ledger.repost_payment_ledger.execute_repost_payment_ledger', + args: { + docname: frm.doc.name, + } + }); + frappe.msgprint(__('Reposting in the background.')); + }); + } + + } +}); + diff --git a/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.json b/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.json new file mode 100644 index 0000000000..5175fd169f --- /dev/null +++ b/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.json @@ -0,0 +1,159 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2022-10-19 21:59:33.553852", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "filters_section", + "company", + "posting_date", + "column_break_4", + "voucher_type", + "add_manually", + "status_section", + "repost_status", + "repost_error_log", + "selected_vouchers_section", + "repost_vouchers", + "amended_from" + ], + "fields": [ + { + "default": "Today", + "fieldname": "posting_date", + "fieldtype": "Date", + "label": "Posting Date", + "reqd": 1 + }, + { + "fieldname": "voucher_type", + "fieldtype": "Link", + "label": "Voucher Type", + "options": "DocType" + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Repost Payment Ledger", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "company", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Company", + "options": "Company", + "reqd": 1 + }, + { + "fieldname": "selected_vouchers_section", + "fieldtype": "Section Break", + "label": "Vouchers" + }, + { + "fieldname": "filters_section", + "fieldtype": "Section Break", + "label": "Filters" + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "fieldname": "repost_vouchers", + "fieldtype": "Table", + "label": "Selected Vouchers", + "options": "Repost Payment Ledger Items" + }, + { + "fieldname": "repost_status", + "fieldtype": "Select", + "label": "Repost Status", + "options": "\nQueued\nFailed\nCompleted", + "read_only": 1 + }, + { + "fieldname": "status_section", + "fieldtype": "Section Break", + "label": "Status" + }, + { + "default": "0", + "description": "Ignore Voucher Type filter and Select Vouchers Manually", + "fieldname": "add_manually", + "fieldtype": "Check", + "label": "Add Manually" + }, + { + "depends_on": "eval:doc.repost_error_log", + "fieldname": "repost_error_log", + "fieldtype": "Long Text", + "label": "Repost Error Log" + } + ], + "index_web_pages_for_search": 1, + "is_submittable": 1, + "links": [], + "modified": "2022-11-08 07:38:40.079038", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Repost Payment Ledger", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts Manager", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "create": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts User", + "share": 1, + "write": 1 + }, + { + "email": 1, + "export": 1, + "permlevel": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.py b/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.py new file mode 100644 index 0000000000..209cad4f90 --- /dev/null +++ b/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.py @@ -0,0 +1,109 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import copy + +import frappe +from frappe import _, qb +from frappe.model.document import Document +from frappe.query_builder.custom import ConstantColumn + +from erpnext.accounts.utils import _delete_pl_entries, create_payment_ledger_entry + +VOUCHER_TYPES = ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"] + + +def repost_ple_for_voucher(voucher_type, voucher_no, gle_map=None): + if voucher_type and voucher_no and gle_map: + _delete_pl_entries(voucher_type, voucher_no) + create_payment_ledger_entry(gle_map, cancel=0) + + +@frappe.whitelist() +def start_payment_ledger_repost(docname=None): + """ + Repost Payment Ledger Entries for Vouchers through Background Job + """ + if docname: + repost_doc = frappe.get_doc("Repost Payment Ledger", docname) + if repost_doc.docstatus.is_submitted() and repost_doc.repost_status in ["Queued", "Failed"]: + try: + for entry in repost_doc.repost_vouchers: + doc = frappe.get_doc(entry.voucher_type, entry.voucher_no) + + if doc.doctype in ["Payment Entry", "Journal Entry"]: + gle_map = doc.build_gl_map() + else: + gle_map = doc.get_gl_entries() + + repost_ple_for_voucher(entry.voucher_type, entry.voucher_no, gle_map) + + frappe.db.set_value(repost_doc.doctype, repost_doc.name, "repost_error_log", "") + frappe.db.set_value(repost_doc.doctype, repost_doc.name, "repost_status", "Completed") + except Exception as e: + frappe.db.rollback() + + traceback = frappe.get_traceback() + if traceback: + message = "Traceback:
" + traceback + frappe.db.set_value(repost_doc.doctype, repost_doc.name, "repost_error_log", message) + + frappe.db.set_value(repost_doc.doctype, repost_doc.name, "repost_status", "Failed") + + +class RepostPaymentLedger(Document): + def __init__(self, *args, **kwargs): + super(RepostPaymentLedger, self).__init__(*args, **kwargs) + self.vouchers = [] + + def before_validate(self): + self.load_vouchers_based_on_filters() + self.set_status() + + def load_vouchers_based_on_filters(self): + if not self.add_manually: + self.repost_vouchers.clear() + self.get_vouchers() + self.extend("repost_vouchers", copy.deepcopy(self.vouchers)) + + def get_vouchers(self): + self.vouchers.clear() + + filter_on_voucher_types = [self.voucher_type] if self.voucher_type else VOUCHER_TYPES + + for vtype in filter_on_voucher_types: + doc = qb.DocType(vtype) + doctype_name = ConstantColumn(vtype) + query = ( + qb.from_(doc) + .select(doctype_name.as_("voucher_type"), doc.name.as_("voucher_no")) + .where( + (doc.docstatus == 1) + & (doc.company == self.company) + & (doc.posting_date.gte(self.posting_date)) + ) + ) + entries = query.run(as_dict=True) + self.vouchers.extend(entries) + + def set_status(self): + if self.docstatus == 0: + self.repost_status = "Queued" + + def on_submit(self): + execute_repost_payment_ledger(self.name) + frappe.msgprint(_("Repost started in the background")) + + +@frappe.whitelist() +def execute_repost_payment_ledger(docname): + """Repost Payment Ledger Entries by background job.""" + + job_name = "payment_ledger_repost_" + docname + + frappe.enqueue( + method="erpnext.accounts.doctype.repost_payment_ledger.repost_payment_ledger.start_payment_ledger_repost", + docname=docname, + is_async=True, + job_name=job_name, + ) diff --git a/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger_list.js b/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger_list.js new file mode 100644 index 0000000000..e0451845ce --- /dev/null +++ b/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger_list.js @@ -0,0 +1,12 @@ +frappe.listview_settings["Repost Payment Ledger"] = { + add_fields: ["repost_status"], + get_indicator: function(doc) { + var colors = { + 'Queued': 'orange', + 'Completed': 'green', + 'Failed': 'red', + }; + let status = doc.repost_status; + return [__(status), colors[status], 'status,=,'+status]; + }, +}; diff --git a/erpnext/accounts/doctype/repost_payment_ledger/test_repost_payment_ledger.py b/erpnext/accounts/doctype/repost_payment_ledger/test_repost_payment_ledger.py new file mode 100644 index 0000000000..781726a1e3 --- /dev/null +++ b/erpnext/accounts/doctype/repost_payment_ledger/test_repost_payment_ledger.py @@ -0,0 +1,9 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestRepostPaymentLedger(FrappeTestCase): + pass diff --git a/erpnext/erpnext_integrations/doctype/taxjar_settings/__init__.py b/erpnext/accounts/doctype/repost_payment_ledger_items/__init__.py similarity index 100% rename from erpnext/erpnext_integrations/doctype/taxjar_settings/__init__.py rename to erpnext/accounts/doctype/repost_payment_ledger_items/__init__.py diff --git a/erpnext/accounts/doctype/repost_payment_ledger_items/repost_payment_ledger_items.json b/erpnext/accounts/doctype/repost_payment_ledger_items/repost_payment_ledger_items.json new file mode 100644 index 0000000000..93005ee137 --- /dev/null +++ b/erpnext/accounts/doctype/repost_payment_ledger_items/repost_payment_ledger_items.json @@ -0,0 +1,35 @@ +{ + "actions": [], + "creation": "2022-10-20 10:44:18.796489", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "voucher_type", + "voucher_no" + ], + "fields": [ + { + "fieldname": "voucher_type", + "fieldtype": "Link", + "label": "Voucher Type", + "options": "DocType" + }, + { + "fieldname": "voucher_no", + "fieldtype": "Dynamic Link", + "label": "Voucher No", + "options": "voucher_type" + } + ], + "istable": 1, + "links": [], + "modified": "2022-10-28 14:47:11.838109", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Repost Payment Ledger Items", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/erpnext/regional/doctype/ksa_vat_sales_account/ksa_vat_sales_account.py b/erpnext/accounts/doctype/repost_payment_ledger_items/repost_payment_ledger_items.py similarity index 51% rename from erpnext/regional/doctype/ksa_vat_sales_account/ksa_vat_sales_account.py rename to erpnext/accounts/doctype/repost_payment_ledger_items/repost_payment_ledger_items.py index 7c2689f530..fb19e84f26 100644 --- a/erpnext/regional/doctype/ksa_vat_sales_account/ksa_vat_sales_account.py +++ b/erpnext/accounts/doctype/repost_payment_ledger_items/repost_payment_ledger_items.py @@ -1,9 +1,9 @@ -# Copyright (c) 2021, Havenir Solutions and contributors +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt # import frappe from frappe.model.document import Document -class KSAVATSalesAccount(Document): +class RepostPaymentLedgerItems(Document): pass diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index ba4cdd606e..47e3f9b935 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -34,7 +34,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e super.onload(); this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet', 'POS Invoice Merge Log', - 'POS Closing Entry', 'Journal Entry', 'Payment Entry']; + 'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger"]; if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) { // show debit_to in print format @@ -64,6 +64,25 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e this.frm.toggle_reqd("due_date", !this.frm.doc.is_return); + if (this.frm.doc.repost_required && this.frm.doc.docstatus===1) { + this.frm.set_intro(__("Accounting entries for this invoice needs to be reposted. Please click on 'Repost' button to update.")); + this.frm.add_custom_button(__('Repost Accounting Entries'), + () => { + this.frm.call({ + doc: this.frm.doc, + method: 'repost_accounting_entries', + freeze: true, + freeze_message: __('Reposting...'), + callback: (r) => { + if (!r.exc) { + frappe.msgprint(__('Accounting Entries are reposted')); + me.frm.refresh(); + } + } + }); + }).removeClass('btn-default').addClass('btn-warning'); + } + if (this.frm.doc.is_return) { this.frm.return_print_format = "Sales Invoice Return"; } @@ -479,9 +498,13 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e is_cash_or_non_trade_discount() { this.frm.set_df_property("additional_discount_account", "hidden", 1 - this.frm.doc.is_cash_or_non_trade_discount); + this.frm.set_df_property("additional_discount_account", "reqd", this.frm.doc.is_cash_or_non_trade_discount); + if (!this.frm.doc.is_cash_or_non_trade_discount) { this.frm.set_value("additional_discount_account", ""); } + + this.calculate_taxes_and_totals(); } }; @@ -1024,7 +1047,7 @@ var select_loyalty_program = function(frm, loyalty_programs) { ] }); - dialog.set_primary_action(__("Set"), function() { + dialog.set_primary_action(__("Set Loyalty Program"), function() { dialog.hide(); return frappe.call({ method: "frappe.client.set_value", diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 1c9d3fbfb2..2f4e45e618 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -12,44 +12,29 @@ "customer", "customer_name", "tax_id", - "pos_profile", - "is_pos", - "is_consolidated", - "is_return", - "is_debit_note", - "update_billed_amount_in_sales_order", - "column_break1", "company", "company_tax_id", + "column_break1", "posting_date", "posting_time", "set_posting_time", "due_date", + "column_break_14", + "is_pos", + "pos_profile", + "is_consolidated", + "is_return", "return_against", + "update_billed_amount_in_sales_order", + "is_debit_note", "amended_from", "accounting_dimensions_section", - "project", - "dimension_col_break", "cost_center", - "customer_po_details", - "po_no", - "column_break_23", - "po_date", - "address_and_contact", - "customer_address", - "address_display", - "contact_person", - "contact_display", - "contact_mobile", - "contact_email", - "territory", - "col_break4", - "shipping_address_name", - "shipping_address", - "company_address", - "company_address_display", - "dispatch_address_name", - "dispatch_address", + "dimension_col_break", + "project", + "column_break_27", + "campaign", + "source", "currency_and_price_list", "currency", "conversion_rate", @@ -58,60 +43,37 @@ "price_list_currency", "plc_conversion_rate", "ignore_pricing_rule", - "sec_warehouse", - "set_warehouse", - "column_break_55", - "set_target_warehouse", "items_section", - "update_stock", "scan_barcode", + "update_stock", + "column_break_39", + "set_warehouse", + "set_target_warehouse", + "section_break_42", "items", - "pricing_rule_details", - "pricing_rules", - "packing_list", - "packed_items", - "product_bundle_help", - "time_sheet_list", - "timesheets", - "total_billing_amount", - "total_billing_hours", "section_break_30", "total_qty", + "total_net_weight", + "column_break_32", "base_total", "base_net_total", - "column_break_32", - "total_net_weight", + "column_break_52", "total", "net_total", "taxes_section", + "tax_category", "taxes_and_charges", "column_break_38", "shipping_rule", - "tax_category", + "column_break_55", + "incoterm", + "named_place", "section_break_40", "taxes", - "sec_tax_breakup", - "other_charges_calculation", "section_break_43", "base_total_taxes_and_charges", "column_break_47", "total_taxes_and_charges", - "loyalty_points_redemption", - "loyalty_points", - "loyalty_amount", - "redeem_loyalty_points", - "column_break_77", - "loyalty_program", - "loyalty_redemption_account", - "loyalty_redemption_cost_center", - "section_break_49", - "apply_discount_on", - "is_cash_or_non_trade_discount", - "base_discount_amount", - "additional_discount_account", - "column_break_51", - "additional_discount_percentage", - "discount_amount", "totals", "base_grand_total", "base_rounding_adjustment", @@ -125,21 +87,28 @@ "total_advance", "outstanding_amount", "disable_rounded_total", - "column_break4", - "write_off_amount", - "base_write_off_amount", - "write_off_outstanding_amount_automatically", - "column_break_74", - "write_off_account", - "write_off_cost_center", - "advances_section", - "allocate_advances_automatically", - "get_advances", - "advances", - "payment_schedule_section", - "ignore_default_payment_terms_template", - "payment_terms_template", - "payment_schedule", + "section_break_49", + "apply_discount_on", + "base_discount_amount", + "is_cash_or_non_trade_discount", + "additional_discount_account", + "column_break_51", + "additional_discount_percentage", + "discount_amount", + "sec_tax_breakup", + "other_charges_calculation", + "pricing_rule_details", + "pricing_rules", + "packing_list", + "packed_items", + "product_bundle_help", + "time_sheet_list", + "timesheets", + "section_break_104", + "total_billing_hours", + "column_break_106", + "total_billing_amount", + "payments_tab", "payments_section", "cash_bank_account", "payments", @@ -152,47 +121,96 @@ "column_break_90", "change_amount", "account_for_change_amount", + "advances_section", + "allocate_advances_automatically", + "get_advances", + "advances", + "write_off_section", + "write_off_amount", + "base_write_off_amount", + "write_off_outstanding_amount_automatically", + "column_break_74", + "write_off_account", + "write_off_cost_center", + "loyalty_points_redemption", + "redeem_loyalty_points", + "loyalty_points", + "loyalty_amount", + "column_break_77", + "loyalty_program", + "loyalty_redemption_account", + "loyalty_redemption_cost_center", + "contact_and_address_tab", + "address_and_contact", + "customer_address", + "address_display", + "col_break4", + "contact_person", + "contact_display", + "contact_mobile", + "contact_email", + "territory", + "shipping_address_section", + "shipping_address_name", + "shipping_address", + "shipping_addr_col_break", + "dispatch_address_name", + "dispatch_address", + "company_address_section", + "company_address", + "company_addr_col_break", + "company_address_display", + "terms_tab", + "payment_schedule_section", + "ignore_default_payment_terms_template", + "payment_terms_template", + "payment_schedule", "terms_section_break", "tc_name", "terms", - "edit_printing_settings", - "letter_head", - "group_same_items", - "select_print_heading", - "column_break_84", - "language", - "more_information", - "status", - "inter_company_invoice_reference", - "represents_company", - "customer_group", - "campaign", - "col_break23", - "is_internal_customer", - "is_discounted", - "source", + "more_info_tab", + "customer_po_details", + "po_no", + "column_break_23", + "po_date", "more_info", "debit_to", "party_account_currency", "is_opening", "column_break8", "unrealized_profit_loss_account", - "remarks", + "against_income_account", "sales_team_section_break", "sales_partner", - "column_break10", "amount_eligible_for_commission", + "column_break10", "commission_rate", "total_commission", "section_break2", "sales_team", + "edit_printing_settings", + "letter_head", + "group_same_items", + "column_break_84", + "select_print_heading", + "language", "subscription_section", "from_date", - "to_date", - "column_break_140", "auto_repeat", + "column_break_140", + "to_date", "update_auto_repeat_reference", - "against_income_account" + "more_information", + "status", + "inter_company_invoice_reference", + "represents_company", + "customer_group", + "col_break23", + "is_internal_customer", + "is_discounted", + "remarks", + "repost_required", + "connections_tab" ], "fields": [ { @@ -453,12 +471,11 @@ "label": "Customer's Purchase Order Date" }, { - "collapsible": 1, "fieldname": "address_and_contact", "fieldtype": "Section Break", "hide_days": 1, "hide_seconds": 1, - "label": "Address and Contact" + "label": "Billing Address" }, { "fieldname": "customer_address", @@ -560,7 +577,6 @@ { "fieldname": "company_address_display", "fieldtype": "Small Text", - "hidden": 1, "hide_days": 1, "hide_seconds": 1, "label": "Company Address", @@ -649,16 +665,8 @@ "hide_days": 1, "hide_seconds": 1, "label": "Ignore Pricing Rule", - "no_copy": 1, "print_hide": 1 }, - { - "fieldname": "sec_warehouse", - "fieldtype": "Section Break", - "hide_days": 1, - "hide_seconds": 1, - "label": "Warehouse" - }, { "depends_on": "update_stock", "fieldname": "set_warehouse", @@ -672,6 +680,7 @@ { "fieldname": "items_section", "fieldtype": "Section Break", + "hide_border": 1, "hide_days": 1, "hide_seconds": 1, "label": "Items", @@ -703,7 +712,6 @@ "fieldtype": "Table", "hide_days": 1, "hide_seconds": 1, - "label": "Items", "oldfieldname": "entries", "oldfieldtype": "Table", "options": "Sales Invoice Item", @@ -756,9 +764,10 @@ { "collapsible": 1, "collapsible_depends_on": "eval:doc.total_billing_amount > 0", - "depends_on": "eval: !doc.is_return", + "depends_on": "eval:!doc.is_return", "fieldname": "time_sheet_list", "fieldtype": "Section Break", + "hide_border": 1, "hide_days": 1, "hide_seconds": 1, "label": "Time Sheet List" @@ -846,6 +855,7 @@ "read_only": 1 }, { + "depends_on": "total_net_weight", "fieldname": "total_net_weight", "fieldtype": "Float", "hide_days": 1, @@ -857,8 +867,10 @@ { "fieldname": "taxes_section", "fieldtype": "Section Break", + "hide_border": 1, "hide_days": 1, "hide_seconds": 1, + "label": "Taxes and Charges", "oldfieldtype": "Section Break", "options": "fa fa-money" }, @@ -901,6 +913,7 @@ { "fieldname": "section_break_40", "fieldtype": "Section Break", + "hide_border": 1, "hide_days": 1, "hide_seconds": 1 }, @@ -1026,6 +1039,7 @@ "read_only": 1 }, { + "allow_on_submit": 1, "depends_on": "redeem_loyalty_points", "fieldname": "loyalty_redemption_account", "fieldtype": "Link", @@ -1047,7 +1061,6 @@ }, { "collapsible": 1, - "collapsible_depends_on": "discount_amount", "fieldname": "section_break_49", "fieldtype": "Section Break", "hide_days": 1, @@ -1103,6 +1116,7 @@ "fieldtype": "Section Break", "hide_days": 1, "hide_seconds": 1, + "label": "Totals", "oldfieldtype": "Section Break", "options": "fa fa-money", "print_hide": 1 @@ -1284,8 +1298,6 @@ "print_hide": 1 }, { - "collapsible": 1, - "collapsible_depends_on": "eval:(!doc.is_pos && !doc.is_return)", "fieldname": "payment_schedule_section", "fieldtype": "Section Break", "hide_days": 1, @@ -1315,7 +1327,9 @@ "print_hide": 1 }, { - "depends_on": "eval:doc.is_pos===1||(doc.advances && doc.advances.length>0)", + "collapsible": 1, + "collapsible_depends_on": "eval:!doc.is_pos", + "depends_on": "eval:doc.is_pos===1", "fieldname": "payments_section", "fieldtype": "Section Break", "hide_days": 1, @@ -1324,6 +1338,7 @@ "options": "fa fa-money" }, { + "allow_on_submit": 1, "depends_on": "is_pos", "fieldname": "cash_bank_account", "fieldtype": "Link", @@ -1353,6 +1368,7 @@ "hide_seconds": 1 }, { + "depends_on": "eval: doc.is_pos || doc.redeem_loyalty_points", "fieldname": "base_paid_amount", "fieldtype": "Currency", "hide_days": 1, @@ -1384,10 +1400,13 @@ "read_only": 1 }, { + "collapsible": 1, + "depends_on": "is_pos", "fieldname": "section_break_88", "fieldtype": "Section Break", "hide_days": 1, - "hide_seconds": 1 + "hide_seconds": 1, + "label": "Changes" }, { "depends_on": "is_pos", @@ -1419,6 +1438,7 @@ "print_hide": 1 }, { + "allow_on_submit": 1, "depends_on": "is_pos", "fieldname": "account_for_change_amount", "fieldtype": "Link", @@ -1428,17 +1448,6 @@ "options": "Account", "print_hide": 1 }, - { - "collapsible": 1, - "collapsible_depends_on": "write_off_amount", - "depends_on": "grand_total", - "fieldname": "column_break4", - "fieldtype": "Section Break", - "hide_days": 1, - "hide_seconds": 1, - "label": "Write Off", - "width": "50%" - }, { "fieldname": "write_off_amount", "fieldtype": "Currency", @@ -1478,6 +1487,7 @@ "hide_seconds": 1 }, { + "allow_on_submit": 1, "fieldname": "write_off_account", "fieldtype": "Link", "hide_days": 1, @@ -1496,8 +1506,6 @@ "print_hide": 1 }, { - "collapsible": 1, - "collapsible_depends_on": "terms", "fieldname": "terms_section_break", "fieldtype": "Section Break", "hide_days": 1, @@ -1531,7 +1539,7 @@ "fieldtype": "Section Break", "hide_days": 1, "hide_seconds": 1, - "label": "Printing Settings" + "label": "Print Settings" }, { "allow_on_submit": 1, @@ -1592,7 +1600,7 @@ "fieldtype": "Section Break", "hide_days": 1, "hide_seconds": 1, - "label": "More Information" + "label": "Additional Info" }, { "fieldname": "inter_company_invoice_reference", @@ -1703,6 +1711,7 @@ "read_only": 1 }, { + "allow_on_submit": 1, "default": "No", "fieldname": "is_opening", "fieldtype": "Select", @@ -1767,6 +1776,8 @@ "width": "50%" }, { + "fetch_from": "sales_partner.commission_rate", + "fetch_if_empty": 1, "fieldname": "commission_rate", "fieldtype": "Float", "hide_days": 1, @@ -1815,7 +1826,7 @@ "fieldtype": "Section Break", "hide_days": 1, "hide_seconds": 1, - "label": "Subscription Section" + "label": "Subscription" }, { "allow_on_submit": 1, @@ -1917,6 +1928,7 @@ "read_only": 1 }, { + "allow_on_submit": 1, "depends_on": "eval:doc.is_internal_customer", "description": "Unrealized Profit / Loss account for intra-company transfers", "fieldname": "unrealized_profit_loss_account", @@ -1936,10 +1948,6 @@ "options": "Company", "read_only": 1 }, - { - "fieldname": "column_break_55", - "fieldtype": "Column Break" - }, { "depends_on": "eval: doc.is_internal_customer && doc.update_stock", "fieldname": "set_target_warehouse", @@ -1963,6 +1971,7 @@ "label": "Disable Rounded Total" }, { + "allow_on_submit": 1, "fieldname": "additional_discount_account", "fieldtype": "Link", "label": "Discount Account", @@ -2010,6 +2019,118 @@ "fieldname": "is_cash_or_non_trade_discount", "fieldtype": "Check", "label": "Is Cash or Non Trade Discount" + }, + { + "fieldname": "contact_and_address_tab", + "fieldtype": "Tab Break", + "label": "Contact & Address" + }, + { + "fieldname": "payments_tab", + "fieldtype": "Tab Break", + "label": "Payments" + }, + { + "fieldname": "terms_tab", + "fieldtype": "Tab Break", + "label": "Terms" + }, + { + "fieldname": "more_info_tab", + "fieldtype": "Tab Break", + "label": "More Info" + }, + { + "fieldname": "connections_tab", + "fieldtype": "Tab Break", + "label": "Connections", + "show_dashboard": 1 + }, + { + "fieldname": "column_break_14", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_39", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_42", + "fieldtype": "Section Break", + "hide_border": 1, + "hide_days": 1, + "hide_seconds": 1 + }, + { + "fieldname": "column_break_55", + "fieldtype": "Column Break" + }, + { + "fieldname": "shipping_address_section", + "fieldtype": "Section Break", + "label": "Shipping Address" + }, + { + "fieldname": "company_address_section", + "fieldtype": "Section Break", + "label": "Company Address" + }, + { + "fieldname": "shipping_addr_col_break", + "fieldtype": "Column Break" + }, + { + "fieldname": "company_addr_col_break", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_27", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_52", + "fieldtype": "Column Break" + }, + { + "depends_on": "eval:(!doc.is_return && doc.total_billing_amount > 0)", + "fieldname": "section_break_104", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_106", + "fieldtype": "Column Break" + }, + { + "collapsible": 1, + "collapsible_depends_on": "write_off_amount", + "depends_on": "is_pos", + "fieldname": "write_off_section", + "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1, + "label": "Write Off", + "width": "50%" + }, + { + "default": "0", + "fieldname": "repost_required", + "fieldtype": "Check", + "hidden": 1, + "label": "Repost Required", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "incoterm", + "fieldtype": "Link", + "label": "Incoterm", + "options": "Incoterm" + }, + { + "depends_on": "incoterm", + "fieldname": "named_place", + "fieldtype": "Data", + "label": "Named Place" } ], "icon": "fa fa-file-text", @@ -2022,7 +2143,7 @@ "link_fieldname": "consolidated_invoice" } ], - "modified": "2022-07-11 17:43:56.435382", + "modified": "2023-01-28 19:45:47.538163", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index f8c26d1e92..5cda276087 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -7,20 +7,13 @@ from frappe import _, msgprint, throw from frappe.contacts.doctype.address.address import get_address_display from frappe.model.mapper import get_mapped_doc from frappe.model.utils import get_fetch_values -from frappe.utils import ( - add_days, - add_months, - cint, - cstr, - flt, - formatdate, - get_link_to_form, - getdate, - nowdate, -) +from frappe.utils import add_days, cint, cstr, flt, formatdate, get_link_to_form, getdate, nowdate import erpnext from erpnext.accounts.deferred_revenue import validate_service_stop_date +from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( + get_accounting_dimensions, +) from erpnext.accounts.doctype.loyalty_program.loyalty_program import ( get_loyalty_program_details_with_points, validate_loyalty_points, @@ -32,10 +25,12 @@ from erpnext.accounts.general_ledger import get_round_off_account_and_cost_cente from erpnext.accounts.party import get_due_date, get_party_account, get_party_details from erpnext.accounts.utils import get_account_currency from erpnext.assets.doctype.asset.depreciation import ( + depreciate_asset, get_disposal_account_and_cost_center, get_gl_entries_on_asset_disposal, get_gl_entries_on_asset_regain, - make_depreciation_entry, + reset_depreciation_schedule, + reverse_depreciation_entry_made_after_disposal, ) from erpnext.controllers.accounts_controller import validate_account_head from erpnext.controllers.selling_controller import SellingController @@ -108,13 +103,11 @@ class SalesInvoice(SellingController): self.validate_debit_to_acc() self.clear_unallocated_advances("Sales Invoice Advance", "advances") self.add_remarks() - self.validate_write_off_account() - self.validate_account_for_change_amount() self.validate_fixed_asset() self.set_income_account_for_fixed_assets() self.validate_item_cost_centers() - self.validate_income_account() self.check_conversion_rate() + self.validate_accounts() validate_inter_company_party( self.doctype, self.customer, self.company, self.inter_company_invoice_reference @@ -178,6 +171,11 @@ class SalesInvoice(SellingController): self.reset_default_field_value("set_warehouse", "items", "warehouse") + def validate_accounts(self): + self.validate_write_off_account() + self.validate_account_for_change_amount() + self.validate_income_account() + 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: @@ -186,7 +184,7 @@ class SalesInvoice(SellingController): if self.update_stock: frappe.throw(_("'Update Stock' cannot be checked for fixed asset sale")) - elif asset.status in ("Scrapped", "Cancelled") or ( + elif asset.status in ("Scrapped", "Cancelled", "Capitalized", "Decapitalized") or ( asset.status == "Sold" and not self.is_return ): frappe.throw( @@ -375,7 +373,8 @@ class SalesInvoice(SellingController): if self.update_stock == 1: self.repost_future_sle_and_gle() - frappe.db.set(self, "status", "Cancelled") + self.db_set("status", "Cancelled") + self.db_set("repost_required", 0) if ( frappe.db.get_single_value("Selling Settings", "sales_update_frequency") == "Each Transaction" @@ -398,6 +397,8 @@ class SalesInvoice(SellingController): "GL Entry", "Stock Ledger Entry", "Repost Item Valuation", + "Repost Payment Ledger", + "Repost Payment Ledger Items", "Payment Ledger Entry", ) @@ -522,6 +523,93 @@ class SalesInvoice(SellingController): def on_update(self): self.set_paid_amount() + def on_update_after_submit(self): + if hasattr(self, "repost_required"): + needs_repost = 0 + + # Check if any field affecting accounting entry is altered + doc_before_update = self.get_doc_before_save() + accounting_dimensions = get_accounting_dimensions() + ["cost_center", "project"] + + # Check if opening entry check updated + if doc_before_update.get("is_opening") != self.is_opening: + needs_repost = 1 + + if not needs_repost: + # Parent Level Accounts excluding party account + for field in ( + "additional_discount_account", + "cash_bank_account", + "account_for_change_amount", + "write_off_account", + "loyalty_redemption_account", + "unrealized_profit_loss_account", + ): + if doc_before_update.get(field) != self.get(field): + needs_repost = 1 + break + + # Check for parent accounting dimensions + for dimension in accounting_dimensions: + if doc_before_update.get(dimension) != self.get(dimension): + needs_repost = 1 + break + + # Check for child tables + if self.check_if_child_table_updated( + "items", + doc_before_update, + ("income_account", "expense_account", "discount_account"), + accounting_dimensions, + ): + needs_repost = 1 + + if self.check_if_child_table_updated( + "taxes", doc_before_update, ("account_head",), accounting_dimensions + ): + needs_repost = 1 + + self.validate_accounts() + + # validate if deferred revenue is enabled for any item + # Don't allow to update the invoice if deferred revenue is enabled + if needs_repost: + for item in self.get("items"): + if item.enable_deferred_revenue: + frappe.throw( + _( + "Deferred Revenue is enabled for item {0}. You cannot update the invoice after submission." + ).format(item.item_code) + ) + + self.db_set("repost_required", needs_repost) + + def check_if_child_table_updated( + self, child_table, doc_before_update, fields_to_check, accounting_dimensions + ): + # Check if any field affecting accounting entry is altered + for index, item in enumerate(self.get(child_table)): + for field in fields_to_check: + if doc_before_update.get(child_table)[index].get(field) != item.get(field): + return True + + for dimension in accounting_dimensions: + if doc_before_update.get(child_table)[index].get(dimension) != item.get(dimension): + return True + + return False + + @frappe.whitelist() + def repost_accounting_entries(self): + if self.repost_required: + self.docstatus = 2 + self.make_gl_entries_on_cancel() + self.docstatus = 1 + self.make_gl_entries() + self.db_set("repost_required", 0) + else: + frappe.throw(_("No updates pending for reposting")) + def set_paid_amount(self): paid_amount = 0.0 base_paid_amount = 0.0 @@ -710,6 +798,7 @@ class SalesInvoice(SellingController): if ( cint(frappe.db.get_single_value("Selling Settings", "maintain_same_sales_rate")) and not self.is_return + and not self.is_internal_customer ): self.validate_rate_with_reference_doc( [["Sales Order", "sales_order", "so_detail"], ["Delivery Note", "delivery_note", "dn_detail"]] @@ -1033,22 +1122,6 @@ class SalesInvoice(SellingController): ) ) - if self.apply_discount_on == "Grand Total" and self.get("is_cash_or_discount_account"): - gl_entries.append( - self.get_gl_dict( - { - "account": self.additional_discount_account, - "against": self.debit_to, - "debit": self.base_discount_amount, - "debit_in_account_currency": self.discount_amount, - "cost_center": self.cost_center, - "project": self.project, - }, - self.currency, - item=self, - ) - ) - def make_tax_gl_entries(self, gl_entries): enable_discount_accounting = cint( frappe.db.get_single_value("Selling Settings", "enable_discount_accounting") @@ -1107,23 +1180,38 @@ class SalesInvoice(SellingController): if self.is_return: fixed_asset_gl_entries = get_gl_entries_on_asset_regain( - asset, item.base_net_amount, item.finance_book + asset, item.base_net_amount, item.finance_book, self.get("doctype"), self.get("name") ) asset.db_set("disposal_date", None) if asset.calculate_depreciation: - self.reverse_depreciation_entry_made_after_sale(asset) - self.reset_depreciation_schedule(asset) + posting_date = frappe.db.get_value("Sales Invoice", self.return_against, "posting_date") + reverse_depreciation_entry_made_after_disposal(asset, posting_date) + notes = _( + "This schedule was created when Asset {0} was returned after being sold through Sales Invoice {1}." + ).format( + get_link_to_form(asset.doctype, asset.name), + get_link_to_form(self.doctype, self.get("name")), + ) + reset_depreciation_schedule(asset, self.posting_date, notes) + asset.reload() else: + if asset.calculate_depreciation: + notes = _( + "This schedule was created when Asset {0} was sold through Sales Invoice {1}." + ).format( + get_link_to_form(asset.doctype, asset.name), + get_link_to_form(self.doctype, self.get("name")), + ) + depreciate_asset(asset, self.posting_date, notes) + asset.reload() + fixed_asset_gl_entries = get_gl_entries_on_asset_disposal( - asset, item.base_net_amount, item.finance_book + asset, item.base_net_amount, item.finance_book, self.get("doctype"), self.get("name") ) asset.db_set("disposal_date", self.posting_date) - if asset.calculate_depreciation: - self.depreciate_asset(asset) - for gle in fixed_asset_gl_entries: gle["against"] = self.customer gl_entries.append(self.get_gl_dict(gle, item=item)) @@ -1177,101 +1265,6 @@ class SalesInvoice(SellingController): self.check_finance_books(item, asset) return asset - def check_finance_books(self, item, asset): - if ( - len(asset.finance_books) > 1 and not item.finance_book and asset.finance_books[0].finance_book - ): - frappe.throw( - _("Select finance book for the item {0} at row {1}").format(item.item_code, item.idx) - ) - - def depreciate_asset(self, asset): - asset.flags.ignore_validate_update_after_submit = True - asset.prepare_depreciation_data(date_of_sale=self.posting_date) - asset.save() - - make_depreciation_entry(asset.name, self.posting_date) - - def reset_depreciation_schedule(self, asset): - asset.flags.ignore_validate_update_after_submit = True - - # recreate original depreciation schedule of the asset - asset.prepare_depreciation_data(date_of_return=self.posting_date) - - self.modify_depreciation_schedule_for_asset_repairs(asset) - asset.save() - - def modify_depreciation_schedule_for_asset_repairs(self, asset): - asset_repairs = frappe.get_all( - "Asset Repair", filters={"asset": asset.name}, fields=["name", "increase_in_asset_life"] - ) - - for repair in asset_repairs: - if repair.increase_in_asset_life: - asset_repair = frappe.get_doc("Asset Repair", repair.name) - asset_repair.modify_depreciation_schedule() - asset.prepare_depreciation_data() - - def reverse_depreciation_entry_made_after_sale(self, asset): - from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry - - posting_date_of_original_invoice = self.get_posting_date_of_sales_invoice() - - row = -1 - finance_book = asset.get("schedules")[0].get("finance_book") - for schedule in asset.get("schedules"): - if schedule.finance_book != finance_book: - row = 0 - finance_book = schedule.finance_book - else: - row += 1 - - if schedule.schedule_date == posting_date_of_original_invoice: - if not self.sale_was_made_on_original_schedule_date( - asset, schedule, row, posting_date_of_original_invoice - ) or self.sale_happens_in_the_future(posting_date_of_original_invoice): - - reverse_journal_entry = make_reverse_journal_entry(schedule.journal_entry) - reverse_journal_entry.posting_date = nowdate() - frappe.flags.is_reverse_depr_entry = True - reverse_journal_entry.submit() - - 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): - return frappe.db.get_value("Sales Invoice", self.return_against, "posting_date") - - # if the invoice had been posted on the date the depreciation was initially supposed to happen, the depreciation shouldn't be undone - def sale_was_made_on_original_schedule_date( - self, asset, schedule, row, posting_date_of_original_invoice - ): - for finance_book in asset.get("finance_books"): - if schedule.finance_book == finance_book.finance_book: - orginal_schedule_date = add_months( - finance_book.depreciation_start_date, row * cint(finance_book.frequency_of_depreciation) - ) - - if orginal_schedule_date == posting_date_of_original_invoice: - return True - return False - - def sale_happens_in_the_future(self, posting_date_of_original_invoice): - if posting_date_of_original_invoice > getdate(): - return True - - 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"): @@ -1416,7 +1409,11 @@ class SalesInvoice(SellingController): def make_write_off_gl_entry(self, gl_entries): # write off entries, applicable if only pos - if self.write_off_account and flt(self.write_off_amount, self.precision("write_off_amount")): + if ( + self.is_pos + and self.write_off_account + and flt(self.write_off_amount, self.precision("write_off_amount")) + ): write_off_account_currency = get_account_currency(self.write_off_account) default_cost_center = frappe.get_cached_value("Company", self.company, "cost_center") @@ -2103,13 +2100,13 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): target_detail_field = "sales_invoice_item" if doctype == "Sales Invoice" else "sales_order_item" source_document_warehouse_field = "target_warehouse" target_document_warehouse_field = "from_warehouse" + received_items = get_received_items(source_name, target_doctype, target_detail_field) else: source_doc = frappe.get_doc(doctype, source_name) target_doctype = "Sales Invoice" if doctype == "Purchase Invoice" else "Sales Order" source_document_warehouse_field = "from_warehouse" target_document_warehouse_field = "target_warehouse" - - received_items = get_received_items(source_name, target_doctype, target_detail_field) + received_items = {} validate_inter_company_transaction(source_doc, doctype) details = get_inter_company_details(source_doc, doctype) @@ -2133,6 +2130,9 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): update_address( target_doc, "shipping_address", "shipping_address_display", source_doc.customer_address ) + update_address( + target_doc, "billing_address", "billing_address_display", source_doc.customer_address + ) if currency: target_doc.currency = currency @@ -2177,6 +2177,19 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): def update_item(source, target, source_parent): target.qty = flt(source.qty) - received_items.get(source.name, 0.0) + if source.doctype == "Purchase Order Item" and target.doctype == "Sales Order Item": + target.purchase_order = source.parent + target.purchase_order_item = source.name + target.material_request = source.material_request + target.material_request_item = source.material_request_item + + if ( + source.get("purchase_order") + and source.get("purchase_order_item") + and target.doctype == "Purchase Invoice Item" + ): + target.purchase_order = source.purchase_order + target.po_detail = source.purchase_order_item item_field_map = { "doctype": target_doctype + " Item", @@ -2203,6 +2216,12 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): "serial_no": "serial_no", } ) + elif target_doctype == "Sales Order": + item_field_map["field_map"].update( + { + source_document_warehouse_field: "warehouse", + } + ) doclist = get_mapped_doc( doctype, @@ -2247,6 +2266,7 @@ def get_received_items(reference_name, doctype, reference_fieldname): def set_purchase_references(doc): # add internal PO or PR links if any + if doc.is_internal_transfer(): if doc.doctype == "Purchase Receipt": so_item_map = get_delivery_note_details(doc.inter_company_invoice_reference) @@ -2276,15 +2296,6 @@ def set_purchase_references(doc): warehouse_map, ) - if list(so_item_map.values()): - pd_item_map, parent_child_map, warehouse_map = get_pd_details( - "Purchase Order Item", so_item_map, "sales_order_item" - ) - - update_pi_items( - doc, "po_detail", "purchase_order", so_item_map, pd_item_map, parent_child_map, warehouse_map - ) - def update_pi_items( doc, @@ -2300,13 +2311,19 @@ def update_pi_items( item.set(parent_field, parent_child_map.get(sales_item_map.get(item.sales_invoice_item))) if doc.update_stock: item.warehouse = warehouse_map.get(sales_item_map.get(item.sales_invoice_item)) + if not item.warehouse and item.get("purchase_order") and item.get("purchase_order_item"): + item.warehouse = frappe.db.get_value( + "Purchase Order Item", item.purchase_order_item, "warehouse" + ) def update_pr_items(doc, sales_item_map, purchase_item_map, parent_child_map, warehouse_map): for item in doc.get("items"): - item.purchase_order_item = purchase_item_map.get(sales_item_map.get(item.delivery_note_item)) item.warehouse = warehouse_map.get(sales_item_map.get(item.delivery_note_item)) - item.purchase_order = parent_child_map.get(sales_item_map.get(item.delivery_note_item)) + if not item.warehouse and item.get("purchase_order") and item.get("purchase_order_item"): + item.warehouse = frappe.db.get_value( + "Purchase Order Item", item.purchase_order_item, "warehouse" + ) def get_delivery_note_details(internal_reference): @@ -2404,7 +2421,7 @@ def get_loyalty_programs(customer): lp_details = get_loyalty_programs(customer) if len(lp_details) == 1: - frappe.db.set(customer, "loyalty_program", lp_details[0]) + customer.db_set("loyalty_program", lp_details[0]) return lp_details else: return lp_details @@ -2535,7 +2552,6 @@ def create_dunning(source_name, target_doc=None): target.closing_text = letter_text.get("closing_text") target.language = letter_text.get("language") amounts = calculate_interest_and_amount( - target.posting_date, target.outstanding_amount, target.rate_of_interest, target.dunning_fee, diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice_list.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice_list.js index 1130284ecc..1605b151a1 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice_list.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice_list.js @@ -29,7 +29,7 @@ frappe.listview_settings['Sales Invoice'] = { }); listview.page.add_action_item(__("Payment"), ()=>{ - erpnext.bulk_transaction_processing.create(listview, "Sales Invoice", "Payment"); + erpnext.bulk_transaction_processing.create(listview, "Sales Invoice", "Payment Entry"); }); } }; diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 782e08e33b..0ffd9463e6 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -8,7 +8,7 @@ import frappe from frappe.model.dynamic_links import get_dynamic_link_map from frappe.model.naming import make_autoname from frappe.tests.utils import change_settings -from frappe.utils import add_days, flt, getdate, nowdate +from frappe.utils import add_days, flt, getdate, nowdate, today import erpnext from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account @@ -21,6 +21,9 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_inter_comp 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.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import ( + get_depr_schedule, +) 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 @@ -32,10 +35,20 @@ from erpnext.stock.doctype.stock_entry.test_stock_entry import ( get_qty_after_transaction, make_stock_entry, ) -from erpnext.stock.utils import get_incoming_rate +from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( + create_stock_reconciliation, +) +from erpnext.stock.utils import get_incoming_rate, get_stock_balance class TestSalesInvoice(unittest.TestCase): + def setUp(self): + from erpnext.stock.doctype.stock_ledger_entry.test_stock_ledger_entry import create_items + + create_items(["_Test Internal Transfer Item"], uoms=[{"uom": "Box", "conversion_factor": 10}]) + create_internal_parties() + setup_accounts() + def make(self): w = frappe.copy_doc(test_records[0]) w.is_pos = 0 @@ -955,7 +968,8 @@ class TestSalesInvoice(unittest.TestCase): pos_return.insert() pos_return.submit() - self.assertEqual(pos_return.get("payments")[0].amount, -1000) + self.assertEqual(pos_return.get("payments")[0].amount, -500) + self.assertEqual(pos_return.get("payments")[1].amount, -500) def test_pos_change_amount(self): make_pos_profile( @@ -1155,6 +1169,46 @@ class TestSalesInvoice(unittest.TestCase): frappe.db.sql("delete from `tabPOS Profile`") + def test_bin_details_of_packed_item(self): + from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle + from erpnext.stock.doctype.item.test_item import make_item + + # test Update Items with product bundle + if not frappe.db.exists("Item", "_Test Product Bundle Item New"): + bundle_item = make_item("_Test Product Bundle Item New", {"is_stock_item": 0}) + bundle_item.append( + "item_defaults", {"company": "_Test Company", "default_warehouse": "_Test Warehouse - _TC"} + ) + bundle_item.save(ignore_permissions=True) + + make_item("_Packed Item New 1", {"is_stock_item": 1}) + make_product_bundle("_Test Product Bundle Item New", ["_Packed Item New 1"], 2) + + si = create_sales_invoice( + item_code="_Test Product Bundle Item New", + update_stock=1, + warehouse="_Test Warehouse - _TC", + transaction_date=add_days(nowdate(), -1), + do_not_submit=1, + ) + + make_stock_entry(item="_Packed Item New 1", target="_Test Warehouse - _TC", qty=120, rate=100) + + bin_details = frappe.db.get_value( + "Bin", + {"item_code": "_Packed Item New 1", "warehouse": "_Test Warehouse - _TC"}, + ["actual_qty", "projected_qty", "ordered_qty"], + as_dict=1, + ) + + si.transaction_date = nowdate() + si.save() + + packed_item = si.packed_items[0] + self.assertEqual(flt(bin_details.actual_qty), flt(packed_item.actual_qty)) + self.assertEqual(flt(bin_details.projected_qty), flt(packed_item.projected_qty)) + self.assertEqual(flt(bin_details.ordered_qty), flt(packed_item.ordered_qty)) + def test_pos_si_without_payment(self): make_pos_profile() @@ -1705,7 +1759,7 @@ class TestSalesInvoice(unittest.TestCase): si.save() self.assertEqual(si.get("items")[0].rate, flt((price_list_rate * 25) / 100 + price_list_rate)) - def test_outstanding_amount_after_advance_jv_cancelation(self): + def test_outstanding_amount_after_advance_jv_cancellation(self): from erpnext.accounts.doctype.journal_entry.test_journal_entry import ( test_records as jv_test_records, ) @@ -1749,7 +1803,7 @@ class TestSalesInvoice(unittest.TestCase): flt(si.rounded_total + si.total_advance, si.precision("outstanding_amount")), ) - def test_outstanding_amount_after_advance_payment_entry_cancelation(self): + def test_outstanding_amount_after_advance_payment_entry_cancellation(self): pe = frappe.get_doc( { "doctype": "Payment Entry", @@ -2367,29 +2421,6 @@ class TestSalesInvoice(unittest.TestCase): acc_settings.save() def test_inter_company_transaction(self): - from erpnext.selling.doctype.customer.test_customer import create_internal_customer - - create_internal_customer( - customer_name="_Test Internal Customer", - represents_company="_Test Company 1", - allowed_to_interact_with="Wind Power LLC", - ) - - if not frappe.db.exists("Supplier", "_Test Internal Supplier"): - supplier = frappe.get_doc( - { - "supplier_group": "_Test Supplier Group", - "supplier_name": "_Test Internal Supplier", - "doctype": "Supplier", - "is_internal_supplier": 1, - "represents_company": "Wind Power LLC", - } - ) - - supplier.append("companies", {"company": "_Test Company 1"}) - - supplier.insert() - si = create_sales_invoice( company="Wind Power LLC", customer="_Test Internal Customer", @@ -2440,38 +2471,6 @@ class TestSalesInvoice(unittest.TestCase): "Expenses Included In Valuation - _TC1", ) - if not frappe.db.exists("Customer", "_Test Internal Customer"): - customer = frappe.get_doc( - { - "customer_group": "_Test Customer Group", - "customer_name": "_Test Internal Customer", - "customer_type": "Individual", - "doctype": "Customer", - "territory": "_Test Territory", - "is_internal_customer": 1, - "represents_company": "_Test Company 1", - } - ) - - customer.append("companies", {"company": "Wind Power LLC"}) - - customer.insert() - - if not frappe.db.exists("Supplier", "_Test Internal Supplier"): - supplier = frappe.get_doc( - { - "supplier_group": "_Test Supplier Group", - "supplier_name": "_Test Internal Supplier", - "doctype": "Supplier", - "is_internal_supplier": 1, - "represents_company": "Wind Power LLC", - } - ) - - supplier.append("companies", {"company": "_Test Company 1"}) - - supplier.insert() - # begin test si = create_sales_invoice( company="Wind Power LLC", @@ -2541,34 +2540,9 @@ class TestSalesInvoice(unittest.TestCase): se.cancel() def test_internal_transfer_gl_entry(self): - ## Create internal transfer account - from erpnext.selling.doctype.customer.test_customer import create_internal_customer - - account = create_account( - account_name="Unrealized Profit", - parent_account="Current Liabilities - TCP1", - company="_Test Company with perpetual inventory", - ) - - frappe.db.set_value( - "Company", "_Test Company with perpetual inventory", "unrealized_profit_loss_account", account - ) - - customer = create_internal_customer( - "_Test Internal Customer 2", - "_Test Company with perpetual inventory", - "_Test Company with perpetual inventory", - ) - - create_internal_supplier( - "_Test Internal Supplier 2", - "_Test Company with perpetual inventory", - "_Test Company with perpetual inventory", - ) - si = create_sales_invoice( company="_Test Company with perpetual inventory", - customer=customer, + customer="_Test Internal Customer 2", debit_to="Debtors - TCP1", warehouse="Stores - TCP1", income_account="Sales - TCP1", @@ -2582,7 +2556,7 @@ class TestSalesInvoice(unittest.TestCase): si.update_stock = 1 si.items[0].target_warehouse = "Work In Progress - TCP1" - # Add stock to stores for succesful stock transfer + # Add stock to stores for successful stock transfer make_stock_entry( target="Stores - TCP1", company="_Test Company with perpetual inventory", qty=1, basic_rate=100 ) @@ -2638,6 +2612,77 @@ class TestSalesInvoice(unittest.TestCase): check_gl_entries(self, target_doc.name, pi_gl_entries, add_days(nowdate(), -1)) + def test_internal_transfer_gl_precision_issues(self): + # Make a stock queue of an item with two valuations + + # Remove all existing stock for this + if get_stock_balance("_Test Internal Transfer Item", "Stores - TCP1", "2022-04-10"): + create_stock_reconciliation( + item_code="_Test Internal Transfer Item", + warehouse="Stores - TCP1", + qty=0, + rate=0, + company="_Test Company with perpetual inventory", + expense_account="Stock Adjustment - TCP1" + if frappe.get_all("Stock Ledger Entry") + else "Temporary Opening - TCP1", + posting_date="2020-04-10", + posting_time="14:00", + ) + + make_stock_entry( + item_code="_Test Internal Transfer Item", + target="Stores - TCP1", + qty=9000000, + basic_rate=52.0, + posting_date="2020-04-10", + posting_time="14:00", + ) + make_stock_entry( + item_code="_Test Internal Transfer Item", + target="Stores - TCP1", + qty=60000000, + basic_rate=52.349777, + posting_date="2020-04-10", + posting_time="14:00", + ) + + # Make an internal transfer Sales Invoice Stock in non stock uom to check + # for rounding errors while converting to stock uom + si = create_sales_invoice( + company="_Test Company with perpetual inventory", + customer="_Test Internal Customer 2", + item_code="_Test Internal Transfer Item", + qty=5000000, + uom="Box", + debit_to="Debtors - TCP1", + warehouse="Stores - TCP1", + income_account="Sales - TCP1", + expense_account="Cost of Goods Sold - TCP1", + cost_center="Main - TCP1", + currency="INR", + do_not_save=1, + ) + + # Check GL Entries with precision + si.update_stock = 1 + si.items[0].target_warehouse = "Work In Progress - TCP1" + si.items[0].conversion_factor = 10 + si.save() + si.submit() + + # Check if adjustment entry is created + self.assertTrue( + frappe.db.exists( + "GL Entry", + { + "voucher_type": "Sales Invoice", + "voucher_no": si.name, + "remarks": "Rounding gain/loss Entry for Stock Transfer", + }, + ) + ) + def test_item_tax_net_range(self): item = create_item("T Shirt") @@ -2727,6 +2772,31 @@ class TestSalesInvoice(unittest.TestCase): check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1)) + # Update Invoice post submit and then check GL Entries again + + si.load_from_db() + si.items[0].income_account = "Service - _TC" + si.additional_discount_account = "_Test Account Sales - _TC" + si.taxes[0].account_head = "TDS Payable - _TC" + si.save() + + si.load_from_db() + self.assertTrue(si.repost_required) + + si.repost_accounting_entries() + + expected_gle = [ + ["_Test Account Sales - _TC", 22.0, 0.0, nowdate()], + ["Debtors - _TC", 88, 0.0, nowdate()], + ["Service - _TC", 0.0, 100.0, nowdate()], + ["TDS Payable - _TC", 0.0, 10.0, nowdate()], + ] + + check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1)) + + si.load_from_db() + self.assertFalse(si.repost_required) + def test_asset_depreciation_on_sale_with_pro_rata(self): """ Tests if an Asset set to depreciate yearly on June 30, that gets sold on Sept 30, creates an additional depreciation entry on its date of sale. @@ -2747,7 +2817,7 @@ class TestSalesInvoice(unittest.TestCase): ["2021-09-30", 5041.1, 26407.22], ] - for i, schedule in enumerate(asset.schedules): + for i, schedule in enumerate(get_depr_schedule(asset.name, "Active")): self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date) self.assertEqual(expected_values[i][1], schedule.depreciation_amount) self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount) @@ -2778,7 +2848,7 @@ class TestSalesInvoice(unittest.TestCase): expected_values = [["2020-12-31", 30000, 30000], ["2021-12-31", 30000, 60000]] - for i, schedule in enumerate(asset.schedules): + for i, schedule in enumerate(get_depr_schedule(asset.name, "Active")): self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date) self.assertEqual(expected_values[i][1], schedule.depreciation_amount) self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount) @@ -2807,7 +2877,7 @@ class TestSalesInvoice(unittest.TestCase): ["2025-06-06", 18633.88, 100000.0, False], ] - for i, schedule in enumerate(asset.schedules): + for i, schedule in enumerate(get_depr_schedule(asset.name, "Active")): self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date) self.assertEqual(expected_values[i][1], schedule.depreciation_amount) self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount) @@ -3077,7 +3147,7 @@ class TestSalesInvoice(unittest.TestCase): [deferred_account, 2022.47, 0.0, "2019-03-15"], ] - gl_entries = gl_entries = frappe.db.sql( + gl_entries = frappe.db.sql( """select account, debit, credit, posting_date from `tabGL Entry` where voucher_type='Journal Entry' and voucher_detail_no=%s and posting_date <= %s @@ -3196,6 +3266,53 @@ class TestSalesInvoice(unittest.TestCase): "Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice", unlink_enabled ) + def test_batch_expiry_for_sales_invoice_return(self): + from erpnext.controllers.sales_and_purchase_return import make_return_doc + from erpnext.stock.doctype.item.test_item import make_item + + item = make_item( + "_Test Batch Item For Return Check", + { + "is_purchase_item": 1, + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "TBIRC.#####", + }, + ) + + pr = make_purchase_receipt(qty=1, item_code=item.name) + + batch_no = pr.items[0].batch_no + si = create_sales_invoice(qty=1, item_code=item.name, update_stock=1, batch_no=batch_no) + + si.load_from_db() + batch_no = si.items[0].batch_no + self.assertTrue(batch_no) + + frappe.db.set_value("Batch", batch_no, "expiry_date", add_days(today(), -1)) + + return_si = make_return_doc(si.doctype, si.name) + return_si.save().submit() + + self.assertTrue(return_si.docstatus == 1) + + def test_sales_invoice_with_payable_tax_account(self): + si = create_sales_invoice(do_not_submit=True) + si.append( + "taxes", + { + "charge_type": "Actual", + "account_head": "Creditors - _TC", + "description": "Test", + "cost_center": "Main - _TC", + "tax_amount": 10, + "total": 10, + "dont_recompute_tax": 0, + }, + ) + self.assertRaises(frappe.ValidationError, si.submit) + def get_sales_invoice_for_e_invoice(): si = make_sales_invoice_for_ewaybill() @@ -3237,6 +3354,7 @@ def check_gl_entries(doc, voucher_no, expected_gle, posting_date): """select account, debit, credit, posting_date from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s and posting_date > %s + and is_cancelled = 0 order by posting_date asc, account asc""", (voucher_no, posting_date), as_dict=1, @@ -3275,6 +3393,7 @@ def create_sales_invoice(**args): "item_name": args.item_name or "_Test Item", "description": args.description or "_Test Item", "warehouse": args.warehouse or "_Test Warehouse - _TC", + "target_warehouse": args.target_warehouse, "qty": args.qty or 1, "uom": args.uom or "Nos", "stock_uom": args.uom or "Nos", @@ -3287,8 +3406,9 @@ def create_sales_invoice(**args): "asset": args.asset or None, "cost_center": args.cost_center or "_Test Cost Center - _TC", "serial_no": args.serial_no, - "conversion_factor": 1, + "conversion_factor": args.get("conversion_factor", 1), "incoming_rate": args.incoming_rate or 0, + "batch_no": args.batch_no or None, }, ) @@ -3399,6 +3519,34 @@ def get_taxes_and_charges(): ] +def create_internal_parties(): + from erpnext.selling.doctype.customer.test_customer import create_internal_customer + + create_internal_customer( + customer_name="_Test Internal Customer", + represents_company="_Test Company 1", + allowed_to_interact_with="Wind Power LLC", + ) + + create_internal_customer( + customer_name="_Test Internal Customer 2", + represents_company="_Test Company with perpetual inventory", + allowed_to_interact_with="_Test Company with perpetual inventory", + ) + + create_internal_supplier( + supplier_name="_Test Internal Supplier", + represents_company="Wind Power LLC", + allowed_to_interact_with="_Test Company 1", + ) + + create_internal_supplier( + supplier_name="_Test Internal Supplier 2", + represents_company="_Test Company with perpetual inventory", + allowed_to_interact_with="_Test Company with perpetual inventory", + ) + + def create_internal_supplier(supplier_name, represents_company, allowed_to_interact_with): if not frappe.db.exists("Supplier", supplier_name): supplier = frappe.get_doc( @@ -3421,6 +3569,19 @@ def create_internal_supplier(supplier_name, represents_company, allowed_to_inter return supplier_name +def setup_accounts(): + ## Create internal transfer account + account = create_account( + account_name="Unrealized Profit", + parent_account="Current Liabilities - TCP1", + company="_Test Company with perpetual inventory", + ) + + frappe.db.set_value( + "Company", "_Test Company with perpetual inventory", "unrealized_profit_loss_account", account + ) + + def add_taxes(doc): doc.append( "taxes", 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 b417c7de03..35d19ed843 100644 --- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json +++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json @@ -8,6 +8,7 @@ "engine": "InnoDB", "field_order": [ "barcode", + "has_item_scanned", "item_code", "col_break1", "item_name", @@ -96,6 +97,10 @@ "delivery_note", "dn_detail", "delivered_qty", + "internal_transfer_section", + "purchase_order", + "column_break_92", + "purchase_order_item", "accounting_dimensions_section", "cost_center", "dimension_col_break", @@ -243,6 +248,7 @@ }, { "collapsible": 1, + "collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount", "fieldname": "discount_and_margin", "fieldtype": "Section Break", "label": "Discount and Margin" @@ -282,7 +288,6 @@ "label": "Discount (%) on Price List Rate with Margin", "oldfieldname": "adj_rate", "oldfieldtype": "Float", - "precision": "2", "print_hide": 1 }, { @@ -433,6 +438,7 @@ "label": "Accounting Details" }, { + "allow_on_submit": 1, "fieldname": "income_account", "fieldtype": "Link", "label": "Income Account", @@ -445,6 +451,7 @@ "width": "120px" }, { + "allow_on_submit": 1, "fieldname": "expense_account", "fieldtype": "Link", "label": "Expense Account", @@ -464,6 +471,7 @@ "print_hide": 1 }, { + "allow_on_submit": 1, "default": ":Company", "fieldname": "cost_center", "fieldtype": "Link", @@ -795,6 +803,7 @@ "options": "Finance Book" }, { + "allow_on_submit": 1, "fieldname": "project", "fieldtype": "Link", "label": "Project", @@ -829,6 +838,7 @@ "read_only": 1 }, { + "allow_on_submit": 1, "fieldname": "discount_account", "fieldtype": "Link", "label": "Discount Account", @@ -841,12 +851,46 @@ "fieldtype": "Check", "label": "Grant Commission", "read_only": 1 + }, + { + "collapsible": 1, + "depends_on": "eval:parent.is_internal_customer == 1", + "fieldname": "internal_transfer_section", + "fieldtype": "Section Break", + "label": "Internal Transfer" + }, + { + "fieldname": "purchase_order", + "fieldtype": "Link", + "label": "Purchase Order", + "options": "Purchase Order", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "column_break_92", + "fieldtype": "Column Break" + }, + { + "fieldname": "purchase_order_item", + "fieldtype": "Data", + "label": "Purchase Order Item", + "print_hide": 1, + "read_only": 1 + }, + { + "default": "0", + "depends_on": "barcode", + "fieldname": "has_item_scanned", + "fieldtype": "Check", + "label": "Has Item Scanned", + "read_only": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2022-06-17 05:33:15.335912", + "modified": "2022-12-28 16:17:33.484531", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice Item", diff --git a/erpnext/accounts/doctype/sales_taxes_and_charges/sales_taxes_and_charges.json b/erpnext/accounts/doctype/sales_taxes_and_charges/sales_taxes_and_charges.json index 3a871bfced..e236577e11 100644 --- a/erpnext/accounts/doctype/sales_taxes_and_charges/sales_taxes_and_charges.json +++ b/erpnext/accounts/doctype/sales_taxes_and_charges/sales_taxes_and_charges.json @@ -51,6 +51,7 @@ "oldfieldtype": "Data" }, { + "allow_on_submit": 1, "columns": 2, "fieldname": "account_head", "fieldtype": "Link", @@ -63,6 +64,7 @@ "search_index": 1 }, { + "allow_on_submit": 1, "default": ":Company", "fieldname": "cost_center", "fieldtype": "Link", @@ -216,12 +218,13 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-08-05 20:04:01.726867", + "modified": "2022-10-17 13:08:17.776528", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Taxes and Charges", "owner": "Administrator", "permissions": [], "sort_field": "modified", - "sort_order": "ASC" + "sort_order": "ASC", + "states": [] } \ No newline at end of file diff --git a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py index d9009bae4c..a3a5d627b7 100644 --- a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py +++ b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py @@ -27,7 +27,7 @@ class SalesTaxesandChargesTemplate(Document): def set_missing_values(self): for data in self.taxes: if data.charge_type == "On Net Total" and flt(data.rate) == 0.0: - data.rate = frappe.db.get_value("Account", data.account_head, "tax_rate") + data.rate = frappe.get_cached_value("Account", data.account_head, "tax_rate") def valdiate_taxes_and_charges_template(doc): diff --git a/erpnext/accounts/doctype/subscription/subscription.py b/erpnext/accounts/doctype/subscription/subscription.py index 9dab4e91fb..8708342b11 100644 --- a/erpnext/accounts/doctype/subscription/subscription.py +++ b/erpnext/accounts/doctype/subscription/subscription.py @@ -280,7 +280,8 @@ class Subscription(Document): self.validate_plans_billing_cycle(self.get_billing_cycle_and_interval()) self.validate_end_date() self.validate_to_follow_calendar_months() - self.cost_center = erpnext.get_default_cost_center(self.get("company")) + if not self.cost_center: + self.cost_center = erpnext.get_default_cost_center(self.get("company")) def validate_trial_period(self): """ diff --git a/erpnext/accounts/doctype/subscription_plan/subscription_plan.js b/erpnext/accounts/doctype/subscription_plan/subscription_plan.js index 7d6f2aed10..00727f103f 100644 --- a/erpnext/accounts/doctype/subscription_plan/subscription_plan.js +++ b/erpnext/accounts/doctype/subscription_plan/subscription_plan.js @@ -5,5 +5,9 @@ frappe.ui.form.on('Subscription Plan', { price_determination: function(frm) { frm.toggle_reqd("cost", frm.doc.price_determination === 'Fixed rate'); frm.toggle_reqd("price_list", frm.doc.price_determination === 'Based on price list'); - } + }, + + subscription_plan: function (frm) { + erpnext.utils.check_payments_app(); + }, }); diff --git a/erpnext/accounts/doctype/subscription_plan/subscription_plan.py b/erpnext/accounts/doctype/subscription_plan/subscription_plan.py index a95e0a9c2d..f3acdc5aa8 100644 --- a/erpnext/accounts/doctype/subscription_plan/subscription_plan.py +++ b/erpnext/accounts/doctype/subscription_plan/subscription_plan.py @@ -3,6 +3,7 @@ import frappe +from dateutil import relativedelta from frappe import _ from frappe.model.document import Document from frappe.utils import date_diff, flt, get_first_day, get_last_day, getdate @@ -49,7 +50,7 @@ def get_plan_rate( start_date = getdate(start_date) end_date = getdate(end_date) - no_of_months = (end_date.year - start_date.year) * 12 + (end_date.month - start_date.month) + 1 + no_of_months = relativedelta.relativedelta(end_date, start_date).months + 1 cost = plan.cost * no_of_months # Adjust cost if start or end date is not month start or end diff --git a/erpnext/accounts/doctype/tax_rule/tax_rule.py b/erpnext/accounts/doctype/tax_rule/tax_rule.py index 4d201292ed..87c5e6d588 100644 --- a/erpnext/accounts/doctype/tax_rule/tax_rule.py +++ b/erpnext/accounts/doctype/tax_rule/tax_rule.py @@ -32,7 +32,7 @@ class TaxRule(Document): def validate(self): self.validate_tax_template() - self.validate_date() + self.validate_from_to_dates("from_date", "to_date") self.validate_filters() self.validate_use_for_shopping_cart() @@ -51,10 +51,6 @@ class TaxRule(Document): if not (self.sales_tax_template or self.purchase_tax_template): frappe.throw(_("Tax Template is mandatory.")) - def validate_date(self): - if self.from_date and self.to_date and self.from_date > self.to_date: - frappe.throw(_("From Date cannot be greater than To Date")) - def validate_filters(self): filters = { "tax_type": self.tax_type, diff --git a/erpnext/regional/doctype/ksa_vat_purchase_account/__init__.py b/erpnext/accounts/doctype/tax_withheld_vouchers/__init__.py similarity index 100% rename from erpnext/regional/doctype/ksa_vat_purchase_account/__init__.py rename to erpnext/accounts/doctype/tax_withheld_vouchers/__init__.py diff --git a/erpnext/accounts/doctype/tax_withheld_vouchers/tax_withheld_vouchers.json b/erpnext/accounts/doctype/tax_withheld_vouchers/tax_withheld_vouchers.json new file mode 100644 index 0000000000..46b430c659 --- /dev/null +++ b/erpnext/accounts/doctype/tax_withheld_vouchers/tax_withheld_vouchers.json @@ -0,0 +1,49 @@ +{ + "actions": [], + "autoname": "hash", + "creation": "2022-09-13 16:18:59.404842", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "voucher_type", + "voucher_name", + "taxable_amount" + ], + "fields": [ + { + "fieldname": "voucher_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Voucher Type", + "options": "DocType" + }, + { + "fieldname": "voucher_name", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "label": "Voucher Name", + "options": "voucher_type" + }, + { + "fieldname": "taxable_amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Taxable Amount", + "options": "Company:company:default_currency" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2023-01-13 13:40:41.479208", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Tax Withheld Vouchers", + "naming_rule": "Random", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/erpnext/regional/doctype/ksa_vat_purchase_account/ksa_vat_purchase_account.py b/erpnext/accounts/doctype/tax_withheld_vouchers/tax_withheld_vouchers.py similarity index 52% rename from erpnext/regional/doctype/ksa_vat_purchase_account/ksa_vat_purchase_account.py rename to erpnext/accounts/doctype/tax_withheld_vouchers/tax_withheld_vouchers.py index 3920bc546c..ea54c5403a 100644 --- a/erpnext/regional/doctype/ksa_vat_purchase_account/ksa_vat_purchase_account.py +++ b/erpnext/accounts/doctype/tax_withheld_vouchers/tax_withheld_vouchers.py @@ -1,9 +1,9 @@ -# Copyright (c) 2021, Havenir Solutions and contributors +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt # import frappe from frappe.model.document import Document -class KSAVATPurchaseAccount(Document): +class TaxWithheldVouchers(Document): pass 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 a519d8be73..f0146ea70e 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -61,6 +61,9 @@ def get_party_details(inv): def get_party_tax_withholding_details(inv, tax_withholding_category=None): + if inv.doctype == "Payment Entry": + inv.tax_withholding_net_total = inv.net_total + pan_no = "" parties = [] party_type, party = get_party_details(inv) @@ -109,7 +112,7 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None): ).format(tax_withholding_category, inv.company, party) ) - tax_amount, tax_deducted, tax_deducted_on_advances = get_tax_amount( + tax_amount, tax_deducted, tax_deducted_on_advances, voucher_wise_amount = get_tax_amount( party_type, parties, inv, tax_details, posting_date, pan_no ) @@ -118,12 +121,24 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None): else: tax_row = get_tax_row_for_tcs(inv, tax_details, tax_amount, tax_deducted) + cost_center = get_cost_center(inv) + tax_row.update({"cost_center": cost_center}) + if inv.doctype == "Purchase Invoice": - return tax_row, tax_deducted_on_advances + return tax_row, tax_deducted_on_advances, voucher_wise_amount else: return tax_row +def get_cost_center(inv): + cost_center = frappe.get_cached_value("Company", inv.company, "cost_center") + + if len(inv.get("taxes", [])) > 0: + cost_center = inv.get("taxes")[0].cost_center + + return cost_center + + def get_tax_withholding_details(tax_withholding_category, posting_date, company): tax_withholding = frappe.get_doc("Tax Withholding Category", tax_withholding_category) @@ -217,7 +232,9 @@ def get_lower_deduction_certificate(tax_details, pan_no): def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=None): - vouchers = get_invoice_vouchers(parties, tax_details, inv.company, party_type=party_type) + vouchers, voucher_wise_amount = get_invoice_vouchers( + parties, tax_details, inv.company, party_type=party_type + ) advance_vouchers = get_advance_vouchers( parties, company=inv.company, @@ -236,16 +253,18 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N tax_deducted = get_deducted_tax(taxable_vouchers, tax_details) tax_amount = 0 + if party_type == "Supplier": ldc = get_lower_deduction_certificate(tax_details, pan_no) if tax_deducted: - net_total = inv.net_total + net_total = inv.tax_withholding_net_total if ldc: - tax_amount = get_tds_amount_from_ldc( - ldc, parties, pan_no, tax_details, posting_date, net_total - ) + tax_amount = get_tds_amount_from_ldc(ldc, parties, tax_details, posting_date, net_total) else: tax_amount = net_total * tax_details.rate / 100 if net_total > 0 else 0 + + # once tds is deducted, not need to add vouchers in the invoice + voucher_wise_amount = {} else: tax_amount = get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers) @@ -259,14 +278,20 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N tax_amount = get_tcs_amount(parties, inv, tax_details, vouchers, advance_vouchers) if cint(tax_details.round_off_tax_amount): - tax_amount = round(tax_amount) + tax_amount = normal_round(tax_amount) - return tax_amount, tax_deducted, tax_deducted_on_advances + return tax_amount, tax_deducted, tax_deducted_on_advances, voucher_wise_amount def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"): - dr_or_cr = "credit" if party_type == "Supplier" else "debit" doctype = "Purchase Invoice" if party_type == "Supplier" else "Sales Invoice" + field = ( + "base_tax_withholding_net_total as base_net_total" + if party_type == "Supplier" + else "base_net_total" + ) + voucher_wise_amount = {} + vouchers = [] filters = { "company": company, @@ -281,29 +306,40 @@ def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"): {"apply_tds": 1, "tax_withholding_category": tax_details.get("tax_withholding_category")} ) - invoices = frappe.get_all(doctype, filters=filters, pluck="name") or [""] + invoices_details = frappe.get_all(doctype, filters=filters, fields=["name", field]) - journal_entries = frappe.db.sql( + for d in invoices_details: + vouchers.append(d.name) + voucher_wise_amount.update({d.name: {"amount": d.base_net_total, "voucher_type": doctype}}) + + journal_entries_details = frappe.db.sql( """ - SELECT j.name + SELECT j.name, ja.credit - ja.debit AS amount FROM `tabJournal Entry` j, `tabJournal Entry Account` ja WHERE - j.docstatus = 1 + j.name = ja.parent + AND j.docstatus = 1 AND j.is_opening = 'No' AND j.posting_date between %s and %s - AND ja.{dr_or_cr} > 0 AND ja.party in %s - """.format( - dr_or_cr=dr_or_cr + AND j.apply_tds = 1 + AND j.tax_withholding_category = %s + """, + ( + tax_details.from_date, + tax_details.to_date, + tuple(parties), + tax_details.get("tax_withholding_category"), ), - (tax_details.from_date, tax_details.to_date, tuple(parties)), - as_list=1, + as_dict=1, ) - if journal_entries: - journal_entries = journal_entries[0] + if journal_entries_details: + for d in journal_entries_details: + vouchers.append(d.name) + voucher_wise_amount.update({d.name: {"amount": d.amount, "voucher_type": "Journal Entry"}}) - return invoices + journal_entries + return vouchers, voucher_wise_amount def get_advance_vouchers( @@ -318,9 +354,11 @@ def get_advance_vouchers( "is_cancelled": 0, "party_type": party_type, "party": ["in", parties], - "against_voucher": ["is", "not set"], } + if party_type == "Customer": + filters.update({"against_voucher": ["is", "not set"]}) + if company: filters["company"] = company if from_date and to_date: @@ -330,23 +368,25 @@ def get_advance_vouchers( 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") + if inv.get("advances"): + advances = [d.reference_name for d in inv.get("advances")] - 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) - ) + 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 @@ -370,12 +410,26 @@ def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers): tds_amount = 0 invoice_filters = {"name": ("in", vouchers), "docstatus": 1, "apply_tds": 1} - field = "sum(net_total)" + ## for TDS to be deducted on advances + payment_entry_filters = { + "party_type": "Supplier", + "party": ("in", parties), + "docstatus": 1, + "apply_tax_withholding_amount": 1, + "unallocated_amount": (">", 0), + "posting_date": ["between", (tax_details.from_date, tax_details.to_date)], + "tax_withholding_category": tax_details.get("tax_withholding_category"), + } + + field = "sum(tax_withholding_net_total)" if cint(tax_details.consider_party_ledger_amount): invoice_filters.pop("apply_tds", None) field = "sum(grand_total)" + payment_entry_filters.pop("apply_tax_withholding_amount", None) + payment_entry_filters.pop("tax_withholding_category", None) + supp_credit_amt = frappe.db.get_value("Purchase Invoice", invoice_filters, field) or 0.0 supp_jv_credit_amt = ( @@ -387,23 +441,32 @@ def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers): "party": ("in", parties), "reference_type": ("!=", "Purchase Invoice"), }, - "sum(credit_in_account_currency)", + "sum(credit_in_account_currency - debit_in_account_currency)", ) or 0.0 ) - supp_credit_amt += supp_jv_credit_amt - supp_credit_amt += inv.net_total - - debit_note_amount = get_debit_note_amount( - parties, tax_details.from_date, tax_details.to_date, inv.company + # Get Amount via payment entry + payment_entry_amounts = frappe.db.get_all( + "Payment Entry", + filters=payment_entry_filters, + fields=["sum(unallocated_amount) as amount", "payment_type"], + group_by="payment_type", ) - supp_credit_amt -= debit_note_amount + + supp_credit_amt += supp_jv_credit_amt + supp_credit_amt += inv.tax_withholding_net_total + + for type in payment_entry_amounts: + if type.payment_type == "Pay": + supp_credit_amt += type.amount + else: + supp_credit_amt -= type.amount threshold = tax_details.get("threshold", 0) cumulative_threshold = tax_details.get("cumulative_threshold", 0) - if (threshold and inv.net_total >= threshold) or ( + if (threshold and inv.tax_withholding_net_total >= threshold) or ( cumulative_threshold and supp_credit_amt >= cumulative_threshold ): if (cumulative_threshold and supp_credit_amt >= cumulative_threshold) and cint( @@ -411,8 +474,11 @@ def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers): ): # Get net total again as TDS is calculated on net total # Grand is used to just check for threshold breach - net_total = frappe.db.get_value("Purchase Invoice", invoice_filters, "sum(net_total)") or 0.0 - net_total += inv.net_total + net_total = ( + frappe.db.get_value("Purchase Invoice", invoice_filters, "sum(tax_withholding_net_total)") + or 0.0 + ) + net_total += inv.tax_withholding_net_total supp_credit_amt = net_total - cumulative_threshold if ldc and is_valid_certificate( @@ -420,7 +486,7 @@ def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers): ldc.valid_upto, inv.get("posting_date") or inv.get("transaction_date"), tax_deducted, - inv.net_total, + inv.tax_withholding_net_total, ldc.certificate_limit, ): tds_amount = get_ltds_amount(supp_credit_amt, 0, ldc.certificate_limit, ldc.rate, tax_details) @@ -498,12 +564,12 @@ def get_invoice_total_without_tcs(inv, tax_details): return inv.grand_total - tcs_tax_row_amount -def get_tds_amount_from_ldc(ldc, parties, pan_no, tax_details, posting_date, net_total): +def get_tds_amount_from_ldc(ldc, parties, tax_details, posting_date, net_total): tds_amount = 0 limit_consumed = frappe.db.get_value( "Purchase Invoice", {"supplier": ("in", parties), "apply_tds": 1, "docstatus": 1}, - "sum(net_total)", + "sum(tax_withholding_net_total)", ) if is_valid_certificate( @@ -516,22 +582,6 @@ def get_tds_amount_from_ldc(ldc, parties, pan_no, tax_details, posting_date, net return tds_amount -def get_debit_note_amount(suppliers, from_date, to_date, company=None): - - filters = { - "supplier": ["in", suppliers], - "is_return": 1, - "docstatus": 1, - "posting_date": ["between", (from_date, to_date)], - } - fields = ["abs(sum(net_total)) as net_total"] - - if company: - filters["company"] = company - - return frappe.get_all("Purchase Invoice", filters, fields)[0].get("net_total") or 0.0 - - def get_ltds_amount(current_amount, deducted_amount, certificate_limit, rate, tax_details): if current_amount < (certificate_limit - deducted_amount): return current_amount * rate / 100 @@ -553,3 +603,20 @@ def is_valid_certificate( valid = True return valid + + +def normal_round(number): + """ + Rounds a number to the nearest integer. + :param number: The number to round. + """ + decimal_part = number - int(number) + + if decimal_part >= 0.5: + decimal_part = 1 + else: + decimal_part = 0 + + number = int(number) + decimal_part + + return number diff --git a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py index 3059f8d64b..1e86cf5d2e 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py @@ -16,7 +16,7 @@ class TestTaxWithholdingCategory(unittest.TestCase): def setUpClass(self): # create relevant supplier, etc create_records() - create_tax_with_holding_category() + create_tax_withholding_category_records() def tearDown(self): cancel_invoices() @@ -38,7 +38,7 @@ class TestTaxWithholdingCategory(unittest.TestCase): pi = create_purchase_invoice(supplier="Test TDS Supplier") pi.submit() - # assert equal tax deduction on total invoice amount uptil now + # assert equal tax deduction on total invoice amount until now self.assertEqual(pi.taxes_and_charges_deducted, 3000) self.assertEqual(pi.grand_total, 7000) invoices.append(pi) @@ -47,12 +47,12 @@ class TestTaxWithholdingCategory(unittest.TestCase): pi = create_purchase_invoice(supplier="Test TDS Supplier", rate=5000) pi.submit() - # assert equal tax deduction on total invoice amount uptil now + # assert equal tax deduction on total invoice amount until now self.assertEqual(pi.taxes_and_charges_deducted, 500) invoices.append(pi) # delete invoices to avoid clashing - for d in invoices: + for d in reversed(invoices): d.cancel() def test_single_threshold_tds(self): @@ -88,7 +88,7 @@ class TestTaxWithholdingCategory(unittest.TestCase): self.assertEqual(pi.taxes_and_charges_deducted, 1000) # delete invoices to avoid clashing - for d in invoices: + for d in reversed(invoices): d.cancel() def test_tax_withholding_category_checks(self): @@ -114,7 +114,7 @@ class TestTaxWithholdingCategory(unittest.TestCase): # TDS should be applied only on 1000 self.assertEqual(pi1.taxes[0].tax_amount, 1000) - for d in invoices: + for d in reversed(invoices): d.cancel() def test_cumulative_threshold_tcs(self): @@ -130,7 +130,7 @@ class TestTaxWithholdingCategory(unittest.TestCase): invoices.append(si) # create another invoice whose total when added to previously created invoice, - # surpasses cumulative threshhold + # surpasses cumulative threshold si = create_sales_invoice(customer="Test TCS Customer", rate=12000) si.submit() @@ -148,8 +148,8 @@ class TestTaxWithholdingCategory(unittest.TestCase): self.assertEqual(tcs_charged, 500) invoices.append(si) - # delete invoices to avoid clashing - for d in invoices: + # cancel invoices to avoid clashing + for d in reversed(invoices): d.cancel() def test_tds_calculation_on_net_total(self): @@ -182,8 +182,84 @@ class TestTaxWithholdingCategory(unittest.TestCase): self.assertEqual(pi1.taxes[0].tax_amount, 4000) - # delete invoices to avoid clashing - for d in invoices: + # cancel invoices to avoid clashing + for d in reversed(invoices): + d.cancel() + + def test_tds_calculation_on_net_total_partial_tds(self): + frappe.db.set_value( + "Supplier", "Test TDS Supplier4", "tax_withholding_category", "Cumulative Threshold TDS" + ) + invoices = [] + + pi = create_purchase_invoice(supplier="Test TDS Supplier4", rate=20000, do_not_save=True) + pi.extend( + "items", + [ + { + "doctype": "Purchase Invoice Item", + "item_code": frappe.db.get_value("Item", {"item_name": "TDS Item"}, "name"), + "qty": 1, + "rate": 20000, + "cost_center": "Main - _TC", + "expense_account": "Stock Received But Not Billed - _TC", + "apply_tds": 0, + }, + { + "doctype": "Purchase Invoice Item", + "item_code": frappe.db.get_value("Item", {"item_name": "TDS Item"}, "name"), + "qty": 1, + "rate": 35000, + "cost_center": "Main - _TC", + "expense_account": "Stock Received But Not Billed - _TC", + "apply_tds": 1, + }, + ], + ) + pi.save() + pi.submit() + invoices.append(pi) + + self.assertEqual(pi.taxes[0].tax_amount, 5500) + + # cancel invoices to avoid clashing + for d in reversed(invoices): + d.cancel() + + orders = [] + + po = create_purchase_order(supplier="Test TDS Supplier4", rate=20000, do_not_save=True) + po.extend( + "items", + [ + { + "doctype": "Purchase Order Item", + "item_code": frappe.db.get_value("Item", {"item_name": "TDS Item"}, "name"), + "qty": 1, + "rate": 20000, + "cost_center": "Main - _TC", + "expense_account": "Stock Received But Not Billed - _TC", + "apply_tds": 0, + }, + { + "doctype": "Purchase Order Item", + "item_code": frappe.db.get_value("Item", {"item_name": "TDS Item"}, "name"), + "qty": 1, + "rate": 35000, + "cost_center": "Main - _TC", + "expense_account": "Stock Received But Not Billed - _TC", + "apply_tds": 1, + }, + ], + ) + po.save() + po.submit() + orders.append(po) + + self.assertEqual(po.taxes[0].tax_amount, 5500) + + # cancel orders to avoid clashing + for d in reversed(orders): d.cancel() def test_multi_category_single_supplier(self): @@ -207,10 +283,84 @@ class TestTaxWithholdingCategory(unittest.TestCase): self.assertEqual(pi1.taxes[0].tax_amount, 250) - # delete invoices to avoid clashing - for d in invoices: + # cancel invoices to avoid clashing + for d in reversed(invoices): d.cancel() + def test_tax_withholding_category_voucher_display(self): + frappe.db.set_value( + "Supplier", "Test TDS Supplier6", "tax_withholding_category", "Test Multi Invoice Category" + ) + invoices = [] + + pi = create_purchase_invoice(supplier="Test TDS Supplier6", rate=4000, do_not_save=True) + pi.apply_tds = 1 + pi.tax_withholding_category = "Test Multi Invoice Category" + pi.save() + pi.submit() + invoices.append(pi) + + pi1 = create_purchase_invoice(supplier="Test TDS Supplier6", rate=2000, do_not_save=True) + pi1.apply_tds = 1 + pi1.is_return = 1 + pi1.items[0].qty = -1 + pi1.tax_withholding_category = "Test Multi Invoice Category" + pi1.save() + pi1.submit() + invoices.append(pi1) + + pi2 = create_purchase_invoice(supplier="Test TDS Supplier6", rate=9000, do_not_save=True) + pi2.apply_tds = 1 + pi2.tax_withholding_category = "Test Multi Invoice Category" + pi2.save() + pi2.submit() + invoices.append(pi2) + + pi2.load_from_db() + + self.assertTrue(pi2.taxes[0].tax_amount, 1100) + + self.assertTrue(pi2.tax_withheld_vouchers[0].voucher_name == pi1.name) + self.assertTrue(pi2.tax_withheld_vouchers[0].taxable_amount == pi1.net_total) + self.assertTrue(pi2.tax_withheld_vouchers[1].voucher_name == pi.name) + self.assertTrue(pi2.tax_withheld_vouchers[1].taxable_amount == pi.net_total) + + # cancel invoices to avoid clashing + for d in reversed(invoices): + d.cancel() + + def test_tax_withholding_via_payment_entry_for_advances(self): + frappe.db.set_value( + "Supplier", "Test TDS Supplier7", "tax_withholding_category", "Advance TDS Category" + ) + + # create payment entry + pe1 = create_payment_entry( + payment_type="Pay", party_type="Supplier", party="Test TDS Supplier7", paid_amount=4000 + ) + pe1.submit() + + self.assertFalse(pe1.get("taxes")) + + pe2 = create_payment_entry( + payment_type="Pay", party_type="Supplier", party="Test TDS Supplier7", paid_amount=4000 + ) + pe2.submit() + + self.assertFalse(pe2.get("taxes")) + + pe3 = create_payment_entry( + payment_type="Pay", party_type="Supplier", party="Test TDS Supplier7", paid_amount=4000 + ) + pe3.apply_tax_withholding_amount = 1 + pe3.save() + pe3.submit() + + self.assertEquals(pe3.get("taxes")[0].tax_amount, 1200) + pe1.cancel() + pe2.cancel() + pe3.cancel() + def cancel_invoices(): purchase_invoices = frappe.get_all( @@ -266,6 +416,39 @@ def create_purchase_invoice(**args): return pi +def create_purchase_order(**args): + # return purchase order doc object + item = frappe.db.get_value("Item", {"item_name": "TDS Item"}, "name") + + args = frappe._dict(args) + po = frappe.get_doc( + { + "doctype": "Purchase Order", + "transaction_date": today(), + "schedule_date": today(), + "apply_tds": 0 if args.do_not_apply_tds else 1, + "supplier": args.supplier, + "company": "_Test Company", + "taxes_and_charges": "", + "currency": "INR", + "taxes": [], + "items": [ + { + "doctype": "Purchase Order Item", + "item_code": item, + "qty": args.qty or 1, + "rate": args.rate or 10000, + "cost_center": "Main - _TC", + "expense_account": "Stock Received But Not Billed - _TC", + } + ], + } + ) + + po.save() + return po + + def create_sales_invoice(**args): # return sales invoice doc object item = frappe.db.get_value("Item", {"item_name": "TCS Item"}, "name") @@ -299,6 +482,32 @@ def create_sales_invoice(**args): return si +def create_payment_entry(**args): + # return payment entry doc object + args = frappe._dict(args) + pe = frappe.get_doc( + { + "doctype": "Payment Entry", + "posting_date": today(), + "payment_type": args.payment_type, + "party_type": args.party_type, + "party": args.party, + "company": "_Test Company", + "paid_from": "Cash - _TC", + "paid_to": "Creditors - _TC", + "paid_amount": args.paid_amount or 10000, + "received_amount": args.paid_amount or 10000, + "reference_no": args.reference_no or "12345", + "reference_date": today(), + "paid_from_account_currency": "INR", + "paid_to_account_currency": "INR", + } + ) + + pe.save() + return pe + + def create_records(): # create a new suppliers for name in [ @@ -308,6 +517,8 @@ def create_records(): "Test TDS Supplier3", "Test TDS Supplier4", "Test TDS Supplier5", + "Test TDS Supplier6", + "Test TDS Supplier7", ]: if frappe.db.exists("Supplier", name): continue @@ -378,123 +589,129 @@ def create_records(): ).insert() -def create_tax_with_holding_category(): +def create_tax_withholding_category_records(): fiscal_year = get_fiscal_year(today(), company="_Test Company") + from_date = fiscal_year[1] + to_date = fiscal_year[2] + # Cumulative threshold - if not frappe.db.exists("Tax Withholding Category", "Cumulative Threshold TDS"): - frappe.get_doc( - { - "doctype": "Tax Withholding Category", - "name": "Cumulative Threshold TDS", - "category_name": "10% TDS", - "rates": [ - { - "from_date": fiscal_year[1], - "to_date": fiscal_year[2], - "tax_withholding_rate": 10, - "single_threshold": 0, - "cumulative_threshold": 30000.00, - } - ], - "accounts": [{"company": "_Test Company", "account": "TDS - _TC"}], - } - ).insert() + create_tax_withholding_category( + category_name="Cumulative Threshold TDS", + rate=10, + from_date=from_date, + to_date=to_date, + account="TDS - _TC", + single_threshold=0, + cumulative_threshold=30000.00, + ) - if not frappe.db.exists("Tax Withholding Category", "Cumulative Threshold TCS"): - frappe.get_doc( - { - "doctype": "Tax Withholding Category", - "name": "Cumulative Threshold TCS", - "category_name": "10% TCS", - "rates": [ - { - "from_date": fiscal_year[1], - "to_date": fiscal_year[2], - "tax_withholding_rate": 10, - "single_threshold": 0, - "cumulative_threshold": 30000.00, - } - ], - "accounts": [{"company": "_Test Company", "account": "TCS - _TC"}], - } - ).insert() + # Category for TCS + create_tax_withholding_category( + category_name="Cumulative Threshold TCS", + rate=10, + from_date=from_date, + to_date=to_date, + account="TCS - _TC", + single_threshold=0, + cumulative_threshold=30000.00, + ) - # Single thresold - if not frappe.db.exists("Tax Withholding Category", "Single Threshold TDS"): - frappe.get_doc( - { - "doctype": "Tax Withholding Category", - "name": "Single Threshold TDS", - "category_name": "10% TDS", - "rates": [ - { - "from_date": fiscal_year[1], - "to_date": fiscal_year[2], - "tax_withholding_rate": 10, - "single_threshold": 20000.00, - "cumulative_threshold": 0, - } - ], - "accounts": [{"company": "_Test Company", "account": "TDS - _TC"}], - } - ).insert() + # Single threshold + create_tax_withholding_category( + category_name="Single Threshold TDS", + rate=10, + from_date=from_date, + to_date=to_date, + account="TDS - _TC", + single_threshold=20000, + cumulative_threshold=0, + ) - if not frappe.db.exists("Tax Withholding Category", "New TDS Category"): - frappe.get_doc( - { - "doctype": "Tax Withholding Category", - "name": "New TDS Category", - "category_name": "New TDS Category", - "round_off_tax_amount": 1, - "consider_party_ledger_amount": 1, - "tax_on_excess_amount": 1, - "rates": [ - { - "from_date": fiscal_year[1], - "to_date": fiscal_year[2], - "tax_withholding_rate": 10, - "single_threshold": 0, - "cumulative_threshold": 30000, - } - ], - "accounts": [{"company": "_Test Company", "account": "TDS - _TC"}], - } - ).insert() + create_tax_withholding_category( + category_name="New TDS Category", + rate=10, + from_date=from_date, + to_date=to_date, + account="TDS - _TC", + single_threshold=0, + cumulative_threshold=30000, + round_off_tax_amount=1, + consider_party_ledger_amount=1, + tax_on_excess_amount=1, + ) - if not frappe.db.exists("Tax Withholding Category", "Test Service Category"): - frappe.get_doc( - { - "doctype": "Tax Withholding Category", - "name": "Test Service Category", - "category_name": "Test Service Category", - "rates": [ - { - "from_date": fiscal_year[1], - "to_date": fiscal_year[2], - "tax_withholding_rate": 10, - "single_threshold": 2000, - "cumulative_threshold": 2000, - } - ], - "accounts": [{"company": "_Test Company", "account": "TDS - _TC"}], - } - ).insert() + create_tax_withholding_category( + category_name="Test Service Category", + rate=10, + from_date=from_date, + to_date=to_date, + account="TDS - _TC", + single_threshold=2000, + cumulative_threshold=2000, + ) - if not frappe.db.exists("Tax Withholding Category", "Test Goods Category"): + create_tax_withholding_category( + category_name="Test Goods Category", + rate=10, + from_date=from_date, + to_date=to_date, + account="TDS - _TC", + single_threshold=2000, + cumulative_threshold=2000, + ) + + create_tax_withholding_category( + category_name="Test Multi Invoice Category", + rate=10, + from_date=from_date, + to_date=to_date, + account="TDS - _TC", + single_threshold=5000, + cumulative_threshold=10000, + ) + + create_tax_withholding_category( + category_name="Advance TDS Category", + rate=10, + from_date=from_date, + to_date=to_date, + account="TDS - _TC", + single_threshold=5000, + cumulative_threshold=10000, + consider_party_ledger_amount=1, + ) + + +def create_tax_withholding_category( + category_name, + rate, + from_date, + to_date, + account, + single_threshold=0, + cumulative_threshold=0, + round_off_tax_amount=0, + consider_party_ledger_amount=0, + tax_on_excess_amount=0, +): + if not frappe.db.exists("Tax Withholding Category", category_name): frappe.get_doc( { "doctype": "Tax Withholding Category", - "name": "Test Goods Category", - "category_name": "Test Goods Category", + "name": category_name, + "category_name": category_name, + "round_off_tax_amount": round_off_tax_amount, + "consider_party_ledger_amount": consider_party_ledger_amount, + "tax_on_excess_amount": tax_on_excess_amount, "rates": [ { - "from_date": fiscal_year[1], - "to_date": fiscal_year[2], - "tax_withholding_rate": 10, - "single_threshold": 2000, - "cumulative_threshold": 2000, + "from_date": from_date, + "to_date": to_date, + "tax_withholding_rate": rate, + "single_threshold": single_threshold, + "cumulative_threshold": cumulative_threshold, } ], - "accounts": [{"company": "_Test Company", "account": "TDS - _TC"}], + "accounts": [{"company": "_Test Company", "account": account}], } ).insert() diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 16072f331f..41fdb6a97f 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -128,6 +128,12 @@ def distribute_gl_based_on_cost_center_allocation(gl_map, precision=None): new_gl_map = [] for d in gl_map: cost_center = d.get("cost_center") + + # Validate budget against main cost center + validate_expense_against_budget( + d, expense_amount=flt(d.debit, precision) - flt(d.credit, precision) + ) + if cost_center and cost_center_allocation.get(cost_center): for sub_cost_center, percentage in cost_center_allocation.get(cost_center, {}).items(): gle = copy.deepcopy(d) @@ -193,7 +199,14 @@ def merge_similar_entries(gl_map, precision=None): # filter zero debit and credit entries merged_gl_map = filter( - lambda x: flt(x.debit, precision) != 0 or flt(x.credit, precision) != 0, merged_gl_map + lambda x: flt(x.debit, precision) != 0 + or flt(x.credit, precision) != 0 + or ( + x.voucher_type == "Journal Entry" + and frappe.get_cached_value("Journal Entry", x.voucher_no, "voucher_type") + == "Exchange Gain Or Loss" + ), + merged_gl_map, ) merged_gl_map = list(merged_gl_map) @@ -344,15 +357,26 @@ def process_debit_credit_difference(gl_map): allowance = get_debit_credit_allowance(voucher_type, precision) debit_credit_diff = get_debit_credit_difference(gl_map, precision) + if abs(debit_credit_diff) > allowance: - raise_debit_credit_not_equal_error(debit_credit_diff, voucher_type, voucher_no) + if not ( + voucher_type == "Journal Entry" + and frappe.get_cached_value("Journal Entry", voucher_no, "voucher_type") + == "Exchange Gain Or Loss" + ): + raise_debit_credit_not_equal_error(debit_credit_diff, voucher_type, voucher_no) elif abs(debit_credit_diff) >= (1.0 / (10**precision)): make_round_off_gle(gl_map, debit_credit_diff, precision) debit_credit_diff = get_debit_credit_difference(gl_map, precision) if abs(debit_credit_diff) > allowance: - raise_debit_credit_not_equal_error(debit_credit_diff, voucher_type, voucher_no) + if not ( + voucher_type == "Journal Entry" + and frappe.get_cached_value("Journal Entry", voucher_no, "voucher_type") + == "Exchange Gain Or Loss" + ): + raise_debit_credit_not_equal_error(debit_credit_diff, voucher_type, voucher_no) def get_debit_credit_difference(gl_map, precision): @@ -388,20 +412,22 @@ def make_round_off_gle(gl_map, debit_credit_diff, precision): round_off_account, round_off_cost_center = get_round_off_account_and_cost_center( gl_map[0].company, gl_map[0].voucher_type, gl_map[0].voucher_no ) - round_off_account_exists = False round_off_gle = frappe._dict() - for d in gl_map: - if d.account == round_off_account: - round_off_gle = d - if d.debit: - debit_credit_diff -= flt(d.debit) - else: - debit_credit_diff += flt(d.credit) - round_off_account_exists = True + round_off_account_exists = False - if round_off_account_exists and abs(debit_credit_diff) < (1.0 / (10**precision)): - gl_map.remove(round_off_gle) - return + if gl_map[0].voucher_type != "Period Closing Voucher": + for d in gl_map: + if d.account == round_off_account: + round_off_gle = d + if d.debit: + debit_credit_diff -= flt(d.debit) - flt(d.credit) + else: + debit_credit_diff += flt(d.credit) + round_off_account_exists = True + + if round_off_account_exists and abs(debit_credit_diff) < (1.0 / (10**precision)): + gl_map.remove(round_off_gle) + return if not round_off_gle: for k in ["voucher_type", "voucher_no", "company", "posting_date", "remarks"]: @@ -424,7 +450,6 @@ def make_round_off_gle(gl_map, debit_credit_diff, precision): ) update_accounting_dimensions(round_off_gle) - if not round_off_account_exists: gl_map.append(round_off_gle) @@ -489,7 +514,6 @@ def make_reverse_gl_entries( ).run(as_dict=1) if gl_entries: - create_payment_ledger_entry(gl_entries, cancel=1) create_payment_ledger_entry( gl_entries, cancel=1, adv_adj=adv_adj, update_outstanding=update_outstanding ) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index e39f22b4cf..01cfb58dec 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -207,11 +207,17 @@ def set_address_details( ) if company_address: - party_details.update({"company_address": company_address}) + party_details.company_address = company_address else: party_details.update(get_company_address(company)) - if doctype and doctype in ["Delivery Note", "Sales Invoice", "Sales Order", "Quotation"]: + if doctype and doctype in [ + "Delivery Note", + "Sales Invoice", + "Sales Order", + "Quotation", + "POS Invoice", + ]: if party_details.company_address: party_details.update( get_fetch_values(doctype, "company_address", party_details.company_address) @@ -219,12 +225,31 @@ def set_address_details( get_regional_address_details(party_details, doctype, company) elif doctype and doctype in ["Purchase Invoice", "Purchase Order", "Purchase Receipt"]: - if party_details.company_address: - party_details["shipping_address"] = shipping_address or party_details["company_address"] - party_details.shipping_address_display = get_address_display(party_details["shipping_address"]) + if shipping_address: party_details.update( - get_fetch_values(doctype, "shipping_address", party_details.shipping_address) + shipping_address=shipping_address, + shipping_address_display=get_address_display(shipping_address), + **get_fetch_values(doctype, "shipping_address", shipping_address) ) + + if party_details.company_address: + # billing address + party_details.update( + billing_address=party_details.company_address, + billing_address_display=( + party_details.company_address_display or get_address_display(party_details.company_address) + ), + **get_fetch_values(doctype, "billing_address", party_details.company_address) + ) + + # shipping address - if not already set + if not party_details.shipping_address: + party_details.update( + shipping_address=party_details.billing_address, + shipping_address_display=party_details.billing_address_display, + **get_fetch_values(doctype, "shipping_address", party_details.billing_address) + ) + get_regional_address_details(party_details, doctype, company) return party_details.get(billing_address_field), party_details.shipping_address_name @@ -277,7 +302,7 @@ def get_default_price_list(party): return party.default_price_list if party.doctype == "Customer": - return frappe.db.get_value("Customer Group", party.customer_group, "default_price_list") + return frappe.get_cached_value("Customer Group", party.customer_group, "default_price_list") def set_price_list(party_details, party, party_type, given_price_list, pos=None): @@ -366,7 +391,7 @@ def get_party_account(party_type, party=None, company=None): existing_gle_currency = get_party_gle_currency(party_type, party, company) if existing_gle_currency: if account: - account_currency = frappe.db.get_value("Account", account, "account_currency", cache=True) + account_currency = frappe.get_cached_value("Account", account, "account_currency") if (account and account_currency != existing_gle_currency) or not account: account = get_party_gle_account(party_type, party, company) @@ -383,7 +408,7 @@ def get_party_bank_account(party_type, party): def get_party_account_currency(party_type, party, company): def generator(): party_account = get_party_account(party_type, party, company) - return frappe.db.get_value("Account", party_account, "account_currency", cache=True) + return frappe.get_cached_value("Account", party_account, "account_currency") return frappe.local_cache("party_account_currency", (party_type, party, company), generator) @@ -455,15 +480,15 @@ def validate_party_accounts(doc): else: companies.append(account.company) - party_account_currency = frappe.db.get_value( - "Account", account.account, "account_currency", cache=True - ) + party_account_currency = frappe.get_cached_value("Account", account.account, "account_currency") if frappe.db.get_default("Company"): company_default_currency = frappe.get_cached_value( "Company", frappe.db.get_default("Company"), "default_currency" ) else: - company_default_currency = frappe.db.get_value("Company", account.company, "default_currency") + company_default_currency = frappe.get_cached_value( + "Company", account.company, "default_currency" + ) validate_party_gle_currency(doc.doctype, doc.name, account.company, party_account_currency) @@ -525,7 +550,7 @@ def get_due_date_from_template(template_name, posting_date, bill_date): elif term.due_date_based_on == "Day(s) after the end of the invoice month": due_date = max(due_date, add_days(get_last_day(due_date), term.credit_days)) else: - due_date = max(due_date, add_months(get_last_day(due_date), term.credit_months)) + due_date = max(due_date, get_last_day(add_months(due_date, term.credit_months))) return due_date @@ -782,7 +807,7 @@ def get_dashboard_info(party_type, party, loyalty_program=None): ) for d in companies: - company_default_currency = frappe.db.get_value("Company", d.company, "default_currency") + company_default_currency = frappe.get_cached_value("Company", d.company, "default_currency") party_account_currency = get_party_account_currency(party_type, party, d.company) if party_account_currency == company_default_currency: diff --git a/erpnext/accounts/report/accounts_payable/accounts_payable.js b/erpnext/accounts/report/accounts_payable/accounts_payable.js index 7cf14e6738..e1a30a4b77 100644 --- a/erpnext/accounts/report/accounts_payable/accounts_payable.js +++ b/erpnext/accounts/report/accounts_payable/accounts_payable.js @@ -51,6 +51,8 @@ frappe.query_reports["Accounts Payable"] = { } else { frappe.query_report.set_filter_value('tax_id', ""); } + + frappe.query_report.refresh(); } }, { diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js index 0238711a70..0b4e577f6c 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js @@ -178,6 +178,11 @@ frappe.query_reports["Accounts Receivable"] = { "fieldtype": "Data", "hidden": 1 }, + { + "fieldname": "show_remarks", + "label": __("Show Remarks"), + "fieldtype": "Check", + }, { "fieldname": "customer_name", "label": __("Customer Name"), diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index c7c746bede..94a1510f09 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -99,6 +99,9 @@ class ReceivablePayableReport(object): # Get return entries self.get_return_entries() + # Get Exchange Rate Revaluations + self.get_exchange_rate_revaluations() + self.data = [] for ple in self.ple_entries: @@ -119,6 +122,7 @@ class ReceivablePayableReport(object): party_account=ple.account, posting_date=ple.posting_date, account_currency=ple.account_currency, + remarks=ple.remarks, invoiced=0.0, paid=0.0, credit_note=0.0, @@ -165,7 +169,7 @@ class ReceivablePayableReport(object): "range4", "range5", "future_amount", - "remaining_balance" + "remaining_balance", ] def get_voucher_balance(self, ple): @@ -178,6 +182,11 @@ class ReceivablePayableReport(object): key = (ple.against_voucher_type, ple.against_voucher_no, ple.party) row = self.voucher_balance.get(key) + + if not row: + # no invoice, this is an invoice / stand-alone payment / credit note + row = self.voucher_balance.get((ple.voucher_type, ple.voucher_no, ple.party)) + return row def update_voucher_balance(self, ple): @@ -187,7 +196,11 @@ class ReceivablePayableReport(object): if not row: return - amount = ple.amount + # amount in "Party Currency", if its supplied. If not, amount in company currency + if self.filters.get(scrub(self.party_type)): + amount = ple.amount_in_account_currency + else: + amount = ple.amount amount_in_account_currency = ple.amount_in_account_currency # update voucher @@ -241,7 +254,8 @@ class ReceivablePayableReport(object): row.invoice_grand_total = row.invoiced if (abs(row.outstanding) > 1.0 / 10**self.currency_precision) and ( - abs(row.outstanding_in_account_currency) > 1.0 / 10**self.currency_precision + (abs(row.outstanding_in_account_currency) > 1.0 / 10**self.currency_precision) + or (row.voucher_no in self.err_journals) ): # non-zero oustanding, we must consider this row @@ -685,9 +699,10 @@ class ReceivablePayableReport(object): ple.party, ple.posting_date, ple.due_date, - ple.account_currency.as_("currency"), + ple.account_currency, ple.amount, ple.amount_in_account_currency, + ple.remarks, ) .where(ple.delinked == 0) .where(Criterion.all(self.qb_selection_filter)) @@ -722,6 +737,7 @@ class ReceivablePayableReport(object): def prepare_conditions(self): self.qb_selection_filter = [] party_type_field = scrub(self.party_type) + self.qb_selection_filter.append(self.ple.party_type == self.party_type) self.add_common_filters(party_type_field=party_type_field) @@ -736,7 +752,7 @@ class ReceivablePayableReport(object): self.add_accounting_dimensions_filters() - def get_cost_center_conditions(self, conditions): + def get_cost_center_conditions(self): lft, rgt = frappe.db.get_value("Cost Center", self.filters.cost_center, ["lft", "rgt"]) cost_center_list = [ center.name @@ -772,7 +788,7 @@ class ReceivablePayableReport(object): def add_customer_filters( self, ): - self.customter = qb.DocType("Customer") + self.customer = qb.DocType("Customer") if self.filters.get("customer_group"): self.get_hierarchical_filters("Customer Group", "customer_group") @@ -782,19 +798,19 @@ class ReceivablePayableReport(object): if self.filters.get("payment_terms_template"): self.qb_selection_filter.append( - self.ple.party_isin( - qb.from_(self.customer).where( - self.customer.payment_terms == self.filters.get("payment_terms_template") - ) + self.ple.party.isin( + qb.from_(self.customer) + .select(self.customer.name) + .where(self.customer.payment_terms == self.filters.get("payment_terms_template")) ) ) if self.filters.get("sales_partner"): self.qb_selection_filter.append( - self.ple.party_isin( - qb.from_(self.customer).where( - self.customer.default_sales_partner == self.filters.get("payment_terms_template") - ) + self.ple.party.isin( + qb.from_(self.customer) + .select(self.customer.name) + .where(self.customer.default_sales_partner == self.filters.get("sales_partner")) ) ) @@ -826,7 +842,7 @@ class ReceivablePayableReport(object): customer = self.customer groups = qb.from_(doc).select(doc.name).where((doc.lft >= lft) & (doc.rgt <= rgt)) customers = qb.from_(customer).select(customer.name).where(customer[key].isin(groups)) - self.qb_selection_filter.append(ple.isin(ple.party.isin(customers))) + self.qb_selection_filter.append(ple.party.isin(customers)) def add_accounting_dimensions_filters(self): accounting_dimensions = get_accounting_dimensions(as_list=False) @@ -853,10 +869,15 @@ class ReceivablePayableReport(object): def get_party_details(self, party): if not party in self.party_details: if self.party_type == "Customer": + fields = ["customer_name", "territory", "customer_group", "customer_primary_contact"] + + if self.filters.get("sales_partner"): + fields.append("default_sales_partner") + self.party_details[party] = frappe.db.get_value( "Customer", party, - ["customer_name", "territory", "customer_group", "customer_primary_contact"], + fields, as_dict=True, ) else: @@ -957,6 +978,9 @@ class ReceivablePayableReport(object): if self.filters.show_sales_person: self.add_column(label=_("Sales Person"), fieldname="sales_person", fieldtype="Data") + if self.filters.sales_partner: + self.add_column(label=_("Sales Partner"), fieldname="default_sales_partner", fieldtype="Data") + if self.filters.party_type == "Supplier": self.add_column( label=_("Supplier Group"), @@ -965,6 +989,9 @@ class ReceivablePayableReport(object): options="Supplier Group", ) + if self.filters.show_remarks: + self.add_column(label=_("Remarks"), fieldname="remarks", fieldtype="Text", width=200), + def add_column(self, label, fieldname=None, fieldtype="Currency", options=None, width=120): if not fieldname: fieldname = scrub(label) @@ -994,7 +1021,7 @@ class ReceivablePayableReport(object): "{range3}-{range4}".format( range3=cint(self.filters["range3"]) + 1, range4=self.filters["range4"] ), - "{range4}-{above}".format(range4=cint(self.filters["range4"]) + 1, above=_("Above")), + _("{range4}-Above").format(range4=cint(self.filters["range4"]) + 1), ] ): self.add_column(label=label, fieldname="range" + str(i + 1)) @@ -1013,3 +1040,17 @@ class ReceivablePayableReport(object): "data": {"labels": self.ageing_column_labels, "datasets": rows}, "type": "percentage", } + + def get_exchange_rate_revaluations(self): + je = qb.DocType("Journal Entry") + results = ( + qb.from_(je) + .select(je.name) + .where( + (je.company == self.filters.company) + & (je.posting_date.lte(self.filters.report_date)) + & (je.voucher_type == "Exchange Rate Revaluation") + ) + .run() + ) + self.err_journals = [x[0] for x in results] if results else [] diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py index edddbbce21..afd02a006e 100644 --- a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py @@ -1,19 +1,55 @@ import unittest import frappe -from frappe.utils import add_days, getdate, today +from frappe.tests.utils import FrappeTestCase, change_settings +from frappe.utils import add_days, flt, getdate, today +from erpnext import get_default_cost_center from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.report.accounts_receivable.accounts_receivable import execute +from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order -class TestAccountsReceivable(unittest.TestCase): - def test_accounts_receivable(self): +class TestAccountsReceivable(FrappeTestCase): + def setUp(self): frappe.db.sql("delete from `tabSales Invoice` where company='_Test Company 2'") + frappe.db.sql("delete from `tabSales Order` where company='_Test Company 2'") + frappe.db.sql("delete from `tabPayment Entry` where company='_Test Company 2'") frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 2'") frappe.db.sql("delete from `tabPayment Ledger Entry` where company='_Test Company 2'") + frappe.db.sql("delete from `tabJournal Entry` where company='_Test Company 2'") + frappe.db.sql("delete from `tabExchange Rate Revaluation` where company='_Test Company 2'") + self.create_usd_account() + + def tearDown(self): + frappe.db.rollback() + + def create_usd_account(self): + name = "Debtors USD" + exists = frappe.db.get_list( + "Account", filters={"company": "_Test Company 2", "account_name": "Debtors USD"} + ) + if exists: + self.debtors_usd = exists[0].name + else: + debtors = frappe.get_doc( + "Account", + frappe.db.get_list( + "Account", filters={"company": "_Test Company 2", "account_name": "Debtors"} + )[0].name, + ) + + debtors_usd = frappe.new_doc("Account") + debtors_usd.company = debtors.company + debtors_usd.account_name = "Debtors USD" + debtors_usd.account_currency = "USD" + debtors_usd.parent_account = debtors.parent_account + debtors_usd.account_type = debtors.account_type + self.debtors_usd = debtors_usd.save().name + + def test_accounts_receivable(self): filters = { "company": "_Test Company 2", "based_on_payment_terms": 1, @@ -25,7 +61,7 @@ class TestAccountsReceivable(unittest.TestCase): } # check invoice grand total and invoiced column's value for 3 payment terms - name = make_sales_invoice() + name = make_sales_invoice().name report = execute(filters) expected_data = [[100, 30], [100, 50], [100, 20]] @@ -66,8 +102,116 @@ class TestAccountsReceivable(unittest.TestCase): ], ) + def test_payment_againt_po_in_receivable_report(self): + """ + Payments made against Purchase Order will show up as outstanding amount + """ -def make_sales_invoice(): + so = make_sales_order( + company="_Test Company 2", + customer="_Test Customer 2", + warehouse="Finished Goods - _TC2", + currency="EUR", + debit_to="Debtors - _TC2", + income_account="Sales - _TC2", + expense_account="Cost of Goods Sold - _TC2", + cost_center="Main - _TC2", + ) + + pe = get_payment_entry(so.doctype, so.name) + pe = pe.save().submit() + + filters = { + "company": "_Test Company 2", + "based_on_payment_terms": 0, + "report_date": today(), + "range1": 30, + "range2": 60, + "range3": 90, + "range4": 120, + } + + report = execute(filters) + + expected_data_after_payment = [0, 1000, 0, -1000] + + row = report[1][0] + self.assertEqual( + expected_data_after_payment, + [ + row.invoiced, + row.paid, + row.credit_note, + row.outstanding, + ], + ) + + @change_settings( + "Accounts Settings", {"allow_multi_currency_invoices_against_single_party_account": 1} + ) + def test_exchange_revaluation_for_party(self): + """ + Exchange Revaluation for party on Receivable/Payable shoule be included + """ + + company = "_Test Company 2" + customer = "_Test Customer 2" + + # Using Exchange Gain/Loss account for unrealized as well. + company_doc = frappe.get_doc("Company", company) + company_doc.unrealized_exchange_gain_loss_account = company_doc.exchange_gain_loss_account + company_doc.save() + + si = make_sales_invoice(no_payment_schedule=True, do_not_submit=True) + si.currency = "USD" + si.conversion_rate = 0.90 + si.debit_to = self.debtors_usd + si = si.save().submit() + + # Exchange Revaluation + err = frappe.new_doc("Exchange Rate Revaluation") + err.company = company + err.posting_date = today() + accounts = err.get_accounts_data() + err.extend("accounts", accounts) + err.accounts[0].new_exchange_rate = 0.95 + row = err.accounts[0] + row.new_balance_in_base_currency = flt( + row.new_exchange_rate * flt(row.balance_in_account_currency) + ) + row.gain_loss = row.new_balance_in_base_currency - flt(row.balance_in_base_currency) + err.set_total_gain_loss() + err = err.save().submit() + + # Submit JV for ERR + err_journals = err.make_jv_entries() + je = frappe.get_doc("Journal Entry", err_journals.get("revaluation_jv")) + je = je.submit() + + filters = { + "company": company, + "report_date": today(), + "range1": 30, + "range2": 60, + "range3": 90, + "range4": 120, + } + report = execute(filters) + + expected_data_for_err = [0, -5, 0, 5] + row = [x for x in report[1] if x.voucher_type == je.doctype and x.voucher_no == je.name][0] + self.assertEqual( + expected_data_for_err, + [ + row.invoiced, + row.paid, + row.credit_note, + row.outstanding, + ], + ) + + +def make_sales_invoice(no_payment_schedule=False, do_not_submit=False): frappe.set_user("Administrator") si = create_sales_invoice( @@ -82,22 +226,26 @@ def make_sales_invoice(): do_not_save=1, ) - si.append( - "payment_schedule", - dict(due_date=getdate(add_days(today(), 30)), invoice_portion=30.00, payment_amount=30), - ) - si.append( - "payment_schedule", - dict(due_date=getdate(add_days(today(), 60)), invoice_portion=50.00, payment_amount=50), - ) - si.append( - "payment_schedule", - dict(due_date=getdate(add_days(today(), 90)), invoice_portion=20.00, payment_amount=20), - ) + if not no_payment_schedule: + si.append( + "payment_schedule", + dict(due_date=getdate(add_days(today(), 30)), invoice_portion=30.00, payment_amount=30), + ) + si.append( + "payment_schedule", + dict(due_date=getdate(add_days(today(), 60)), invoice_portion=50.00, payment_amount=50), + ) + si.append( + "payment_schedule", + dict(due_date=getdate(add_days(today(), 90)), invoice_portion=20.00, payment_amount=20), + ) - si.submit() + si = si.save() - return si.name + if not do_not_submit: + si = si.submit() + + return si def make_payment(docname): diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py index 889f5a22a8..29217b04be 100644 --- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py +++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py @@ -121,6 +121,9 @@ class AccountsReceivableSummary(ReceivablePayableReport): if row.sales_person: self.party_total[row.party].sales_person.append(row.sales_person) + if self.filters.sales_partner: + self.party_total[row.party]["default_sales_partner"] = row.get("default_sales_partner") + def get_columns(self): self.columns = [] self.add_column( @@ -160,6 +163,10 @@ class AccountsReceivableSummary(ReceivablePayableReport): ) if self.filters.show_sales_person: self.add_column(label=_("Sales Person"), fieldname="sales_person", fieldtype="Data") + + if self.filters.sales_partner: + self.add_column(label=_("Sales Partner"), fieldname="default_sales_partner", fieldtype="Data") + else: self.add_column( label=_("Supplier Group"), diff --git a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py index ad9b1ba58e..5827697023 100644 --- a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py +++ b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py @@ -131,8 +131,36 @@ def get_assets(filters): else 0 end), 0) as depreciation_amount_during_the_period - from `tabAsset` a, `tabDepreciation Schedule` ds - where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and a.name = ds.parent and ifnull(ds.journal_entry, '') != '' + from `tabAsset` a, `tabAsset Depreciation Schedule` ads, `tabDepreciation Schedule` ds + where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and ads.asset = a.name and ads.docstatus=1 and ads.name = ds.parent and ifnull(ds.journal_entry, '') != '' + group by a.asset_category + union + SELECT a.asset_category, + ifnull(sum(case when gle.posting_date < %(from_date)s and (ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s) then + gle.debit + else + 0 + end), 0) as accumulated_depreciation_as_on_from_date, + ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and a.disposal_date >= %(from_date)s + and a.disposal_date <= %(to_date)s and gle.posting_date <= a.disposal_date then + gle.debit + else + 0 + end), 0) as depreciation_eliminated_during_the_period, + ifnull(sum(case when gle.posting_date >= %(from_date)s and gle.posting_date <= %(to_date)s + and (ifnull(a.disposal_date, 0) = 0 or gle.posting_date <= a.disposal_date) then + gle.debit + else + 0 + end), 0) as depreciation_amount_during_the_period + from `tabGL Entry` gle + join `tabAsset` a on + gle.against_voucher = a.name + join `tabAsset Category Account` aca on + aca.parent = a.asset_category and aca.company_name = %(company)s + join `tabCompany` company on + company.name = %(company)s + where a.docstatus=1 and a.company=%(company)s and a.calculate_depreciation=0 and a.purchase_date <= %(to_date)s and gle.debit != 0 and gle.is_cancelled = 0 and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account) group by a.asset_category union SELECT a.asset_category, diff --git a/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py b/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py index 9d2deea523..449ebdcd92 100644 --- a/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py +++ b/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py @@ -22,8 +22,7 @@ def get_columns(): { "label": _("Payment Document Type"), "fieldname": "payment_document_type", - "fieldtype": "Link", - "options": "Doctype", + "fieldtype": "Data", "width": 130, }, { @@ -33,15 +32,15 @@ def get_columns(): "options": "payment_document_type", "width": 140, }, - {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 100}, + {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 120}, {"label": _("Cheque/Reference No"), "fieldname": "cheque_no", "width": 120}, - {"label": _("Clearance Date"), "fieldname": "clearance_date", "fieldtype": "Date", "width": 100}, + {"label": _("Clearance Date"), "fieldname": "clearance_date", "fieldtype": "Date", "width": 120}, { "label": _("Against Account"), "fieldname": "against", "fieldtype": "Link", "options": "Account", - "width": 170, + "width": 200, }, {"label": _("Amount"), "fieldname": "amount", "fieldtype": "Currency", "width": 120}, ] diff --git a/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py b/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py index fbc1a69ddc..b0a0e05f09 100644 --- a/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py +++ b/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py @@ -21,7 +21,7 @@ def execute(filters=None): if not filters.get("account"): return columns, [] - account_currency = frappe.db.get_value("Account", filters.account, "account_currency") + account_currency = frappe.get_cached_value("Account", filters.account, "account_currency") data = get_entries(filters) diff --git a/erpnext/accounts/report/budget_variance_report/budget_variance_report.js b/erpnext/accounts/report/budget_variance_report/budget_variance_report.js index 718b6e2fcb..5955c2e0fc 100644 --- a/erpnext/accounts/report/budget_variance_report/budget_variance_report.js +++ b/erpnext/accounts/report/budget_variance_report/budget_variance_report.js @@ -75,7 +75,7 @@ frappe.query_reports["Budget Variance Report"] = { "formatter": function (value, row, column, data, default_formatter) { value = default_formatter(value, row, column, data); - if (column.fieldname.includes('variance')) { + if (column.fieldname.includes(__("variance"))) { if (data[column.fieldname] < 0) { value = "" + value + ""; diff --git a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py index 7b774ba740..96cfab9f11 100644 --- a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py +++ b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py @@ -383,8 +383,8 @@ def get_chart_data(filters, columns, data): "data": { "labels": labels, "datasets": [ - {"name": "Budget", "chartType": "bar", "values": budget_values}, - {"name": "Actual Expense", "chartType": "bar", "values": actual_values}, + {"name": _("Budget"), "chartType": "bar", "values": budget_values}, + {"name": _("Actual Expense"), "chartType": "bar", "values": actual_values}, ], }, "type": "bar", diff --git a/erpnext/accounts/report/cash_flow/cash_flow.py b/erpnext/accounts/report/cash_flow/cash_flow.py index 75e983afc0..cb3c78a2b0 100644 --- a/erpnext/accounts/report/cash_flow/cash_flow.py +++ b/erpnext/accounts/report/cash_flow/cash_flow.py @@ -8,6 +8,7 @@ from frappe.utils import cint, cstr from erpnext.accounts.report.financial_statements import ( get_columns, + get_cost_centers_with_children, get_data, get_filtered_list_for_consolidated_report, get_period_list, @@ -160,10 +161,11 @@ def get_account_type_based_data(company, account_type, period_list, accumulated_ total = 0 for period in period_list: start_date = get_start_date(period, accumulated_values, company) + filters.start_date = start_date + filters.end_date = period["to_date"] + filters.account_type = account_type - amount = get_account_type_based_gl_data( - company, start_date, period["to_date"], account_type, filters - ) + amount = get_account_type_based_gl_data(company, filters) if amount and account_type == "Depreciation": amount *= -1 @@ -175,12 +177,12 @@ def get_account_type_based_data(company, account_type, period_list, accumulated_ return data -def get_account_type_based_gl_data(company, start_date, end_date, account_type, filters=None): +def get_account_type_based_gl_data(company, filters=None): cond = "" filters = frappe._dict(filters or {}) if filters.include_default_book_entries: - company_fb = frappe.db.get_value("Company", company, "default_finance_book") + company_fb = frappe.get_cached_value("Company", company, "default_finance_book") cond = """ AND (finance_book in (%s, %s, '') OR finance_book IS NULL) """ % ( frappe.db.escape(filters.finance_book), @@ -191,17 +193,21 @@ def get_account_type_based_gl_data(company, start_date, end_date, account_type, frappe.db.escape(cstr(filters.finance_book)) ) + if filters.get("cost_center"): + filters.cost_center = get_cost_centers_with_children(filters.cost_center) + cond += " and cost_center in %(cost_center)s" + gl_sum = frappe.db.sql_list( """ select sum(credit) - sum(debit) from `tabGL Entry` - where company=%s and posting_date >= %s and posting_date <= %s + where company=%(company)s and posting_date >= %(start_date)s and posting_date <= %(end_date)s and voucher_type != 'Period Closing Voucher' - and account in ( SELECT name FROM tabAccount WHERE account_type = %s) {cond} + and account in ( SELECT name FROM tabAccount WHERE account_type = %(account_type)s) {cond} """.format( cond=cond ), - (company, start_date, end_date, account_type), + filters, ) return gl_sum[0] if gl_sum and gl_sum[0] else 0 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 98dbbf6c44..ddee9fc1e1 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py @@ -268,10 +268,12 @@ def get_cash_flow_data(fiscal_year, companies, filters): def get_account_type_based_data(account_type, companies, fiscal_year, filters): data = {} total = 0 + filters.account_type = account_type + filters.start_date = fiscal_year.year_start_date + filters.end_date = fiscal_year.year_end_date + for company in companies: - amount = get_account_type_based_gl_data( - company, fiscal_year.year_start_date, fiscal_year.year_end_date, account_type, filters - ) + amount = get_account_type_based_gl_data(company, filters) if amount and account_type == "Depreciation": amount *= -1 @@ -533,9 +535,14 @@ def get_accounts(root_type, companies): ], filters={"company": company, "root_type": root_type}, ): - if account.account_name not in added_accounts: + if account.account_number: + account_key = account.account_number + "-" + account.account_name + else: + account_key = account.account_name + + if account_key not in added_accounts: accounts.append(account) - added_accounts.append(account.account_name) + added_accounts.append(account_key) return accounts @@ -637,7 +644,7 @@ def set_gl_entries_by_account( "rgt": root_rgt, "company": d.name, "finance_book": filters.get("finance_book"), - "company_fb": frappe.db.get_value("Company", d.name, "default_finance_book"), + "company_fb": frappe.get_cached_value("Company", d.name, "default_finance_book"), }, as_dict=True, ) diff --git a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py index cafe95b360..4765e3b318 100644 --- a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py +++ b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py @@ -26,6 +26,7 @@ class PartyLedgerSummaryReport(object): ) self.get_gl_entries() + self.get_additional_columns() self.get_return_invoices() self.get_party_adjustment_amounts() @@ -33,6 +34,42 @@ class PartyLedgerSummaryReport(object): data = self.get_data() return columns, data + def get_additional_columns(self): + """ + Additional Columns for 'User Permission' based access control + """ + from frappe import qb + + if self.filters.party_type == "Customer": + self.territories = frappe._dict({}) + self.customer_group = frappe._dict({}) + + customer = qb.DocType("Customer") + result = ( + frappe.qb.from_(customer) + .select( + customer.name, customer.territory, customer.customer_group, customer.default_sales_partner + ) + .where((customer.disabled == 0)) + .run(as_dict=True) + ) + + for x in result: + self.territories[x.name] = x.territory + self.customer_group[x.name] = x.customer_group + else: + self.supplier_group = frappe._dict({}) + supplier = qb.DocType("Supplier") + result = ( + frappe.qb.from_(supplier) + .select(supplier.name, supplier.supplier_group) + .where((supplier.disabled == 0)) + .run(as_dict=True) + ) + + for x in result: + self.supplier_group[x.name] = x.supplier_group + def get_columns(self): columns = [ { @@ -116,6 +153,35 @@ class PartyLedgerSummaryReport(object): }, ] + # Hidden columns for handling 'User Permissions' + if self.filters.party_type == "Customer": + columns += [ + { + "label": _("Territory"), + "fieldname": "territory", + "fieldtype": "Link", + "options": "Territory", + "hidden": 1, + }, + { + "label": _("Customer Group"), + "fieldname": "customer_group", + "fieldtype": "Link", + "options": "Customer Group", + "hidden": 1, + }, + ] + else: + columns += [ + { + "label": _("Supplier Group"), + "fieldname": "supplier_group", + "fieldtype": "Link", + "options": "Supplier Group", + "hidden": 1, + } + ] + return columns def get_data(self): @@ -143,6 +209,12 @@ class PartyLedgerSummaryReport(object): ), ) + if self.filters.party_type == "Customer": + self.party_data[gle.party].update({"territory": self.territories.get(gle.party)}) + self.party_data[gle.party].update({"customer_group": self.customer_group.get(gle.party)}) + else: + self.party_data[gle.party].update({"supplier_group": self.supplier_group.get(gle.party)}) + amount = gle.get(invoice_dr_or_cr) - gle.get(reverse_dr_or_cr) self.party_data[gle.party].closing_balance += amount @@ -220,8 +292,8 @@ class PartyLedgerSummaryReport(object): if self.filters.party_type == "Customer": if self.filters.get("customer_group"): - lft, rgt = frappe.db.get_value( - "Customer Group", self.filters.get("customer_group"), ["lft", "rgt"] + lft, rgt = frappe.get_cached_value( + "Customer Group", self.filters["customer_group"], ["lft", "rgt"] ) conditions.append( 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 index 1eb257ac85..3e11643776 100644 --- 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 @@ -378,15 +378,14 @@ class Deferred_Revenue_and_Expense_Report(object): 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"}) + 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) + for idx, period in enumerate(self.period_list, 0): + total_row[period.key] = self.period_total[idx].total + ret.append(total_row) return ret @@ -396,7 +395,7 @@ class Deferred_Revenue_and_Expense_Report(object): "labels": [period.label for period in self.period_list], "datasets": [ { - "name": "Actual Posting", + "name": _("Actual Posting"), "chartType": "bar", "values": [x.actual for x in self.period_total], } @@ -410,7 +409,7 @@ class Deferred_Revenue_and_Expense_Report(object): if self.filters.with_upcoming_postings: chart["data"]["datasets"].append( - {"name": "Expected", "chartType": "line", "values": [x.total for x in self.period_total]} + {"name": _("Expected"), "chartType": "line", "values": [x.total for x in self.period_total]} ) return chart diff --git a/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py b/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py index ecad9f104f..5939a26deb 100644 --- a/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py +++ b/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py @@ -90,7 +90,7 @@ def set_gl_entries_by_account(dimension_list, filters, account, gl_entries_by_ac gl_filters["dimensions"] = set(dimension_list) if filters.get("include_default_book_entries"): - gl_filters["company_fb"] = frappe.db.get_value( + gl_filters["company_fb"] = frappe.get_cached_value( "Company", filters.company, "default_finance_book" ) diff --git a/erpnext/accounts/report/financial_statements.py b/erpnext/accounts/report/financial_statements.py index 36825771f8..8c6fe43a93 100644 --- a/erpnext/accounts/report/financial_statements.py +++ b/erpnext/accounts/report/financial_statements.py @@ -440,7 +440,7 @@ def set_gl_entries_by_account( } if filters.get("include_default_book_entries"): - gl_filters["company_fb"] = frappe.db.get_value("Company", company, "default_finance_book") + gl_filters["company_fb"] = frappe.get_cached_value("Company", company, "default_finance_book") for key, value in filters.items(): if value: diff --git a/erpnext/accounts/report/general_ledger/general_ledger.html b/erpnext/accounts/report/general_ledger/general_ledger.html index 378fa3791c..475be92add 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.html +++ b/erpnext/accounts/report/general_ledger/general_ledger.html @@ -25,8 +25,8 @@ - - + + @@ -45,29 +45,28 @@
{% } %} - {{ __("Against") }}: {%= data[i].against %}
{%= __("Remarks") %}: {%= data[i].remarks %} {% if(data[i].bill_no) { %}
{%= __("Supplier Invoice No") %}: {%= data[i].bill_no %} {% } %} + {%= format_currency(data[i].debit, filters.presentation_currency || data[i].account_currency) %} + {%= format_currency(data[i].credit, filters.presentation_currency || data[i].account_currency) %} {% } else { %} {% } %} {% } %} diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index e77e828e16..27b84c4e77 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -121,7 +121,7 @@ def set_account_currency(filters): if is_same_account_currency: account_currency = currency - elif filters.get("party"): + elif filters.get("party") and filters.get("party_type"): gle_currency = frappe.db.get_value( "GL Entry", {"party_type": filters.party_type, "party": filters.party[0], "company": filters.company}, @@ -134,7 +134,7 @@ def set_account_currency(filters): account_currency = ( None if filters.party_type in ["Employee", "Shareholder", "Member"] - else frappe.db.get_value(filters.party_type, filters.party[0], "default_currency") + else frappe.get_cached_value(filters.party_type, filters.party[0], "default_currency") ) filters["account_currency"] = account_currency or filters.company_currency @@ -174,7 +174,7 @@ def get_gl_entries(filters, accounting_dimensions): order_by_statement = "order by account, posting_date, creation" if filters.get("include_default_book_entries"): - filters["company_fb"] = frappe.db.get_value( + filters["company_fb"] = frappe.get_cached_value( "Company", filters.get("company"), "default_finance_book" ) @@ -237,7 +237,7 @@ def get_conditions(filters): or filters.get("party") or filters.get("group_by") in ["Group by Account", "Group by Party"] ): - conditions.append("posting_date >=%(from_date)s") + conditions.append("(posting_date >=%(from_date)s or is_opening = 'Yes')") conditions.append("(posting_date <=%(to_date)s or is_opening = 'Yes')") @@ -286,9 +286,11 @@ def get_accounts_with_children(accounts): all_accounts = [] for d in accounts: - if frappe.db.exists("Account", d): - lft, rgt = frappe.db.get_value("Account", d, ["lft", "rgt"]) - children = frappe.get_all("Account", filters={"lft": [">=", lft], "rgt": ["<=", rgt]}) + account = frappe.get_cached_doc("Account", d) + if account: + children = frappe.get_all( + "Account", filters={"lft": [">=", account.lft], "rgt": ["<=", account.rgt]} + ) all_accounts += [c.name for c in children] else: frappe.throw(_("Account: {0} does not exist").format(d)) @@ -524,7 +526,7 @@ def get_columns(filters): "options": "GL Entry", "hidden": 1, }, - {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 90}, + {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 100}, { "label": _("Account"), "fieldname": "account", @@ -536,13 +538,13 @@ def get_columns(filters): "label": _("Debit ({0})").format(currency), "fieldname": "debit", "fieldtype": "Float", - "width": 100, + "width": 130, }, { "label": _("Credit ({0})").format(currency), "fieldname": "credit", "fieldtype": "Float", - "width": 100, + "width": 130, }, { "label": _("Balance ({0})").format(currency), diff --git a/erpnext/accounts/report/general_ledger/test_general_ledger.py b/erpnext/accounts/report/general_ledger/test_general_ledger.py index b10e769618..c563785763 100644 --- a/erpnext/accounts/report/general_ledger/test_general_ledger.py +++ b/erpnext/accounts/report/general_ledger/test_general_ledger.py @@ -109,8 +109,7 @@ class TestGeneralLedger(FrappeTestCase): frappe.db.set_value( "Company", company, "unrealized_exchange_gain_loss_account", "_Test Exchange Gain/Loss - _TC" ) - revaluation_jv = revaluation.make_jv_entry() - revaluation_jv = frappe.get_doc(revaluation_jv) + revaluation_jv = revaluation.make_jv_for_revaluation() revaluation_jv.cost_center = "_Test Cost Center - _TC" for acc in revaluation_jv.get("accounts"): acc.cost_center = "_Test Cost Center - _TC" diff --git a/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py b/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py index 9d56678541..cd5f366707 100644 --- a/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py +++ b/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py @@ -155,7 +155,6 @@ def adjust_account(data, period_list, consolidated=False): for d in data: for period in period_list: key = period if consolidated else period.key - d[key] = totals[d["account"]] d["total"] = totals[d["account"]] return data diff --git a/erpnext/accounts/report/gross_profit/gross_profit.js b/erpnext/accounts/report/gross_profit/gross_profit.js index 3d37b5898c..e89d42977b 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.js +++ b/erpnext/accounts/report/gross_profit/gross_profit.js @@ -4,7 +4,7 @@ frappe.query_reports["Gross Profit"] = { "filters": [ { - "fieldname":"company", + "fieldname": "company", "label": __("Company"), "fieldtype": "Link", "options": "Company", @@ -12,46 +12,72 @@ frappe.query_reports["Gross Profit"] = { "reqd": 1 }, { - "fieldname":"from_date", + "fieldname": "from_date", "label": __("From Date"), "fieldtype": "Date", "default": frappe.defaults.get_user_default("year_start_date"), "reqd": 1 }, { - "fieldname":"to_date", + "fieldname": "to_date", "label": __("To Date"), "fieldtype": "Date", "default": frappe.defaults.get_user_default("year_end_date"), "reqd": 1 }, { - "fieldname":"sales_invoice", + "fieldname": "sales_invoice", "label": __("Sales Invoice"), "fieldtype": "Link", "options": "Sales Invoice" }, { - "fieldname":"group_by", + "fieldname": "group_by", "label": __("Group By"), "fieldtype": "Select", "options": "Invoice\nItem Code\nItem Group\nBrand\nWarehouse\nCustomer\nCustomer Group\nTerritory\nSales Person\nProject\nMonthly\nPayment Term", "default": "Invoice" }, + { + "fieldname": "item_group", + "label": __("Item Group"), + "fieldtype": "Link", + "options": "Item Group" + }, + { + "fieldname": "sales_person", + "label": __("Sales Person"), + "fieldtype": "Link", + "options": "Sales Person" + }, + { + "fieldname": "warehouse", + "label": __("Warehouse"), + "fieldtype": "Link", + "options": "Warehouse", + "get_query": function () { + var company = frappe.query_report.get_filter_value('company'); + return { + filters: [ + ["Warehouse", "company", "=", company] + ] + }; + }, + }, ], "tree": true, "name_field": "parent", "parent_field": "parent_invoice", "initial_depth": 3, "formatter": function(value, row, column, data, default_formatter) { - if (column.fieldname == "sales_invoice" && column.options == "Item" && data.indent == 0) { + if (column.fieldname == "sales_invoice" && column.options == "Item" && data && data.indent == 0) { column._options = "Sales Invoice"; } else { column._options = "Item"; } value = default_formatter(value, row, column, data); - if (data && (data.indent == 0.0 || row[1].content == "Total")) { + if (data && (data.indent == 0.0 || (row[1] && row[1].content == "Total"))) { value = $(`${value}`); var $value = $(value).css("font-weight", "bold"); value = $value.wrap("

").parent().html(); diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index 526ea9d6e2..fde4de8402 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -3,10 +3,12 @@ import frappe -from frappe import _, scrub +from frappe import _, qb, scrub +from frappe.query_builder import Order from frappe.utils import cint, flt, formatdate from erpnext.controllers.queries import get_match_cond +from erpnext.stock.report.stock_ledger.stock_ledger import get_item_group_condition from erpnext.stock.utils import get_incoming_rate @@ -393,15 +395,16 @@ def get_column_names(): class GrossProfitGenerator(object): def __init__(self, filters=None): + self.sle = {} self.data = [] self.average_buying_rate = {} self.filters = frappe._dict(filters) self.load_invoice_items() + self.get_delivery_notes() if filters.group_by == "Invoice": self.group_items_by_invoice() - self.load_stock_ledger_entries() self.load_product_bundle() self.load_non_stock_items() self.get_returned_invoice_items() @@ -436,6 +439,18 @@ class GrossProfitGenerator(object): row.delivery_note, frappe._dict() ) row.item_row = row.dn_detail + # Update warehouse and base_amount from 'Packed Item' List + if product_bundles and not row.parent: + # For Packed Items, row.parent_invoice will be the Bundle name + product_bundle = product_bundles.get(row.parent_invoice) + if product_bundle: + for packed_item in product_bundle: + if ( + packed_item.get("item_code") == row.item_code + and packed_item.get("parent_detail_docname") == row.item_row + ): + row.warehouse = packed_item.warehouse + row.base_amount = packed_item.base_amount # get buying amount if row.item_code in product_bundles: @@ -500,7 +515,7 @@ class GrossProfitGenerator(object): invoice_portion = 100 elif row.invoice_portion: invoice_portion = row.invoice_portion - else: + elif row.payment_amount: invoice_portion = row.payment_amount * 100 / row.base_net_amount if i == 0: @@ -586,10 +601,28 @@ class GrossProfitGenerator(object): buying_amount = 0.0 for packed_item in product_bundle: if packed_item.get("parent_detail_docname") == row.item_row: - buying_amount += self.get_buying_amount(row, packed_item.item_code) + packed_item_row = row.copy() + packed_item_row.warehouse = packed_item.warehouse + buying_amount += self.get_buying_amount(packed_item_row, packed_item.item_code) return flt(buying_amount, self.currency_precision) + def calculate_buying_amount_from_sle(self, row, my_sle, parenttype, parent, item_row, item_code): + for i, sle in enumerate(my_sle): + # find the stock valution rate from stock ledger entry + if ( + sle.voucher_type == parenttype + and parent == sle.voucher_no + and sle.voucher_detail_no == item_row + ): + previous_stock_value = len(my_sle) > i + 1 and flt(my_sle[i + 1].stock_value) or 0.0 + + if previous_stock_value: + return abs(previous_stock_value - flt(sle.stock_value)) * flt(row.qty) / abs(flt(sle.qty)) + else: + return flt(row.qty) * self.get_average_buying_rate(row, item_code) + return 0.0 + def get_buying_amount(self, row, item_code): # IMP NOTE # stock_ledger_entries should already be filtered by item_code and warehouse and @@ -600,29 +633,54 @@ class GrossProfitGenerator(object): return flt(row.qty) * item_rate else: - my_sle = self.sle.get((item_code, row.warehouse)) + my_sle = self.get_stock_ledger_entries(item_code, row.warehouse) if (row.update_stock or row.dn_detail) and my_sle: parenttype, parent = row.parenttype, row.parent if row.dn_detail: parenttype, parent = "Delivery Note", row.delivery_note - for i, sle in enumerate(my_sle): - # find the stock valution rate from stock ledger entry - if ( - sle.voucher_type == parenttype - and parent == sle.voucher_no - and sle.voucher_detail_no == row.item_row - ): - previous_stock_value = len(my_sle) > i + 1 and flt(my_sle[i + 1].stock_value) or 0.0 - - if previous_stock_value: - return (previous_stock_value - flt(sle.stock_value)) * flt(row.qty) / abs(flt(sle.qty)) - else: - return flt(row.qty) * self.get_average_buying_rate(row, item_code) + return self.calculate_buying_amount_from_sle( + row, my_sle, parenttype, parent, row.item_row, item_code + ) + elif self.delivery_notes.get((row.parent, row.item_code), None): + # check if Invoice has delivery notes + dn = self.delivery_notes.get((row.parent, row.item_code)) + parenttype, parent, item_row, warehouse = ( + "Delivery Note", + dn["delivery_note"], + dn["item_row"], + dn["warehouse"], + ) + my_sle = self.get_stock_ledger_entries(item_code, row.warehouse) + return self.calculate_buying_amount_from_sle( + row, my_sle, parenttype, parent, item_row, item_code + ) + elif row.sales_order and row.so_detail: + incoming_amount = self.get_buying_amount_from_so_dn(row.sales_order, row.so_detail, item_code) + if incoming_amount: + return incoming_amount else: return flt(row.qty) * self.get_average_buying_rate(row, item_code) - return 0.0 + return flt(row.qty) * self.get_average_buying_rate(row, item_code) + + def get_buying_amount_from_so_dn(self, sales_order, so_detail, item_code): + from frappe.query_builder.functions import Sum + + delivery_note_item = frappe.qb.DocType("Delivery Note Item") + + query = ( + frappe.qb.from_(delivery_note_item) + .select(Sum(delivery_note_item.incoming_rate * delivery_note_item.stock_qty)) + .where(delivery_note_item.docstatus == 1) + .where(delivery_note_item.item_code == item_code) + .where(delivery_note_item.against_sales_order == sales_order) + .where(delivery_note_item.so_detail == so_detail) + .groupby(delivery_note_item.item_code) + ) + + incoming_amount = query.run() + return flt(incoming_amount[0][0]) if incoming_amount else 0 def get_average_buying_rate(self, row, item_code): args = row @@ -676,6 +734,17 @@ class GrossProfitGenerator(object): if self.filters.to_date: conditions += " and posting_date <= %(to_date)s" + if self.filters.item_group: + conditions += " and {0}".format(get_item_group_condition(self.filters.item_group)) + + if self.filters.sales_person: + conditions += """ + and exists(select 1 + from `tabSales Team` st + where st.parent = `tabSales Invoice`.name + and st.sales_person = %(sales_person)s) + """ + if self.filters.group_by == "Sales Person": sales_person_cols = ", sales.sales_person, sales.allocated_amount, sales.incentives" sales_team_table = "left join `tabSales Team` sales on sales.parent = `tabSales Invoice`.name" @@ -703,6 +772,13 @@ class GrossProfitGenerator(object): if self.filters.get("item_code"): conditions += " and `tabSales Invoice Item`.item_code = %(item_code)s" + if self.filters.get("warehouse"): + warehouse_details = frappe.db.get_value( + "Warehouse", self.filters.get("warehouse"), ["lft", "rgt"], as_dict=1 + ) + if warehouse_details: + conditions += f" and `tabSales Invoice Item`.warehouse in (select name from `tabWarehouse` wh where wh.lft >= {warehouse_details.lft} and wh.rgt <= {warehouse_details.rgt} and warehouse = wh.name)" + self.si_list = frappe.db.sql( """ select @@ -713,7 +789,8 @@ class GrossProfitGenerator(object): `tabSales Invoice`.territory, `tabSales Invoice Item`.item_code, `tabSales Invoice Item`.item_name, `tabSales Invoice Item`.description, `tabSales Invoice Item`.warehouse, `tabSales Invoice Item`.item_group, - `tabSales Invoice Item`.brand, `tabSales Invoice Item`.dn_detail, + `tabSales Invoice Item`.brand, `tabSales Invoice Item`.so_detail, + `tabSales Invoice Item`.sales_order, `tabSales Invoice Item`.dn_detail, `tabSales Invoice Item`.delivery_note, `tabSales Invoice Item`.stock_qty as qty, `tabSales Invoice Item`.base_net_rate, `tabSales Invoice Item`.base_net_amount, `tabSales Invoice Item`.name as "item_row", `tabSales Invoice`.is_return, @@ -723,6 +800,7 @@ class GrossProfitGenerator(object): from `tabSales Invoice` inner join `tabSales Invoice Item` on `tabSales Invoice Item`.parent = `tabSales Invoice`.name + join `tabItem` item on item.name = `tabSales Invoice Item`.item_code {sales_team_table} {payment_term_table} where @@ -740,6 +818,29 @@ class GrossProfitGenerator(object): as_dict=1, ) + def get_delivery_notes(self): + self.delivery_notes = frappe._dict({}) + if self.si_list: + invoices = [x.parent for x in self.si_list] + dni = qb.DocType("Delivery Note Item") + delivery_notes = ( + qb.from_(dni) + .select( + dni.against_sales_invoice.as_("sales_invoice"), + dni.item_code, + dni.warehouse, + dni.parent.as_("delivery_note"), + dni.name.as_("item_row"), + ) + .where((dni.docstatus == 1) & (dni.against_sales_invoice.isin(invoices))) + .groupby(dni.against_sales_invoice, dni.item_code) + .orderby(dni.creation, order=Order.desc) + .run(as_dict=True) + ) + + for entry in delivery_notes: + self.delivery_notes[(entry.sales_invoice, entry.item_code)] = entry + def group_items_by_invoice(self): """ Turns list of Sales Invoice Items to a tree of Sales Invoices with their Items as children. @@ -843,34 +944,59 @@ class GrossProfitGenerator(object): "Item", item_code, ["item_name", "description", "item_group", "brand"] ) - def load_stock_ledger_entries(self): - res = frappe.db.sql( - """select item_code, voucher_type, voucher_no, - voucher_detail_no, stock_value, warehouse, actual_qty as qty - from `tabStock Ledger Entry` - where company=%(company)s and is_cancelled = 0 - order by - item_code desc, warehouse desc, posting_date desc, - posting_time desc, creation desc""", - self.filters, - as_dict=True, - ) - self.sle = {} - for r in res: - if (r.item_code, r.warehouse) not in self.sle: - self.sle[(r.item_code, r.warehouse)] = [] + def get_stock_ledger_entries(self, item_code, warehouse): + if item_code and warehouse: + if (item_code, warehouse) not in self.sle: + sle = qb.DocType("Stock Ledger Entry") + res = ( + qb.from_(sle) + .select( + sle.item_code, + sle.voucher_type, + sle.voucher_no, + sle.voucher_detail_no, + sle.stock_value, + sle.warehouse, + sle.actual_qty.as_("qty"), + ) + .where( + (sle.company == self.filters.company) + & (sle.item_code == item_code) + & (sle.warehouse == warehouse) + & (sle.is_cancelled == 0) + ) + .orderby(sle.item_code) + .orderby(sle.warehouse, sle.posting_date, sle.posting_time, sle.creation, order=Order.desc) + .run(as_dict=True) + ) - self.sle[(r.item_code, r.warehouse)].append(r) + self.sle[(item_code, warehouse)] = res + + return self.sle[(item_code, warehouse)] + return [] def load_product_bundle(self): self.product_bundles = {} - for d in frappe.db.sql( - """select parenttype, parent, parent_item, - item_code, warehouse, -1*qty as total_qty, parent_detail_docname - from `tabPacked Item` where docstatus=1""", - as_dict=True, - ): + pki = qb.DocType("Packed Item") + + pki_query = ( + frappe.qb.from_(pki) + .select( + pki.parenttype, + pki.parent, + pki.parent_item, + pki.item_code, + pki.warehouse, + (-1 * pki.qty).as_("total_qty"), + pki.rate, + (pki.rate * pki.qty).as_("base_amount"), + pki.parent_detail_docname, + ) + .where(pki.docstatus == 1) + ) + + for d in pki_query.run(as_dict=True): self.product_bundles.setdefault(d.parenttype, frappe._dict()).setdefault( d.parent, frappe._dict() ).setdefault(d.parent_item, []).append(d) diff --git a/erpnext/accounts/report/gross_profit/test_gross_profit.py b/erpnext/accounts/report/gross_profit/test_gross_profit.py new file mode 100644 index 0000000000..21681bef5b --- /dev/null +++ b/erpnext/accounts/report/gross_profit/test_gross_profit.py @@ -0,0 +1,383 @@ +import frappe +from frappe import qb +from frappe.tests.utils import FrappeTestCase +from frappe.utils import add_days, flt, nowdate + +from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_delivery_note +from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice +from erpnext.accounts.report.gross_profit.gross_profit import execute +from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice +from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note +from erpnext.stock.doctype.item.test_item import create_item +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry + + +class TestGrossProfit(FrappeTestCase): + def setUp(self): + self.create_company() + self.create_item() + self.create_bundle() + self.create_customer() + self.create_sales_invoice() + self.clear_old_entries() + + def tearDown(self): + frappe.db.rollback() + + def create_company(self): + company_name = "_Test Gross Profit" + abbr = "_GP" + if frappe.db.exists("Company", company_name): + company = frappe.get_doc("Company", company_name) + else: + company = frappe.get_doc( + { + "doctype": "Company", + "company_name": company_name, + "country": "India", + "default_currency": "INR", + "create_chart_of_accounts_based_on": "Standard Template", + "chart_of_accounts": "Standard", + } + ) + company = company.save() + + self.company = company.name + self.cost_center = company.cost_center + self.warehouse = "Stores - " + abbr + self.finished_warehouse = "Finished Goods - " + abbr + self.income_account = "Sales - " + abbr + self.expense_account = "Cost of Goods Sold - " + abbr + self.debit_to = "Debtors - " + abbr + self.creditors = "Creditors - " + abbr + + def create_item(self): + item = create_item( + item_code="_Test GP Item", is_stock_item=1, company=self.company, warehouse=self.warehouse + ) + self.item = item if isinstance(item, str) else item.item_code + + def create_bundle(self): + from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle + + item2 = create_item( + item_code="_Test GP Item 2", is_stock_item=1, company=self.company, warehouse=self.warehouse + ) + self.item2 = item2 if isinstance(item2, str) else item2.item_code + + # This will be parent item + bundle = create_item( + item_code="_Test GP bundle", is_stock_item=0, company=self.company, warehouse=self.warehouse + ) + self.bundle = bundle if isinstance(bundle, str) else bundle.item_code + + # Create Product Bundle + self.product_bundle = make_product_bundle(parent=self.bundle, items=[self.item, self.item2]) + + def create_customer(self): + name = "_Test GP Customer" + if frappe.db.exists("Customer", name): + self.customer = name + else: + customer = frappe.new_doc("Customer") + customer.customer_name = name + customer.type = "Individual" + customer.save() + self.customer = customer.name + + def create_sales_invoice( + self, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False + ): + """ + Helper function to populate default values in sales invoice + """ + sinv = create_sales_invoice( + qty=qty, + rate=rate, + company=self.company, + customer=self.customer, + item_code=self.item, + item_name=self.item, + cost_center=self.cost_center, + warehouse=self.warehouse, + debit_to=self.debit_to, + parent_cost_center=self.cost_center, + update_stock=0, + currency="INR", + is_pos=0, + is_return=0, + return_against=None, + income_account=self.income_account, + expense_account=self.expense_account, + do_not_save=do_not_save, + do_not_submit=do_not_submit, + ) + return sinv + + def create_delivery_note( + self, item=None, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False + ): + """ + Helper function to populate default values in Delivery Note + """ + dnote = create_delivery_note( + company=self.company, + customer=self.customer, + currency="INR", + item=item or self.item, + qty=qty, + rate=rate, + cost_center=self.cost_center, + warehouse=self.warehouse, + return_against=None, + expense_account=self.expense_account, + do_not_save=do_not_save, + do_not_submit=do_not_submit, + ) + return dnote + + def clear_old_entries(self): + doctype_list = [ + "Sales Invoice", + "GL Entry", + "Payment Ledger Entry", + "Stock Entry", + "Stock Ledger Entry", + "Delivery Note", + ] + for doctype in doctype_list: + qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run() + + def test_invoice_without_only_delivery_note(self): + """ + Test buying amount for Invoice without `update_stock` flag set but has Delivery Note + """ + se = make_stock_entry( + company=self.company, + item_code=self.item, + target=self.warehouse, + qty=1, + basic_rate=100, + do_not_submit=True, + ) + item = se.items[0] + se.append( + "items", + { + "item_code": item.item_code, + "s_warehouse": item.s_warehouse, + "t_warehouse": item.t_warehouse, + "qty": 1, + "basic_rate": 200, + "conversion_factor": item.conversion_factor or 1.0, + "transfer_qty": flt(item.qty) * (flt(item.conversion_factor) or 1.0), + "serial_no": item.serial_no, + "batch_no": item.batch_no, + "cost_center": item.cost_center, + "expense_account": item.expense_account, + }, + ) + se = se.save().submit() + + sinv = create_sales_invoice( + qty=1, + rate=100, + company=self.company, + customer=self.customer, + item_code=self.item, + item_name=self.item, + cost_center=self.cost_center, + warehouse=self.warehouse, + debit_to=self.debit_to, + parent_cost_center=self.cost_center, + update_stock=0, + currency="INR", + income_account=self.income_account, + expense_account=self.expense_account, + ) + + filters = frappe._dict( + company=self.company, from_date=nowdate(), to_date=nowdate(), group_by="Invoice" + ) + + columns, data = execute(filters=filters) + + # Without Delivery Note, buying rate should be 150 + expected_entry_without_dn = { + "parent_invoice": sinv.name, + "currency": "INR", + "sales_invoice": self.item, + "customer": self.customer, + "posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()), + "item_code": self.item, + "item_name": self.item, + "warehouse": "Stores - _GP", + "qty": 1.0, + "avg._selling_rate": 100.0, + "valuation_rate": 150.0, + "selling_amount": 100.0, + "buying_amount": 150.0, + "gross_profit": -50.0, + "gross_profit_%": -50.0, + } + gp_entry = [x for x in data if x.parent_invoice == sinv.name] + self.assertDictContainsSubset(expected_entry_without_dn, gp_entry[0]) + + # make delivery note + dn = make_delivery_note(sinv.name) + dn.items[0].qty = 1 + dn = dn.save().submit() + + columns, data = execute(filters=filters) + + # Without Delivery Note, buying rate should be 100 + expected_entry_with_dn = { + "parent_invoice": sinv.name, + "currency": "INR", + "sales_invoice": self.item, + "customer": self.customer, + "posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()), + "item_code": self.item, + "item_name": self.item, + "warehouse": "Stores - _GP", + "qty": 1.0, + "avg._selling_rate": 100.0, + "valuation_rate": 100.0, + "selling_amount": 100.0, + "buying_amount": 100.0, + "gross_profit": 0.0, + "gross_profit_%": 0.0, + } + gp_entry = [x for x in data if x.parent_invoice == sinv.name] + self.assertDictContainsSubset(expected_entry_with_dn, gp_entry[0]) + + def test_bundled_delivery_note_with_different_warehouses(self): + """ + Test Delivery Note with bundled item. Packed Item from the bundle having different warehouses + """ + se = make_stock_entry( + company=self.company, + item_code=self.item, + target=self.warehouse, + qty=1, + basic_rate=100, + do_not_submit=True, + ) + item = se.items[0] + se.append( + "items", + { + "item_code": self.item2, + "s_warehouse": "", + "t_warehouse": self.finished_warehouse, + "qty": 1, + "basic_rate": 100, + "conversion_factor": item.conversion_factor or 1.0, + "transfer_qty": flt(item.qty) * (flt(item.conversion_factor) or 1.0), + "serial_no": item.serial_no, + "batch_no": item.batch_no, + "cost_center": item.cost_center, + "expense_account": item.expense_account, + }, + ) + se = se.save().submit() + + # Make a Delivery note with Product bundle + # Packed Items will have different warehouses + dnote = self.create_delivery_note(item=self.bundle, qty=1, rate=200, do_not_submit=True) + dnote.packed_items[1].warehouse = self.finished_warehouse + dnote = dnote.submit() + + # make Sales Invoice for above delivery note + sinv = make_sales_invoice(dnote.name) + sinv = sinv.save().submit() + + filters = frappe._dict( + company=self.company, + from_date=nowdate(), + to_date=nowdate(), + group_by="Invoice", + sales_invoice=sinv.name, + ) + + columns, data = execute(filters=filters) + self.assertGreater(len(data), 0) + + def test_order_connected_dn_and_inv(self): + from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order + + """ + Test gp calculation when invoice and delivery note aren't directly connected. + SO -- INV + | + DN + """ + se = make_stock_entry( + company=self.company, + item_code=self.item, + target=self.warehouse, + qty=3, + basic_rate=100, + do_not_submit=True, + ) + item = se.items[0] + se.append( + "items", + { + "item_code": item.item_code, + "s_warehouse": item.s_warehouse, + "t_warehouse": item.t_warehouse, + "qty": 10, + "basic_rate": 200, + "conversion_factor": item.conversion_factor or 1.0, + "transfer_qty": flt(item.qty) * (flt(item.conversion_factor) or 1.0), + "serial_no": item.serial_no, + "batch_no": item.batch_no, + "cost_center": item.cost_center, + "expense_account": item.expense_account, + }, + ) + se = se.save().submit() + + so = make_sales_order( + customer=self.customer, + company=self.company, + warehouse=self.warehouse, + item=self.item, + qty=4, + do_not_save=False, + do_not_submit=False, + ) + + from erpnext.selling.doctype.sales_order.sales_order import ( + make_delivery_note, + make_sales_invoice, + ) + + make_delivery_note(so.name).submit() + sinv = make_sales_invoice(so.name).submit() + + filters = frappe._dict( + company=self.company, from_date=nowdate(), to_date=nowdate(), group_by="Invoice" + ) + + columns, data = execute(filters=filters) + expected_entry = { + "parent_invoice": sinv.name, + "currency": "INR", + "sales_invoice": self.item, + "customer": self.customer, + "posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()), + "item_code": self.item, + "item_name": self.item, + "warehouse": "Stores - _GP", + "qty": 4.0, + "avg._selling_rate": 100.0, + "valuation_rate": 125.0, + "selling_amount": 400.0, + "buying_amount": 500.0, + "gross_profit": -100.0, + "gross_profit_%": -25.0, + } + gp_entry = [x for x in data if x.parent_invoice == sinv.name] + self.assertDictContainsSubset(expected_entry, gp_entry[0]) diff --git a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py index c04b9c7125..d34c21348c 100644 --- a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py +++ b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py @@ -53,9 +53,6 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum item_details = get_item_details() for d in item_list: - if not d.stock_qty: - continue - item_record = item_details.get(d.item_code) purchase_receipt = None @@ -94,7 +91,7 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum "expense_account": expense_account, "stock_qty": d.stock_qty, "stock_uom": d.stock_uom, - "rate": d.base_net_amount / d.stock_qty, + "rate": d.base_net_amount / d.stock_qty if d.stock_qty else d.base_net_amount, "amount": d.base_net_amount, } ) diff --git a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py index ac70666654..c987231fe1 100644 --- a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py +++ b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py @@ -97,6 +97,7 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum row.update({"rate": d.base_net_rate, "amount": d.base_net_amount}) total_tax = 0 + total_other_charges = 0 for tax in tax_columns: item_tax = itemised_tax.get(d.name, {}).get(tax, {}) row.update( @@ -105,10 +106,18 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum frappe.scrub(tax + " Amount"): item_tax.get("tax_amount", 0), } ) - total_tax += flt(item_tax.get("tax_amount")) + if item_tax.get("is_other_charges"): + total_other_charges += flt(item_tax.get("tax_amount")) + else: + total_tax += flt(item_tax.get("tax_amount")) row.update( - {"total_tax": total_tax, "total": d.base_net_amount + total_tax, "currency": company_currency} + { + "total_tax": total_tax, + "total_other_charges": total_other_charges, + "total": d.base_net_amount + total_tax, + "currency": company_currency, + } ) if filters.get("group_by"): @@ -477,7 +486,7 @@ def get_tax_accounts( tax_details = frappe.db.sql( """ select - name, parent, description, item_wise_tax_detail, + name, parent, description, item_wise_tax_detail, account_head, charge_type, {add_deduct_tax}, base_tax_amount_after_discount_amount from `tab%s` where @@ -493,11 +502,22 @@ def get_tax_accounts( tuple([doctype] + list(invoice_item_row)), ) + account_doctype = frappe.qb.DocType("Account") + + query = ( + frappe.qb.from_(account_doctype) + .select(account_doctype.name) + .where((account_doctype.account_type == "Tax")) + ) + + tax_accounts = query.run() + for ( name, parent, description, item_wise_tax_detail, + account_head, charge_type, add_deduct_tax, tax_amount, @@ -540,7 +560,11 @@ def get_tax_accounts( ) itemised_tax.setdefault(d.name, {})[description] = frappe._dict( - {"tax_rate": tax_rate, "tax_amount": tax_value} + { + "tax_rate": tax_rate, + "tax_amount": tax_value, + "is_other_charges": 0 if tuple([account_head]) in tax_accounts else 1, + } ) except ValueError: @@ -583,6 +607,13 @@ def get_tax_accounts( "options": "currency", "width": 100, }, + { + "label": _("Total Other Charges"), + "fieldname": "total_other_charges", + "fieldtype": "Currency", + "options": "currency", + "width": 100, + }, { "label": _("Total"), "fieldname": "total", diff --git a/erpnext/regional/doctype/ksa_vat_sales_account/__init__.py b/erpnext/accounts/report/payment_ledger/__init__.py similarity index 100% rename from erpnext/regional/doctype/ksa_vat_sales_account/__init__.py rename to erpnext/accounts/report/payment_ledger/__init__.py diff --git a/erpnext/accounts/report/payment_ledger/payment_ledger.js b/erpnext/accounts/report/payment_ledger/payment_ledger.js new file mode 100644 index 0000000000..9779844dc9 --- /dev/null +++ b/erpnext/accounts/report/payment_ledger/payment_ledger.js @@ -0,0 +1,59 @@ +// Copyright (c) 2022, 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":"period_start_date", + "label": __("Start Date"), + "fieldtype": "Date", + "reqd": 1, + "default": frappe.datetime.add_months(frappe.datetime.get_today(), -1) + }, + { + "fieldname":"period_end_date", + "label": __("End Date"), + "fieldtype": "Date", + "reqd": 1, + "default": frappe.datetime.get_today() + }, + { + "fieldname":"account", + "label": __("Account"), + "fieldtype": "MultiSelectList", + "options": "Account", + get_data: function(txt) { + return frappe.db.get_link_options('Account', txt, { + company: frappe.query_report.get_filter_value("company") + }); + } + }, + { + "fieldname":"voucher_no", + "label": __("Voucher No"), + "fieldtype": "Data", + "width": 100, + }, + { + "fieldname":"against_voucher_no", + "label": __("Against Voucher No"), + "fieldtype": "Data", + "width": 100, + }, + + ] + return filters; +} + +frappe.query_reports["Payment Ledger"] = { + "filters": get_filters() +}; diff --git a/erpnext/regional/report/ksa_vat/ksa_vat.json b/erpnext/accounts/report/payment_ledger/payment_ledger.json similarity index 51% rename from erpnext/regional/report/ksa_vat/ksa_vat.json rename to erpnext/accounts/report/payment_ledger/payment_ledger.json index 036e260310..716329fbef 100644 --- a/erpnext/regional/report/ksa_vat/ksa_vat.json +++ b/erpnext/accounts/report/payment_ledger/payment_ledger.json @@ -1,32 +1,32 @@ { "add_total_row": 0, "columns": [], - "creation": "2021-07-13 08:54:38.000949", - "disable_prepared_report": 1, - "disabled": 1, + "creation": "2022-06-06 08:50:43.933708", + "disable_prepared_report": 0, + "disabled": 0, "docstatus": 0, "doctype": "Report", "filters": [], "idx": 0, "is_standard": "Yes", - "modified": "2021-08-26 04:14:37.202594", + "modified": "2022-06-06 08:50:43.933708", "modified_by": "Administrator", - "module": "Regional", - "name": "KSA VAT", + "module": "Accounts", + "name": "Payment Ledger", "owner": "Administrator", - "prepared_report": 1, - "ref_doctype": "GL Entry", - "report_name": "KSA VAT", + "prepared_report": 0, + "ref_doctype": "Payment Ledger Entry", + "report_name": "Payment Ledger", "report_type": "Script Report", "roles": [ { - "role": "System Manager" + "role": "Accounts User" }, { "role": "Accounts Manager" }, { - "role": "Accounts User" + "role": "Auditor" } ] } \ No newline at end of file diff --git a/erpnext/accounts/report/payment_ledger/payment_ledger.py b/erpnext/accounts/report/payment_ledger/payment_ledger.py new file mode 100644 index 0000000000..e470c2727e --- /dev/null +++ b/erpnext/accounts/report/payment_ledger/payment_ledger.py @@ -0,0 +1,222 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from collections import OrderedDict + +import frappe +from frappe import _, qb +from frappe.query_builder import Criterion + + +class PaymentLedger(object): + def __init__(self, filters=None): + self.filters = filters + self.columns, self.data = [], [] + self.voucher_dict = OrderedDict() + self.voucher_amount = [] + self.ple = qb.DocType("Payment Ledger Entry") + + def init_voucher_dict(self): + + if self.voucher_amount: + s = set() + # build a set of unique vouchers + for ple in self.voucher_amount: + key = (ple.voucher_type, ple.voucher_no, ple.party) + s.add(key) + + # for each unique vouchers, initialize +/- list + for key in s: + self.voucher_dict[key] = frappe._dict(increase=list(), decrease=list()) + + # for each ple, using against voucher and amount, assign it to +/- list + # group by against voucher + for ple in self.voucher_amount: + against_key = (ple.against_voucher_type, ple.against_voucher_no, ple.party) + target = None + if self.voucher_dict.get(against_key): + if ple.amount > 0: + target = self.voucher_dict.get(against_key).increase + else: + target = self.voucher_dict.get(against_key).decrease + + # this if condition will lose unassigned ple entries(against_voucher doc doesn't have ple) + # need to somehow include the stray entries as well. + if target is not None: + entry = frappe._dict( + company=ple.company, + account=ple.account, + party_type=ple.party_type, + party=ple.party, + voucher_type=ple.voucher_type, + voucher_no=ple.voucher_no, + against_voucher_type=ple.against_voucher_type, + against_voucher_no=ple.against_voucher_no, + amount=ple.amount, + currency=ple.account_currency, + ) + + if self.filters.include_account_currency: + entry["amount_in_account_currency"] = ple.amount_in_account_currency + + target.append(entry) + + def build_data(self): + self.data.clear() + + for value in self.voucher_dict.values(): + voucher_data = [] + if value.increase != []: + voucher_data.extend(value.increase) + if value.decrease != []: + voucher_data.extend(value.decrease) + + if voucher_data: + # balance row + total = 0 + total_in_account_currency = 0 + + for x in voucher_data: + total += x.amount + if self.filters.include_account_currency: + total_in_account_currency += x.amount_in_account_currency + + entry = frappe._dict( + against_voucher_no="Outstanding:", + amount=total, + currency=voucher_data[0].currency, + ) + + if self.filters.include_account_currency: + entry["amount_in_account_currency"] = total_in_account_currency + + voucher_data.append(entry) + + # empty row + voucher_data.append(frappe._dict()) + self.data.extend(voucher_data) + + def build_conditions(self): + self.conditions = [] + + if self.filters.company: + self.conditions.append(self.ple.company == self.filters.company) + + if self.filters.account: + self.conditions.append(self.ple.account.isin(self.filters.account)) + + if self.filters.period_start_date: + self.conditions.append(self.ple.posting_date.gte(self.filters.period_start_date)) + + if self.filters.period_end_date: + self.conditions.append(self.ple.posting_date.lte(self.filters.period_end_date)) + + if self.filters.voucher_no: + self.conditions.append(self.ple.voucher_no == self.filters.voucher_no) + + if self.filters.against_voucher_no: + self.conditions.append(self.ple.against_voucher_no == self.filters.against_voucher_no) + + def get_data(self): + ple = self.ple + + self.build_conditions() + + # fetch data from table + self.voucher_amount = ( + qb.from_(ple) + .select(ple.star) + .where(ple.delinked == 0) + .where(Criterion.all(self.conditions)) + .run(as_dict=True) + ) + + def get_columns(self): + options = None + self.columns.append( + dict(label=_("Company"), fieldname="company", fieldtype="data", options=options, width="100") + ) + + self.columns.append( + dict(label=_("Account"), fieldname="account", fieldtype="data", options=options, width="100") + ) + + self.columns.append( + dict( + label=_("Party Type"), fieldname="party_type", fieldtype="data", options=options, width="100" + ) + ) + self.columns.append( + dict(label=_("Party"), fieldname="party", fieldtype="data", options=options, width="100") + ) + self.columns.append( + dict( + label=_("Voucher Type"), + fieldname="voucher_type", + fieldtype="data", + options=options, + width="100", + ) + ) + self.columns.append( + dict( + label=_("Voucher No"), fieldname="voucher_no", fieldtype="data", options=options, width="100" + ) + ) + self.columns.append( + dict( + label=_("Against Voucher Type"), + fieldname="against_voucher_type", + fieldtype="data", + options=options, + width="100", + ) + ) + self.columns.append( + dict( + label=_("Against Voucher No"), + fieldname="against_voucher_no", + fieldtype="data", + options=options, + width="100", + ) + ) + self.columns.append( + dict( + label=_("Amount"), + fieldname="amount", + fieldtype="Currency", + options="Company:company:default_currency", + width="100", + ) + ) + + if self.filters.include_account_currency: + self.columns.append( + dict( + label=_("Amount in Account Currency"), + fieldname="amount_in_account_currency", + fieldtype="Currency", + options="currency", + width="100", + ) + ) + self.columns.append( + dict(label=_("Currency"), fieldname="currency", fieldtype="Currency", hidden=True) + ) + + def run(self): + self.get_columns() + self.get_data() + + # initialize dictionary and group using against voucher + self.init_voucher_dict() + + # convert dictionary to list and add balance rows + self.build_data() + + return self.columns, self.data + + +def execute(filters=None): + return PaymentLedger(filters).run() diff --git a/erpnext/accounts/report/payment_ledger/test_payment_ledger.py b/erpnext/accounts/report/payment_ledger/test_payment_ledger.py new file mode 100644 index 0000000000..5ae9b87cde --- /dev/null +++ b/erpnext/accounts/report/payment_ledger/test_payment_ledger.py @@ -0,0 +1,65 @@ +import unittest + +import frappe +from frappe import qb +from frappe.tests.utils import FrappeTestCase + +from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry +from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice +from erpnext.accounts.report.payment_ledger.payment_ledger import execute + + +class TestPaymentLedger(FrappeTestCase): + def setUp(self): + self.create_company() + self.cleanup() + + def cleanup(self): + doctypes = [] + doctypes.append(qb.DocType("GL Entry")) + doctypes.append(qb.DocType("Payment Ledger Entry")) + doctypes.append(qb.DocType("Sales Invoice")) + doctypes.append(qb.DocType("Payment Entry")) + + for doctype in doctypes: + qb.from_(doctype).delete().where(doctype.company == self.company).run() + + def create_company(self): + name = "Test Payment Ledger" + company = None + if frappe.db.exists("Company", name): + company = frappe.get_doc("Company", name) + else: + company = frappe.get_doc( + { + "doctype": "Company", + "company_name": name, + "country": "India", + "default_currency": "INR", + "create_chart_of_accounts_based_on": "Standard Template", + "chart_of_accounts": "Standard", + } + ) + company = company.save() + self.company = company.name + self.cost_center = company.cost_center + self.warehouse = "All Warehouses" + " - " + company.abbr + self.income_account = company.default_income_account + self.expense_account = company.default_expense_account + self.debit_to = company.default_receivable_account + + def test_unpaid_invoice_outstanding(self): + sinv = create_sales_invoice( + company=self.company, + debit_to=self.debit_to, + expense_account=self.expense_account, + cost_center=self.cost_center, + income_account=self.income_account, + warehouse=self.warehouse, + ) + pe = get_payment_entry(sinv.doctype, sinv.name).save().submit() + + filters = frappe._dict({"company": self.company}) + columns, data = execute(filters=filters) + outstanding = [x for x in data if x.get("against_voucher_no") == "Outstanding:"] + self.assertEqual(outstanding[0].get("amount"), 0) diff --git a/erpnext/accounts/report/purchase_register/purchase_register.py b/erpnext/accounts/report/purchase_register/purchase_register.py index e8a1e795d9..a05d581207 100644 --- a/erpnext/accounts/report/purchase_register/purchase_register.py +++ b/erpnext/accounts/report/purchase_register/purchase_register.py @@ -232,12 +232,12 @@ def get_conditions(filters): conditions += ( common_condition - + "and ifnull(`tabPurchase Invoice Item`.{0}, '') in %({0})s)".format(dimension.fieldname) + + "and ifnull(`tabPurchase Invoice`.{0}, '') in %({0})s)".format(dimension.fieldname) ) else: conditions += ( common_condition - + "and ifnull(`tabPurchase Invoice Item`.{0}, '') in %({0})s)".format(dimension.fieldname) + + "and ifnull(`tabPurchase Invoice`.{0}, '') in %({0})s)".format(dimension.fieldname) ) return conditions diff --git a/erpnext/accounts/report/sales_register/sales_register.py b/erpnext/accounts/report/sales_register/sales_register.py index 33bd3c7496..b333901d7b 100644 --- a/erpnext/accounts/report/sales_register/sales_register.py +++ b/erpnext/accounts/report/sales_register/sales_register.py @@ -370,7 +370,7 @@ def get_conditions(filters): where parent=`tabSales Invoice`.name and ifnull(`tab{table}`.{field}, '') = %({field})s)""" - conditions += get_sales_invoice_item_field_condition("mode_of_payments", "Sales Invoice Payment") + conditions += get_sales_invoice_item_field_condition("mode_of_payment", "Sales Invoice Payment") conditions += get_sales_invoice_item_field_condition("cost_center") conditions += get_sales_invoice_item_field_condition("warehouse") conditions += get_sales_invoice_item_field_condition("brand") @@ -390,12 +390,12 @@ def get_conditions(filters): conditions += ( common_condition - + "and ifnull(`tabSales Invoice Item`.{0}, '') in %({0})s)".format(dimension.fieldname) + + "and ifnull(`tabSales Invoice`.{0}, '') in %({0})s)".format(dimension.fieldname) ) else: conditions += ( common_condition - + "and ifnull(`tabSales Invoice Item`.{0}, '') in %({0})s)".format(dimension.fieldname) + + "and ifnull(`tabSales Invoice`.{0}, '') in %({0})s)".format(dimension.fieldname) ) return conditions diff --git a/erpnext/accounts/report/supplier_ledger_summary/supplier_ledger_summary.js b/erpnext/accounts/report/supplier_ledger_summary/supplier_ledger_summary.js index f81297760e..5dc4c3d1c1 100644 --- a/erpnext/accounts/report/supplier_ledger_summary/supplier_ledger_summary.js +++ b/erpnext/accounts/report/supplier_ledger_summary/supplier_ledger_summary.js @@ -63,24 +63,6 @@ frappe.query_reports["Supplier Ledger Summary"] = { "fieldtype": "Link", "options": "Payment Terms Template" }, - { - "fieldname":"territory", - "label": __("Territory"), - "fieldtype": "Link", - "options": "Territory" - }, - { - "fieldname":"sales_partner", - "label": __("Sales Partner"), - "fieldtype": "Link", - "options": "Sales Partner" - }, - { - "fieldname":"sales_person", - "label": __("Sales Person"), - "fieldtype": "Link", - "options": "Sales Person" - }, { "fieldname":"tax_id", "label": __("Tax Id"), diff --git a/erpnext/accounts/report/tax_detail/tax_detail.py b/erpnext/accounts/report/tax_detail/tax_detail.py index ba8d307228..ba733c2d18 100644 --- a/erpnext/accounts/report/tax_detail/tax_detail.py +++ b/erpnext/accounts/report/tax_detail/tax_detail.py @@ -234,8 +234,11 @@ def modify_report_columns(doctype, field, column): if field in ["item_tax_rate", "base_net_amount"]: return None - if doctype == "GL Entry" and field in ["debit", "credit"]: - column.update({"label": _("Amount"), "fieldname": "amount"}) + if doctype == "GL Entry": + if field in ["debit", "credit"]: + column.update({"label": _("Amount"), "fieldname": "amount"}) + elif field == "voucher_type": + column.update({"fieldtype": "Data", "options": ""}) if field == "taxes_and_charges": column.update({"label": _("Taxes and Charges Template")}) diff --git a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py index 08d2008682..c6aa21cc86 100644 --- a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py +++ b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py @@ -14,9 +14,17 @@ def execute(filters=None): filters.naming_series = frappe.db.get_single_value("Buying Settings", "supp_master_name") columns = get_columns(filters) - tds_docs, tds_accounts, tax_category_map = get_tds_docs(filters) + ( + tds_docs, + tds_accounts, + tax_category_map, + journal_entry_party_map, + invoice_total_map, + ) = get_tds_docs(filters) - res = get_result(filters, tds_docs, tds_accounts, tax_category_map) + res = get_result( + filters, tds_docs, tds_accounts, tax_category_map, journal_entry_party_map, invoice_total_map + ) final_result = group_by_supplier_and_category(res) return columns, final_result 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 16e0ac1de6..bfe2a0fd2b 100644 --- a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py +++ b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py @@ -4,15 +4,24 @@ import frappe from frappe import _ +from frappe.utils import flt def execute(filters=None): validate_filters(filters) - tds_docs, tds_accounts, tax_category_map, journal_entry_party_map = get_tds_docs(filters) + ( + tds_docs, + tds_accounts, + tax_category_map, + journal_entry_party_map, + invoice_net_total_map, + ) = get_tds_docs(filters) columns = get_columns(filters) - res = get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_party_map) + res = get_result( + filters, tds_docs, tds_accounts, tax_category_map, journal_entry_party_map, invoice_net_total_map + ) return columns, res @@ -22,11 +31,12 @@ def validate_filters(filters): frappe.throw(_("From Date must be before To Date")) -def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_party_map): +def get_result( + filters, tds_docs, tds_accounts, tax_category_map, journal_entry_party_map, invoice_net_total_map +): supplier_map = get_supplier_pan_map() tax_rate_map = get_tax_rate_map(filters) gle_map = get_gle_map(tds_docs) - print(journal_entry_party_map) out = [] for name, details in gle_map.items(): @@ -51,7 +61,16 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_ if entry.account in tds_accounts: tds_deducted += entry.credit - entry.debit - total_amount_credited += entry.credit + if invoice_net_total_map.get(name): + total_amount_credited = invoice_net_total_map.get(name) + else: + total_amount_credited += entry.credit + + ## Check if ldc is applied and show rate as per ldc + actual_rate = (tds_deducted / total_amount_credited) * 100 + + if flt(actual_rate) < flt(rate): + rate = actual_rate if tds_deducted: row = { @@ -180,9 +199,10 @@ def get_tds_docs(filters): purchase_invoices = [] payment_entries = [] journal_entries = [] - tax_category_map = {} - or_filters = {} - journal_entry_party_map = {} + tax_category_map = frappe._dict() + invoice_net_total_map = frappe._dict() + or_filters = frappe._dict() + journal_entry_party_map = frappe._dict() bank_accounts = frappe.get_all("Account", {"is_group": 0, "account_type": "Bank"}, pluck="name") tds_accounts = frappe.get_all( @@ -219,16 +239,22 @@ def get_tds_docs(filters): tds_documents.append(d.voucher_no) if purchase_invoices: - get_tax_category_map(purchase_invoices, "Purchase Invoice", tax_category_map) + get_doc_info(purchase_invoices, "Purchase Invoice", tax_category_map, invoice_net_total_map) if payment_entries: - get_tax_category_map(payment_entries, "Payment Entry", tax_category_map) + get_doc_info(payment_entries, "Payment Entry", tax_category_map) if journal_entries: journal_entry_party_map = get_journal_entry_party_map(journal_entries) - get_tax_category_map(journal_entries, "Journal Entry", tax_category_map) + get_doc_info(journal_entries, "Journal Entry", tax_category_map) - return tds_documents, tds_accounts, tax_category_map, journal_entry_party_map + return ( + tds_documents, + tds_accounts, + tax_category_map, + journal_entry_party_map, + invoice_net_total_map, + ) def get_journal_entry_party_map(journal_entries): @@ -245,17 +271,18 @@ def get_journal_entry_party_map(journal_entries): return journal_entry_party_map -def get_tax_category_map(vouchers, doctype, tax_category_map): - tax_category_map.update( - frappe._dict( - frappe.get_all( - doctype, - filters={"name": ("in", vouchers)}, - fields=["name", "tax_withholding_category"], - as_list=1, - ) - ) - ) +def get_doc_info(vouchers, doctype, tax_category_map, invoice_net_total_map=None): + if doctype == "Purchase Invoice": + fields = ["name", "tax_withholding_category", "base_tax_withholding_net_total"] + else: + fields = ["name", "tax_withholding_category"] + + entries = frappe.get_all(doctype, filters={"name": ("in", vouchers)}, fields=fields) + + for entry in entries: + tax_category_map.update({entry.name: entry.tax_withholding_category}) + if doctype == "Purchase Invoice": + invoice_net_total_map.update({entry.name: entry.base_tax_withholding_net_total}) def get_tax_rate_map(filters): diff --git a/erpnext/accounts/report/trial_balance/trial_balance.py b/erpnext/accounts/report/trial_balance/trial_balance.py index 6bd08ad837..3af01fde7d 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.py +++ b/erpnext/accounts/report/trial_balance/trial_balance.py @@ -38,7 +38,7 @@ def validate_filters(filters): if not filters.fiscal_year: frappe.throw(_("Fiscal Year {0} is required").format(filters.fiscal_year)) - fiscal_year = frappe.db.get_value( + fiscal_year = frappe.get_cached_value( "Fiscal Year", filters.fiscal_year, ["year_start_date", "year_end_date"], as_dict=True ) if not fiscal_year: @@ -172,11 +172,12 @@ def get_rootwise_opening_balances(filters, report_type): query_filters = { "company": filters.company, "from_date": filters.from_date, + "to_date": filters.to_date, "report_type": report_type, "year_start_date": filters.year_start_date, "project": filters.project, "finance_book": filters.finance_book, - "company_fb": frappe.db.get_value("Company", filters.company, "default_finance_book"), + "company_fb": frappe.get_cached_value("Company", filters.company, "default_finance_book"), } if accounting_dimensions: @@ -200,7 +201,7 @@ def get_rootwise_opening_balances(filters, report_type): where company=%(company)s {additional_conditions} - and (posting_date < %(from_date)s or ifnull(is_opening, 'No') = 'Yes') + and (posting_date < %(from_date)s or (ifnull(is_opening, 'No') = 'Yes' and posting_date <= %(to_date)s)) and account in (select name from `tabAccount` where report_type=%(report_type)s) and is_cancelled = 0 group by account""".format( diff --git a/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.py b/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.py index 5fcfdff6f1..ee223484d4 100644 --- a/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.py +++ b/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.py @@ -104,12 +104,17 @@ def get_opening_balances(filters): where company=%(company)s and is_cancelled=0 and ifnull(party_type, '') = %(party_type)s and ifnull(party, '') != '' - and (posting_date < %(from_date)s or ifnull(is_opening, 'No') = 'Yes') + and (posting_date < %(from_date)s or (ifnull(is_opening, 'No') = 'Yes' and posting_date <= %(to_date)s)) {account_filter} group by party""".format( account_filter=account_filter ), - {"company": filters.company, "from_date": filters.from_date, "party_type": filters.party_type}, + { + "company": filters.company, + "from_date": filters.from_date, + "to_date": filters.to_date, + "party_type": filters.party_type, + }, as_dict=True, ) diff --git a/erpnext/accounts/report/utils.py b/erpnext/accounts/report/utils.py index eed5836773..97cc1c4a13 100644 --- a/erpnext/accounts/report/utils.py +++ b/erpnext/accounts/report/utils.py @@ -28,7 +28,7 @@ def get_currency(filters): filters["presentation_currency"] if filters.get("presentation_currency") else company_currency ) - report_date = filters.get("to_date") + report_date = filters.get("to_date") or filters.get("period_end_date") if not report_date: fiscal_year_to_date = get_from_and_to_date(filters.get("to_fiscal_year"))["to_date"] @@ -101,11 +101,8 @@ def convert_to_presentation_currency(gl_entries, currency_info, company): account_currency = entry["account_currency"] if len(account_currencies) == 1 and account_currency == presentation_currency: - if debit_in_account_currency: - entry["debit"] = debit_in_account_currency - - if credit_in_account_currency: - entry["credit"] = credit_in_account_currency + entry["debit"] = debit_in_account_currency + entry["credit"] = credit_in_account_currency else: date = currency_info["report_date"] converted_debit_value = convert(debit, presentation_currency, company_currency, date) diff --git a/erpnext/accounts/test/test_utils.py b/erpnext/accounts/test/test_utils.py index 882cd694a3..3aca60eae5 100644 --- a/erpnext/accounts/test/test_utils.py +++ b/erpnext/accounts/test/test_utils.py @@ -3,11 +3,14 @@ import unittest import frappe from frappe.test_runner import make_test_objects +from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry +from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.accounts.party import get_party_shipping_address from erpnext.accounts.utils import ( get_future_stock_vouchers, get_voucherwise_gl_entries, sort_stock_vouchers_by_posting_date, + update_reference_in_payment_entry, ) from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt @@ -73,6 +76,47 @@ class TestUtils(unittest.TestCase): sorted_vouchers = sort_stock_vouchers_by_posting_date(list(reversed(vouchers))) self.assertEqual(sorted_vouchers, vouchers) + def test_update_reference_in_payment_entry(self): + item = make_item().name + + purchase_invoice = make_purchase_invoice( + item=item, supplier="_Test Supplier USD", currency="USD", conversion_rate=82.32 + ) + purchase_invoice.submit() + + payment_entry = get_payment_entry(purchase_invoice.doctype, purchase_invoice.name) + payment_entry.target_exchange_rate = 62.9 + payment_entry.paid_amount = 15725 + payment_entry.deductions = [] + payment_entry.insert() + + self.assertEqual(payment_entry.difference_amount, -4855.00) + payment_entry.references = [] + payment_entry.submit() + + payment_reconciliation = frappe.new_doc("Payment Reconciliation") + payment_reconciliation.company = payment_entry.company + payment_reconciliation.party_type = "Supplier" + payment_reconciliation.party = purchase_invoice.supplier + payment_reconciliation.receivable_payable_account = payment_entry.paid_to + payment_reconciliation.get_unreconciled_entries() + payment_reconciliation.allocate_entries( + { + "payments": [d.__dict__ for d in payment_reconciliation.payments], + "invoices": [d.__dict__ for d in payment_reconciliation.invoices], + } + ) + for d in payment_reconciliation.invoices: + # Reset invoice outstanding_amount because allocate_entries will zero this value out. + d.outstanding_amount = d.amount + for d in payment_reconciliation.allocation: + d.difference_account = "Exchange Gain/Loss - _TC" + payment_reconciliation.reconcile() + + payment_entry.load_from_db() + self.assertEqual(len(payment_entry.references), 1) + self.assertEqual(payment_entry.difference_amount, 0) + ADDRESS_RECORDS = [ { diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 9dafef74f4..2608c03ffe 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -86,7 +86,7 @@ def get_fiscal_years( ) ) - query = query.orderby(FY.year_start_date, Order.desc) + query = query.orderby(FY.year_start_date, order=Order.desc) fiscal_years = query.run(as_dict=True) frappe.cache().hset("fiscal_years", company, fiscal_years) @@ -439,8 +439,7 @@ def reconcile_against_document(args): # nosemgrep # cancel advance entry doc = frappe.get_doc(voucher_type, voucher_no) frappe.flags.ignore_party_validation = True - gl_map = doc.build_gl_map() - create_payment_ledger_entry(gl_map, cancel=1, adv_adj=1) + _delete_pl_entries(voucher_type, voucher_no) for entry in entries: check_if_advance_entry_modified(entry) @@ -452,18 +451,26 @@ def reconcile_against_document(args): # nosemgrep else: update_reference_in_payment_entry(entry, doc, do_not_save=True) + if doc.doctype == "Journal Entry": + try: + doc.validate_total_debit_and_credit() + except Exception as validation_exception: + raise frappe.ValidationError(_(f"Validation Error for {doc.name}")) from validation_exception + doc.save(ignore_permissions=True) # re-submit advance entry doc = frappe.get_doc(entry.voucher_type, entry.voucher_no) gl_map = doc.build_gl_map() - create_payment_ledger_entry(gl_map, cancel=0, adv_adj=1) + create_payment_ledger_entry(gl_map, update_outstanding="No", cancel=0, adv_adj=1) + + # Only update outstanding for newly linked vouchers + for entry in entries: + update_voucher_outstanding( + entry.against_voucher_type, entry.against_voucher, entry.account, entry.party_type, entry.party + ) frappe.flags.ignore_party_validation = False - if entry.voucher_type in ("Payment Entry", "Journal Entry"): - if hasattr(doc, "update_expense_claim"): - doc.update_expense_claim() - def check_if_advance_entry_modified(args): """ @@ -615,11 +622,6 @@ def update_reference_in_payment_entry(d, payment_entry, do_not_save=False): new_row.docstatus = 1 new_row.update(reference_details) - payment_entry.flags.ignore_validate_update_after_submit = True - payment_entry.setup_party_account_field() - payment_entry.set_missing_values() - payment_entry.set_amounts() - if d.difference_amount and d.difference_account: account_details = { "account": d.difference_account, @@ -631,6 +633,11 @@ def update_reference_in_payment_entry(d, payment_entry, do_not_save=False): payment_entry.set_gain_or_loss(account_details=account_details) + payment_entry.flags.ignore_validate_update_after_submit = True + payment_entry.setup_party_account_field() + payment_entry.set_missing_values() + payment_entry.set_amounts() + if not do_not_save: payment_entry.save(ignore_permissions=True) @@ -648,6 +655,16 @@ def unlink_ref_doc_from_payment_entries(ref_doc): (now(), frappe.session.user, ref_doc.doctype, ref_doc.name), ) + ple = qb.DocType("Payment Ledger Entry") + + qb.update(ple).set(ple.against_voucher_type, ple.voucher_type).set( + ple.against_voucher_no, ple.voucher_no + ).set(ple.modified, now()).set(ple.modified_by, frappe.session.user).where( + (ple.against_voucher_type == ref_doc.doctype) + & (ple.against_voucher_no == ref_doc.name) + & (ple.delinked == 0) + ).run() + if ref_doc.doctype in ("Sales Invoice", "Purchase Invoice"): ref_doc.set("advances", []) @@ -823,7 +840,14 @@ def get_held_invoices(party_type, party): def get_outstanding_invoices( - party_type, party, account, common_filter=None, min_outstanding=None, max_outstanding=None + party_type, + party, + account, + common_filter=None, + posting_date=None, + min_outstanding=None, + max_outstanding=None, + accounting_dimensions=None, ): ple = qb.DocType("Payment Ledger Entry") @@ -850,9 +874,11 @@ def get_outstanding_invoices( ple_query = QueryPaymentLedger() invoice_list = ple_query.get_voucher_outstandings( common_filter=common_filter, + posting_date=posting_date, min_outstanding=min_outstanding, max_outstanding=max_outstanding, get_invoices=True, + accounting_dimensions=accounting_dimensions or [], ) for d in invoice_list: @@ -962,7 +988,7 @@ def get_account_balances(accounts, company): def create_payment_gateway_account(gateway, payment_channel="Email"): from erpnext.setup.setup_wizard.operations.install_fixtures import create_bank_account - company = frappe.db.get_value("Global Defaults", None, "default_company") + company = frappe.get_cached_value("Global Defaults", "Global Defaults", "default_company") if not company: return @@ -1030,7 +1056,7 @@ def update_cost_center(docname, cost_center_name, cost_center_number, company, m frappe.db.set_value("Cost Center", docname, "cost_center_name", cost_center_name.strip()) - new_name = get_autoname_with_number(cost_center_number, cost_center_name, docname, company) + new_name = get_autoname_with_number(cost_center_number, cost_center_name, company) if docname != new_name: frappe.rename_doc("Cost Center", docname, new_name, force=1, merge=merge) return new_name @@ -1053,16 +1079,14 @@ def validate_field_number(doctype_name, docname, number_value, company, field_na ) -def get_autoname_with_number(number_value, doc_title, name, company): +def get_autoname_with_number(number_value, doc_title, company): """append title with prefix as number and suffix as company's abbreviation separated by '-'""" - if name: - name_split = name.split("-") - parts = [doc_title.strip(), name_split[len(name_split) - 1].strip()] - else: - abbr = frappe.get_cached_value("Company", company, ["abbr"], as_dict=True) - parts = [doc_title.strip(), abbr.abbr] + company_abbr = frappe.get_cached_value("Company", company, "abbr") + parts = [doc_title.strip(), company_abbr] + if cstr(number_value).strip(): parts.insert(0, cstr(number_value).strip()) + return " - ".join(parts) @@ -1135,10 +1159,10 @@ def repost_gle_for_stock_vouchers( if not existing_gle or not compare_existing_and_expected_gle( existing_gle, expected_gle, precision ): - _delete_gl_entries(voucher_type, voucher_no) + _delete_accounting_ledger_entries(voucher_type, voucher_no) voucher_obj.make_gl_entries(gl_entries=expected_gle, from_repost=True) else: - _delete_gl_entries(voucher_type, voucher_no) + _delete_accounting_ledger_entries(voucher_type, voucher_no) if not frappe.flags.in_test: frappe.db.commit() @@ -1150,12 +1174,26 @@ def repost_gle_for_stock_vouchers( ) +def _delete_pl_entries(voucher_type, voucher_no): + ple = qb.DocType("Payment Ledger Entry") + qb.from_(ple).delete().where( + (ple.voucher_type == voucher_type) & (ple.voucher_no == voucher_no) + ).run() + + def _delete_gl_entries(voucher_type, voucher_no): - frappe.db.sql( - """delete from `tabGL Entry` - where voucher_type=%s and voucher_no=%s""", - (voucher_type, voucher_no), - ) + gle = qb.DocType("GL Entry") + qb.from_(gle).delete().where( + (gle.voucher_type == voucher_type) & (gle.voucher_no == voucher_no) + ).run() + + +def _delete_accounting_ledger_entries(voucher_type, voucher_no): + """ + Remove entries from both General and Payment Ledger for specified Voucher + """ + _delete_gl_entries(voucher_type, voucher_no) + _delete_pl_entries(voucher_type, voucher_no) def sort_stock_vouchers_by_posting_date( @@ -1353,9 +1391,8 @@ def check_and_delete_linked_reports(report): frappe.delete_doc("Desktop Icon", icon) -def create_payment_ledger_entry( - gl_entries, cancel=0, adv_adj=0, update_outstanding="Yes", from_repost=0 -): +def get_payment_ledger_entries(gl_entries, cancel=0): + ple_map = [] if gl_entries: ple = None @@ -1395,43 +1432,57 @@ def create_payment_ledger_entry( dr_or_cr *= -1 dr_or_cr_account_currency *= -1 - ple = frappe.get_doc( - { - "doctype": "Payment Ledger Entry", - "posting_date": gle.posting_date, - "company": gle.company, - "account_type": account_type, - "account": gle.account, - "party_type": gle.party_type, - "party": gle.party, - "cost_center": gle.cost_center, - "finance_book": gle.finance_book, - "due_date": gle.due_date, - "voucher_type": gle.voucher_type, - "voucher_no": gle.voucher_no, - "against_voucher_type": gle.against_voucher_type - if gle.against_voucher_type - else gle.voucher_type, - "against_voucher_no": gle.against_voucher if gle.against_voucher else gle.voucher_no, - "account_currency": gle.account_currency, - "amount": dr_or_cr, - "amount_in_account_currency": dr_or_cr_account_currency, - "delinked": True if cancel else False, - } + ple = frappe._dict( + doctype="Payment Ledger Entry", + posting_date=gle.posting_date, + company=gle.company, + account_type=account_type, + account=gle.account, + party_type=gle.party_type, + party=gle.party, + cost_center=gle.cost_center, + finance_book=gle.finance_book, + due_date=gle.due_date, + voucher_type=gle.voucher_type, + voucher_no=gle.voucher_no, + against_voucher_type=gle.against_voucher_type + if gle.against_voucher_type + else gle.voucher_type, + against_voucher_no=gle.against_voucher if gle.against_voucher else gle.voucher_no, + account_currency=gle.account_currency, + amount=dr_or_cr, + amount_in_account_currency=dr_or_cr_account_currency, + delinked=True if cancel else False, + remarks=gle.remarks, ) dimensions_and_defaults = get_dimensions() if dimensions_and_defaults: for dimension in dimensions_and_defaults[0]: - ple.set(dimension.fieldname, gle.get(dimension.fieldname)) + ple[dimension.fieldname] = gle.get(dimension.fieldname) - if cancel: - delink_original_entry(ple) - ple.flags.ignore_permissions = 1 - ple.flags.adv_adj = adv_adj - ple.flags.from_repost = from_repost - ple.flags.update_outstanding = update_outstanding - ple.submit() + ple_map.append(ple) + return ple_map + + +def create_payment_ledger_entry( + gl_entries, cancel=0, adv_adj=0, update_outstanding="Yes", from_repost=0 +): + if gl_entries: + ple_map = get_payment_ledger_entries(gl_entries, cancel=cancel) + + for entry in ple_map: + + ple = frappe.get_doc(entry) + + if cancel: + delink_original_entry(ple) + + ple.flags.ignore_permissions = 1 + ple.flags.adv_adj = adv_adj + ple.flags.from_repost = from_repost + ple.flags.update_outstanding = update_outstanding + ple.submit() def update_voucher_outstanding(voucher_type, voucher_no, account, party_type, party): @@ -1451,14 +1502,22 @@ def update_voucher_outstanding(voucher_type, voucher_no, account, party_type, pa # on cancellation outstanding can be an empty list voucher_outstanding = ple_query.get_voucher_outstandings(vouchers, common_filter=common_filter) - if voucher_type in ["Sales Invoice", "Purchase Invoice", "Fees"] and voucher_outstanding: + if ( + voucher_type in ["Sales Invoice", "Purchase Invoice", "Fees"] + and party_type + and party + and voucher_outstanding + ): outstanding = voucher_outstanding[0] ref_doc = frappe.get_doc(voucher_type, voucher_no) # Didn't use db_set for optimisation purpose - ref_doc.outstanding_amount = outstanding["outstanding_in_account_currency"] + ref_doc.outstanding_amount = outstanding["outstanding_in_account_currency"] or 0.0 frappe.db.set_value( - voucher_type, voucher_no, "outstanding_amount", outstanding["outstanding_in_account_currency"] + voucher_type, + voucher_no, + "outstanding_amount", + outstanding["outstanding_in_account_currency"] or 0.0, ) ref_doc.set_status(update=True) @@ -1501,6 +1560,7 @@ class QueryPaymentLedger(object): # query filters self.vouchers = [] self.common_filter = [] + self.voucher_posting_date = [] self.min_outstanding = None self.max_outstanding = None @@ -1571,6 +1631,8 @@ class QueryPaymentLedger(object): .where(ple.delinked == 0) .where(Criterion.all(filter_on_voucher_no)) .where(Criterion.all(self.common_filter)) + .where(Criterion.all(self.dimensions_filter)) + .where(Criterion.all(self.voucher_posting_date)) .groupby(ple.voucher_type, ple.voucher_no, ple.party_type, ple.party) ) @@ -1652,10 +1714,12 @@ class QueryPaymentLedger(object): self, vouchers=None, common_filter=None, + posting_date=None, min_outstanding=None, max_outstanding=None, get_payments=False, get_invoices=False, + accounting_dimensions=None, ): """ Fetch voucher amount and outstanding amount from Payment Ledger using Database CTE @@ -1671,6 +1735,8 @@ class QueryPaymentLedger(object): self.reset() self.vouchers = vouchers self.common_filter = common_filter or [] + self.dimensions_filter = accounting_dimensions or [] + self.voucher_posting_date = posting_date or [] self.min_outstanding = min_outstanding self.max_outstanding = max_outstanding self.get_payments = get_payments diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index f414930d72..4951385136 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -76,7 +76,6 @@ frappe.ui.form.on('Asset', { refresh: function(frm) { frappe.ui.form.trigger("Asset", "is_existing_asset"); frm.toggle_display("next_depreciation_date", frm.doc.docstatus < 1); - frm.events.make_schedules_editable(frm); if (frm.doc.docstatus==1) { if (in_list(["Submitted", "Partially Depreciated", "Fully Depreciated"], frm.doc.status)) { @@ -136,6 +135,10 @@ frappe.ui.form.on('Asset', { }, __("Manage")); } + if (frm.doc.depr_entry_posting_status === "Failed") { + frm.trigger("set_depr_posting_failure_alert"); + } + frm.trigger("setup_chart"); } @@ -146,6 +149,19 @@ frappe.ui.form.on('Asset', { } }, + set_depr_posting_failure_alert: function (frm) { + const alert = ` +
+
+ + Failed to post depreciation entries + +
+
`; + + frm.dashboard.set_headline_alert(alert); + }, + toggle_reference_doc: function(frm) { if (frm.doc.purchase_receipt && frm.doc.purchase_invoice && frm.doc.docstatus === 1) { frm.set_df_property('purchase_invoice', 'read_only', 1); @@ -188,39 +204,67 @@ frappe.ui.form.on('Asset', { }) }, - setup_chart: function(frm) { - var x_intervals = [frm.doc.purchase_date]; - var asset_values = [frm.doc.gross_purchase_amount]; - var last_depreciation_date = frm.doc.purchase_date; - - if(frm.doc.opening_accumulated_depreciation) { - last_depreciation_date = frappe.datetime.add_months(frm.doc.next_depreciation_date, - -1*frm.doc.frequency_of_depreciation); - - x_intervals.push(last_depreciation_date); - asset_values.push(flt(frm.doc.gross_purchase_amount) - - flt(frm.doc.opening_accumulated_depreciation)); + setup_chart: async function(frm) { + if(frm.doc.finance_books.length > 1) { + return } - $.each(frm.doc.schedules || [], function(i, v) { - x_intervals.push(v.schedule_date); - var asset_value = flt(frm.doc.gross_purchase_amount) - flt(v.accumulated_depreciation_amount); - if(v.journal_entry) { - last_depreciation_date = v.schedule_date; - asset_values.push(asset_value); - } else { - if (in_list(["Scrapped", "Sold"], frm.doc.status)) { - asset_values.push(null); - } else { - asset_values.push(asset_value) - } + var x_intervals = [frappe.format(frm.doc.purchase_date, { fieldtype: 'Date' })]; + var asset_values = [frm.doc.gross_purchase_amount]; + + if(frm.doc.calculate_depreciation) { + if(frm.doc.opening_accumulated_depreciation) { + var depreciation_date = frappe.datetime.add_months( + frm.doc.finance_books[0].depreciation_start_date, + -1 * frm.doc.finance_books[0].frequency_of_depreciation + ); + x_intervals.push(frappe.format(depreciation_date, { fieldtype: 'Date' })); + asset_values.push(flt(frm.doc.gross_purchase_amount - frm.doc.opening_accumulated_depreciation, precision('gross_purchase_amount'))); } - }); + + let depr_schedule = (await frappe.call( + "erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule.get_depr_schedule", + { + asset_name: frm.doc.name, + status: frm.doc.docstatus ? "Active" : "Draft", + finance_book: frm.doc.finance_books[0].finance_book || null + } + )).message; + + $.each(depr_schedule || [], function(i, v) { + x_intervals.push(frappe.format(v.schedule_date, { fieldtype: 'Date' })); + var asset_value = flt(frm.doc.gross_purchase_amount - v.accumulated_depreciation_amount, precision('gross_purchase_amount')); + if(v.journal_entry) { + asset_values.push(asset_value); + } else { + if (in_list(["Scrapped", "Sold"], frm.doc.status)) { + asset_values.push(null); + } else { + asset_values.push(asset_value) + } + } + }); + } else { + if(frm.doc.opening_accumulated_depreciation) { + x_intervals.push(frappe.format(frm.doc.creation.split(" ")[0], { fieldtype: 'Date' })); + asset_values.push(flt(frm.doc.gross_purchase_amount - frm.doc.opening_accumulated_depreciation, precision('gross_purchase_amount'))); + } + + let depr_entries = (await frappe.call({ + method: "get_manual_depreciation_entries", + doc: frm.doc, + })).message; + + $.each(depr_entries || [], function(i, v) { + x_intervals.push(frappe.format(v.posting_date, { fieldtype: 'Date' })); + let last_asset_value = asset_values[asset_values.length - 1] + asset_values.push(flt(last_asset_value - v.value, precision('gross_purchase_amount'))); + }); + } if(in_list(["Scrapped", "Sold"], frm.doc.status)) { - x_intervals.push(frm.doc.disposal_date); + x_intervals.push(frappe.format(frm.doc.disposal_date, { fieldtype: 'Date' })); asset_values.push(0); - last_depreciation_date = frm.doc.disposal_date; } frm.dashboard.render_graph({ @@ -230,7 +274,7 @@ frappe.ui.form.on('Asset', { datasets: [{ color: 'green', values: asset_values, - formatted: asset_values.map(d => d.toFixed(2)) + formatted: asset_values.map(d => d?.toFixed(2)) }] }, type: 'line' @@ -239,8 +283,10 @@ frappe.ui.form.on('Asset', { item_code: function(frm) { - if(frm.doc.item_code) { + if(frm.doc.item_code && frm.doc.calculate_depreciation) { frm.trigger('set_finance_book'); + } else { + frm.set_value('finance_books', []); } }, @@ -264,21 +310,6 @@ frappe.ui.form.on('Asset', { // frm.toggle_reqd("next_depreciation_date", (!frm.doc.is_existing_asset && frm.doc.calculate_depreciation)); }, - opening_accumulated_depreciation: function(frm) { - erpnext.asset.set_accumulated_depreciation(frm); - }, - - make_schedules_editable: function(frm) { - if (frm.doc.finance_books) { - var is_editable = frm.doc.finance_books.filter(d => d.depreciation_method == "Manual").length > 0 - ? true : false; - - frm.toggle_enable("schedules", is_editable); - frm.fields_dict["schedules"].grid.toggle_enable("schedule_date", is_editable); - frm.fields_dict["schedules"].grid.toggle_enable("depreciation_amount", is_editable); - } - }, - make_sales_invoice: function(frm) { frappe.call({ args: { @@ -381,6 +412,11 @@ frappe.ui.form.on('Asset', { calculate_depreciation: function(frm) { frm.toggle_reqd("finance_books", frm.doc.calculate_depreciation); + if (frm.doc.item_code && frm.doc.calculate_depreciation ) { + frm.trigger("set_finance_book"); + } else { + frm.set_value("finance_books", []); + } }, gross_purchase_amount: function(frm) { @@ -425,7 +461,11 @@ frappe.ui.form.on('Asset', { set_values_from_purchase_doc: function(frm, doctype, purchase_doc) { frm.set_value('company', purchase_doc.company); - frm.set_value('purchase_date', purchase_doc.posting_date); + if (purchase_doc.bill_date) { + frm.set_value('purchase_date', purchase_doc.bill_date); + } else { + frm.set_value('purchase_date', purchase_doc.posting_date); + } const item = purchase_doc.items.find(item => item.item_code === frm.doc.item_code); if (!item) { doctype_field = frappe.scrub(doctype) @@ -465,7 +505,6 @@ frappe.ui.form.on('Asset Finance Book', { depreciation_method: function(frm, cdt, cdn) { const row = locals[cdt][cdn]; frm.events.set_depreciation_rate(frm, row); - frm.events.make_schedules_editable(frm); }, expected_value_after_useful_life: function(frm, cdt, cdn) { @@ -501,41 +540,6 @@ frappe.ui.form.on('Asset Finance Book', { } }); -frappe.ui.form.on('Depreciation Schedule', { - make_depreciation_entry: function(frm, cdt, cdn) { - var row = locals[cdt][cdn]; - if (!row.journal_entry) { - frappe.call({ - method: "erpnext.assets.doctype.asset.depreciation.make_depreciation_entry", - args: { - "asset_name": frm.doc.name, - "date": row.schedule_date - }, - callback: function(r) { - frappe.model.sync(r.message); - frm.refresh(); - } - }) - } - }, - - depreciation_amount: function(frm, cdt, cdn) { - erpnext.asset.set_accumulated_depreciation(frm); - } - -}) - -erpnext.asset.set_accumulated_depreciation = function(frm) { - if(frm.doc.depreciation_method != "Manual") return; - - var accumulated_depreciation = flt(frm.doc.opening_accumulated_depreciation); - $.each(frm.doc.schedules || [], function(i, row) { - accumulated_depreciation += flt(row.depreciation_amount); - frappe.model.set_value(row.doctype, row.name, - "accumulated_depreciation_amount", accumulated_depreciation); - }) -}; - erpnext.asset.scrap_asset = function(frm) { frappe.confirm(__("Do you really want to scrap this asset?"), function () { frappe.call({ diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json index 991df4eada..ea575fd71f 100644 --- a/erpnext/assets/doctype/asset/asset.json +++ b/erpnext/assets/doctype/asset/asset.json @@ -52,8 +52,6 @@ "column_break_24", "frequency_of_depreciation", "next_depreciation_date", - "section_break_14", - "schedules", "insurance_details", "policy_number", "insurer", @@ -70,6 +68,7 @@ "column_break_51", "purchase_receipt_amount", "default_finance_book", + "depr_entry_posting_status", "amended_from" ], "fields": [ @@ -307,19 +306,6 @@ "label": "Next Depreciation Date", "no_copy": 1 }, - { - "depends_on": "calculate_depreciation", - "fieldname": "section_break_14", - "fieldtype": "Section Break", - "label": "Depreciation Schedule" - }, - { - "fieldname": "schedules", - "fieldtype": "Table", - "label": "Depreciation Schedule", - "no_copy": 1, - "options": "Depreciation Schedule" - }, { "collapsible": 1, "fieldname": "insurance_details", @@ -388,7 +374,7 @@ "in_standard_filter": 1, "label": "Status", "no_copy": 1, - "options": "Draft\nSubmitted\nPartially Depreciated\nFully Depreciated\nSold\nScrapped\nIn Maintenance\nOut of Order\nIssue\nReceipt", + "options": "Draft\nSubmitted\nPartially Depreciated\nFully Depreciated\nSold\nScrapped\nIn Maintenance\nOut of Order\nIssue\nReceipt\nCapitalized\nDecapitalized", "read_only": 1 }, { @@ -488,6 +474,16 @@ "fieldtype": "Int", "label": "Asset Quantity", "read_only_depends_on": "eval:!doc.is_existing_asset" + }, + { + "fieldname": "depr_entry_posting_status", + "fieldtype": "Select", + "hidden": 1, + "label": "Depreciation Entry Posting Status", + "no_copy": 1, + "options": "\nSuccessful\nFailed", + "print_hide": 1, + "read_only": 1 } ], "idx": 72, @@ -502,15 +498,26 @@ { "group": "Repair", "link_doctype": "Asset Repair", - "link_fieldname": "asset_name" + "link_fieldname": "asset" }, { "group": "Value", "link_doctype": "Asset Value Adjustment", "link_fieldname": "asset" + }, + { + "group": "Depreciation", + "link_doctype": "Asset Depreciation Schedule", + "link_fieldname": "asset" + }, + { + "group": "Journal Entry", + "link_doctype": "Journal Entry", + "link_fieldname": "reference_name", + "table_fieldname": "accounts" } ], - "modified": "2022-07-20 10:15:12.887372", + "modified": "2023-02-02 00:03:11.706427", "modified_by": "Administrator", "module": "Assets", "name": "Asset", diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index a22d70dd63..e1d58a0264 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -8,14 +8,15 @@ import math import frappe from frappe import _ from frappe.utils import ( - add_days, add_months, cint, date_diff, flt, get_datetime, get_last_day, + get_link_to_form, getdate, + is_last_day_of_the_month, month_diff, nowdate, today, @@ -28,6 +29,15 @@ from erpnext.assets.doctype.asset.depreciation import ( get_disposal_account_and_cost_center, ) from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account +from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import ( + cancel_asset_depr_schedules, + convert_draft_asset_depr_schedules_into_active, + get_asset_depr_schedule_doc, + get_depr_schedule, + make_draft_asset_depr_schedules, + make_draft_asset_depr_schedules_if_not_present, + update_draft_asset_depr_schedules, +) from erpnext.controllers.accounts_controller import AccountsController @@ -40,9 +50,9 @@ class Asset(AccountsController): self.set_missing_values() if not self.split_from: self.prepare_depreciation_data() + update_draft_asset_depr_schedules(self) self.validate_gross_and_purchase_amount() - if self.get("schedules"): - self.validate_expected_value_after_useful_life() + self.validate_expected_value_after_useful_life() self.status = self.get_status() @@ -52,16 +62,24 @@ class Asset(AccountsController): self.make_asset_movement() if not self.booked_fixed_asset and self.validate_make_gl_entry(): self.make_gl_entries() + if not self.split_from: + make_draft_asset_depr_schedules_if_not_present(self) + convert_draft_asset_depr_schedules_into_active(self) def on_cancel(self): self.validate_cancellation() self.cancel_movement_entries() self.delete_depreciation_entries() + cancel_asset_depr_schedules(self) self.set_status() self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry") make_reverse_gl_entries(voucher_type="Asset", voucher_no=self.name) self.db_set("booked_fixed_asset", 0) + def after_insert(self): + if not self.split_from: + make_draft_asset_depr_schedules(self) + def validate_asset_and_reference(self): if self.purchase_invoice or self.purchase_receipt: reference_doc = "Purchase Invoice" if self.purchase_invoice else "Purchase Receipt" @@ -79,12 +97,10 @@ class Asset(AccountsController): _("Purchase Invoice cannot be made against an existing asset {0}").format(self.name) ) - def prepare_depreciation_data(self, date_of_sale=None, date_of_return=None): + def prepare_depreciation_data(self): if self.calculate_depreciation: self.value_after_depreciation = 0 self.set_depreciation_rate() - self.make_depreciation_schedule(date_of_sale) - self.set_accumulated_depreciation(date_of_sale, date_of_return) else: self.finance_books = [] self.value_after_depreciation = flt(self.gross_purchase_amount) - flt( @@ -223,211 +239,6 @@ class Asset(AccountsController): self.get_depreciation_rate(d, on_validate=True), d.precision("rate_of_depreciation") ) - def make_depreciation_schedule(self, date_of_sale): - if "Manual" not in [d.depreciation_method for d in self.finance_books] and not self.get( - "schedules" - ): - self.schedules = [] - - if not self.available_for_use_date: - return - - start = self.clear_depreciation_schedule() - - for finance_book in self.get("finance_books"): - self._make_depreciation_schedule(finance_book, start, date_of_sale) - - def _make_depreciation_schedule(self, finance_book, start, date_of_sale): - self.validate_asset_finance_books(finance_book) - - value_after_depreciation = self._get_value_after_depreciation(finance_book) - finance_book.value_after_depreciation = value_after_depreciation - - 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(finance_book) - if has_pro_rata: - number_of_pending_depreciations += 1 - - skip_row = False - should_get_last_day = is_last_day_of_the_month(finance_book.depreciation_start_date) - - 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, finance_book) - - if not has_pro_rata or n < cint(number_of_pending_depreciations) - 1: - schedule_date = add_months( - finance_book.depreciation_start_date, n * cint(finance_book.frequency_of_depreciation) - ) - - if should_get_last_day: - schedule_date = get_last_day(schedule_date) - - # 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, -finance_book.frequency_of_depreciation + 1) - - # if asset is being sold - if date_of_sale: - 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._add_depreciation_row( - date_of_sale, - depreciation_amount, - finance_book.depreciation_method, - finance_book.finance_book, - finance_book.idx, - ) - - break - - # For first row - if has_pro_rata and not self.opening_accumulated_depreciation and n == 0: - from_date = add_days( - self.available_for_use_date, -1 - ) # needed to calc depr amount for available_for_use_date too - depreciation_amount, days, months = self.get_pro_rata_amt( - finance_book, depreciation_amount, from_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(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 + 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( - finance_book, depreciation_amount, schedule_date, self.to_date - ) - - depreciation_amount = self.get_adjusted_depreciation_amount( - depreciation_amount_without_pro_rata, depreciation_amount, finance_book.finance_book - ) - - monthly_schedule_date = add_months(schedule_date, 1) - schedule_date = add_days(schedule_date, days) - last_schedule_date = schedule_date - - if not depreciation_amount: - continue - value_after_depreciation -= flt(depreciation_amount, self.precision("gross_purchase_amount")) - - # Adjust depreciation amount in the last period based on the 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: - self._add_depreciation_row( - schedule_date, - depreciation_amount, - finance_book.depreciation_method, - finance_book.finance_book, - finance_book.idx, - ) - - def _add_depreciation_row( - self, schedule_date, depreciation_amount, depreciation_method, finance_book, finance_book_id - ): - self.append( - "schedules", - { - "schedule_date": schedule_date, - "depreciation_amount": depreciation_amount, - "depreciation_method": depreciation_method, - "finance_book": finance_book, - "finance_book_id": finance_book_id, - }, - ) - - def _get_value_after_depreciation(self, finance_book): - # value_after_depreciation - current Asset value - 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 - ) - - return value_after_depreciation - - # 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 = [] - 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): - if not self.get("schedules"): - return self.available_for_use_date - - if len(self.finance_books) == 1: - return self.schedules[-1].schedule_date - - from_date = "" - for schedule in self.get("schedules"): - if schedule.finance_book == finance_book: - from_date = schedule.schedule_date - - if from_date: - return from_date - - # since depr for available_for_use_date is not yet booked - return add_days(self.available_for_use_date, -1) - # 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 @@ -512,83 +323,15 @@ class Asset(AccountsController): ).format(row.idx) ) - # 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 - ): - 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 - ) - - return depreciation_amount_for_last_row - - def get_depreciation_amount_for_first_row(self, finance_book): - if self.has_only_one_finance_book(): - return self.schedules[0].depreciation_amount - else: - for schedule in self.schedules: - if schedule.finance_book == finance_book: - return schedule.depreciation_amount - - def has_only_one_finance_book(self): - if len(self.finance_books) == 1: - return True - - def set_accumulated_depreciation( - self, date_of_sale=None, date_of_return=None, ignore_booked_entry=False - ): - straight_line_idx = [ - d.idx for d in self.get("schedules") if d.depreciation_method == "Straight Line" - ] - finance_books = [] - - for i, d in enumerate(self.get("schedules")): - if ignore_booked_entry and d.journal_entry: - continue - - if int(d.finance_book_id) not in finance_books: - accumulated_depreciation = flt(self.opening_accumulated_depreciation) - value_after_depreciation = flt(self.get_value_after_depreciation(d.finance_book_id)) - finance_books.append(int(d.finance_book_id)) - - depreciation_amount = flt(d.depreciation_amount, d.precision("depreciation_amount")) - value_after_depreciation -= flt(depreciation_amount) - - # for the last row, if depreciation method = Straight Line - if ( - straight_line_idx - and i == max(straight_line_idx) - 1 - and not date_of_sale - and not date_of_return - ): - book = self.get("finance_books")[cint(d.finance_book_id) - 1] - depreciation_amount += flt( - value_after_depreciation - flt(book.expected_value_after_useful_life), - d.precision("depreciation_amount"), - ) - - d.depreciation_amount = depreciation_amount - accumulated_depreciation += d.depreciation_amount - d.accumulated_depreciation_amount = flt( - accumulated_depreciation, d.precision("accumulated_depreciation_amount") - ) - - def get_value_after_depreciation(self, idx): - return flt(self.get("finance_books")[cint(idx) - 1].value_after_depreciation) - def validate_expected_value_after_useful_life(self): for row in self.get("finance_books"): + depr_schedule = get_depr_schedule(self.name, "Draft", row.finance_book) + + if not depr_schedule: + continue + accumulated_depreciation_after_full_schedule = [ - d.accumulated_depreciation_amount - for d in self.get("schedules") - if cint(d.finance_book_id) == row.idx + d.accumulated_depreciation_amount for d in depr_schedule ] if accumulated_depreciation_after_full_schedule: @@ -637,15 +380,23 @@ class Asset(AccountsController): movement.cancel() def delete_depreciation_entries(self): - for d in self.get("schedules"): - if d.journal_entry: - frappe.get_doc("Journal Entry", d.journal_entry).cancel() - d.db_set("journal_entry", None) + if self.calculate_depreciation: + for row in self.get("finance_books"): + depr_schedule = get_depr_schedule(self.name, "Active", row.finance_book) - self.db_set( - "value_after_depreciation", - (flt(self.gross_purchase_amount) - flt(self.opening_accumulated_depreciation)), - ) + for d in depr_schedule or []: + if d.journal_entry: + frappe.get_doc("Journal Entry", d.journal_entry).cancel() + else: + depr_entries = self.get_manual_depreciation_entries() + + for depr_entry in depr_entries or []: + frappe.get_doc("Journal Entry", depr_entry.name).cancel() + + self.db_set( + "value_after_depreciation", + (flt(self.gross_purchase_amount) - flt(self.opening_accumulated_depreciation)), + ) def set_status(self, status=None): """Get and update status""" @@ -662,11 +413,14 @@ class Asset(AccountsController): if self.journal_entry_for_scrap: status = "Scrapped" - elif self.finance_books: - idx = self.get_default_finance_book_idx() or 0 + else: + expected_value_after_useful_life = 0 + value_after_depreciation = self.value_after_depreciation - expected_value_after_useful_life = self.finance_books[idx].expected_value_after_useful_life - value_after_depreciation = self.finance_books[idx].value_after_depreciation + if self.calculate_depreciation: + idx = self.get_default_finance_book_idx() or 0 + expected_value_after_useful_life = self.finance_books[idx].expected_value_after_useful_life + value_after_depreciation = self.finance_books[idx].value_after_depreciation if flt(value_after_depreciation) <= expected_value_after_useful_life: status = "Fully Depreciated" @@ -676,6 +430,19 @@ class Asset(AccountsController): status = "Cancelled" return status + def get_value_after_depreciation(self, finance_book=None): + if not self.calculate_depreciation: + return flt(self.value_after_depreciation, self.precision("gross_purchase_amount")) + + if not finance_book: + return flt( + self.get("finance_books")[0].value_after_depreciation, self.precision("gross_purchase_amount") + ) + + for row in self.get("finance_books"): + if finance_book == row.finance_book: + return flt(row.value_after_depreciation, self.precision("gross_purchase_amount")) + def get_default_finance_book_idx(self): if not self.get("default_finance_book") and self.company: self.default_finance_book = erpnext.get_default_finance_book(self.company) @@ -685,6 +452,44 @@ class Asset(AccountsController): if d.finance_book == self.default_finance_book: return cint(d.idx) - 1 + @frappe.whitelist() + def get_manual_depreciation_entries(self): + (_, _, depreciation_expense_account) = get_depreciation_accounts(self) + + gle = frappe.qb.DocType("GL Entry") + + records = ( + frappe.qb.from_(gle) + .select(gle.voucher_no.as_("name"), gle.debit.as_("value"), gle.posting_date) + .where(gle.against_voucher == self.name) + .where(gle.account == depreciation_expense_account) + .where(gle.debit != 0) + .where(gle.is_cancelled == 0) + .orderby(gle.posting_date) + .orderby(gle.creation) + ).run(as_dict=True) + + return records + + @erpnext.allow_regional + def get_depreciation_amount(self, depreciable_value, fb_row): + if fb_row.depreciation_method in ("Straight Line", "Manual"): + # if the Depreciation Schedule is being prepared for the first time + if not self.flags.increase_in_asset_life: + depreciation_amount = ( + flt(self.gross_purchase_amount) - flt(fb_row.expected_value_after_useful_life) + ) / flt(fb_row.total_number_of_depreciations) + + # if the Depreciation Schedule is being modified after Asset Repair + else: + depreciation_amount = ( + flt(fb_row.value_after_depreciation) - flt(fb_row.expected_value_after_useful_life) + ) / (date_diff(self.to_date, self.available_for_use_date) / 365) + else: + depreciation_amount = flt(depreciable_value * (flt(fb_row.rate_of_depreciation) / 100)) + + return depreciation_amount + def validate_make_gl_entry(self): purchase_document = self.get_purchase_document() if not purchase_document: @@ -828,7 +633,9 @@ class Asset(AccountsController): def update_maintenance_status(): - assets = frappe.get_all("Asset", filters={"docstatus": 1, "maintenance_required": 1}) + assets = frappe.get_all( + "Asset", filters={"docstatus": 1, "maintenance_required": 1, "disposal_date": ("is", "not set")} + ) for asset in assets: asset = frappe.get_doc("Asset", asset.name) @@ -843,7 +650,6 @@ def update_maintenance_status(): def make_post_gl_entry(): - asset_categories = frappe.db.get_all("Asset Category", fields=["name", "enable_cwip_accounting"]) for asset_category in asset_categories: @@ -996,7 +802,7 @@ def make_journal_entry(asset_name): depreciation_expense_account, ) = get_depreciation_accounts(asset) - depreciation_cost_center, depreciation_series = frappe.db.get_value( + depreciation_cost_center, depreciation_series = frappe.get_cached_value( "Company", asset.company, ["depreciation_cost_center", "series_for_depreciation_entry"] ) depreciation_cost_center = asset.cost_center or depreciation_cost_center @@ -1061,6 +867,13 @@ def is_cwip_accounting_enabled(asset_category): return cint(frappe.db.get_value("Asset Category", asset_category, "enable_cwip_accounting")) +@frappe.whitelist() +def get_asset_value_after_depreciation(asset_name, finance_book=None): + asset = frappe.get_doc("Asset", asset_name) + + return asset.get_value_after_depreciation(finance_book) + + def get_total_days(date, frequency): period_start_date = add_months(date, cint(frequency) * -1) @@ -1070,32 +883,6 @@ def get_total_days(date, frequency): return date_diff(date, period_start_date) -def is_last_day_of_the_month(date): - last_day_of_the_month = get_last_day(date) - - return getdate(last_day_of_the_month) == getdate(date) - - -@erpnext.allow_regional -def get_depreciation_amount(asset, depreciable_value, row): - 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(row.expected_value_after_useful_life) - ) / flt(row.total_number_of_depreciations) - - # if the Depreciation Schedule is being modified after Asset Repair - else: - depreciation_amount = ( - flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life) - ) / (date_diff(asset.to_date, asset.available_for_use_date) / 365) - else: - depreciation_amount = flt(depreciable_value * (flt(row.rate_of_depreciation) / 100)) - - return depreciation_amount - - @frappe.whitelist() def split_asset(asset_name, split_qty): asset = frappe.get_doc("Asset", asset_name) @@ -1107,12 +894,12 @@ def split_asset(asset_name, split_qty): remaining_qty = asset.asset_quantity - split_qty new_asset = create_new_asset_after_split(asset, split_qty) - update_existing_asset(asset, remaining_qty) + update_existing_asset(asset, remaining_qty, new_asset.name) return new_asset -def update_existing_asset(asset, remaining_qty): +def update_existing_asset(asset, remaining_qty, new_asset_name): remaining_gross_purchase_amount = flt( (asset.gross_purchase_amount * remaining_qty) / asset.asset_quantity ) @@ -1130,34 +917,49 @@ def update_existing_asset(asset, remaining_qty): }, ) - for finance_book in asset.get("finance_books"): + for row in asset.get("finance_books"): value_after_depreciation = flt( - (finance_book.value_after_depreciation * remaining_qty) / asset.asset_quantity + (row.value_after_depreciation * remaining_qty) / asset.asset_quantity ) expected_value_after_useful_life = flt( - (finance_book.expected_value_after_useful_life * remaining_qty) / asset.asset_quantity + (row.expected_value_after_useful_life * remaining_qty) / asset.asset_quantity ) frappe.db.set_value( - "Asset Finance Book", finance_book.name, "value_after_depreciation", value_after_depreciation + "Asset Finance Book", row.name, "value_after_depreciation", value_after_depreciation ) frappe.db.set_value( "Asset Finance Book", - finance_book.name, + row.name, "expected_value_after_useful_life", expected_value_after_useful_life, ) - accumulated_depreciation = 0 + current_asset_depr_schedule_doc = get_asset_depr_schedule_doc( + asset.name, "Active", row.finance_book + ) + new_asset_depr_schedule_doc = frappe.copy_doc(current_asset_depr_schedule_doc) - for term in asset.get("schedules"): - depreciation_amount = flt((term.depreciation_amount * remaining_qty) / asset.asset_quantity) - frappe.db.set_value( - "Depreciation Schedule", term.name, "depreciation_amount", depreciation_amount - ) - accumulated_depreciation += depreciation_amount - frappe.db.set_value( - "Depreciation Schedule", term.name, "accumulated_depreciation_amount", accumulated_depreciation + new_asset_depr_schedule_doc.set_draft_asset_depr_schedule_details(asset, row) + + accumulated_depreciation = 0 + + for term in new_asset_depr_schedule_doc.get("depreciation_schedule"): + depreciation_amount = flt((term.depreciation_amount * remaining_qty) / asset.asset_quantity) + term.depreciation_amount = depreciation_amount + accumulated_depreciation += depreciation_amount + term.accumulated_depreciation_amount = accumulated_depreciation + + notes = _( + "This schedule was created when Asset {0} was updated after being split into new Asset {1}." + ).format( + get_link_to_form(asset.doctype, asset.name), get_link_to_form(asset.doctype, new_asset_name) ) + new_asset_depr_schedule_doc.notes = notes + + current_asset_depr_schedule_doc.flags.should_not_cancel_depreciation_entries = True + current_asset_depr_schedule_doc.cancel() + + new_asset_depr_schedule_doc.submit() def create_new_asset_after_split(asset, split_qty): @@ -1171,31 +973,49 @@ def create_new_asset_after_split(asset, split_qty): new_asset.opening_accumulated_depreciation = opening_accumulated_depreciation new_asset.asset_quantity = split_qty new_asset.split_from = asset.name - accumulated_depreciation = 0 - for finance_book in new_asset.get("finance_books"): - finance_book.value_after_depreciation = flt( - (finance_book.value_after_depreciation * split_qty) / asset.asset_quantity + for row in new_asset.get("finance_books"): + row.value_after_depreciation = flt( + (row.value_after_depreciation * split_qty) / asset.asset_quantity ) - finance_book.expected_value_after_useful_life = flt( - (finance_book.expected_value_after_useful_life * split_qty) / asset.asset_quantity + row.expected_value_after_useful_life = flt( + (row.expected_value_after_useful_life * split_qty) / asset.asset_quantity ) - for term in new_asset.get("schedules"): - depreciation_amount = flt((term.depreciation_amount * split_qty) / asset.asset_quantity) - term.depreciation_amount = depreciation_amount - accumulated_depreciation += depreciation_amount - term.accumulated_depreciation_amount = accumulated_depreciation - new_asset.submit() new_asset.set_status() - for term in new_asset.get("schedules"): - # Update references in JV - if term.journal_entry: - add_reference_in_jv_on_split( - term.journal_entry, new_asset.name, asset.name, term.depreciation_amount - ) + for row in new_asset.get("finance_books"): + current_asset_depr_schedule_doc = get_asset_depr_schedule_doc( + asset.name, "Active", row.finance_book + ) + new_asset_depr_schedule_doc = frappe.copy_doc(current_asset_depr_schedule_doc) + + new_asset_depr_schedule_doc.set_draft_asset_depr_schedule_details(new_asset, row) + + accumulated_depreciation = 0 + + for term in new_asset_depr_schedule_doc.get("depreciation_schedule"): + depreciation_amount = flt((term.depreciation_amount * split_qty) / asset.asset_quantity) + term.depreciation_amount = depreciation_amount + accumulated_depreciation += depreciation_amount + term.accumulated_depreciation_amount = accumulated_depreciation + + notes = _("This schedule was created when new Asset {0} was split from Asset {1}.").format( + get_link_to_form(new_asset.doctype, new_asset.name), get_link_to_form(asset.doctype, asset.name) + ) + new_asset_depr_schedule_doc.notes = notes + + new_asset_depr_schedule_doc.submit() + + for row in new_asset.get("finance_books"): + depr_schedule = get_depr_schedule(new_asset.name, "Active", row.finance_book) + for term in depr_schedule: + # Update references in JV + if term.journal_entry: + add_reference_in_jv_on_split( + term.journal_entry, new_asset.name, asset.name, term.depreciation_amount + ) return new_asset diff --git a/erpnext/assets/doctype/asset/asset_list.js b/erpnext/assets/doctype/asset/asset_list.js index 4302cb2c51..3d00eb74aa 100644 --- a/erpnext/assets/doctype/asset/asset_list.js +++ b/erpnext/assets/doctype/asset/asset_list.js @@ -10,6 +10,9 @@ frappe.listview_settings['Asset'] = { } else if (doc.status === "Sold") { return [__("Sold"), "green", "status,=,Sold"]; + } else if (["Capitalized", "Decapitalized"].includes(doc.status)) { + return [__(doc.status), "grey", "status,=," + doc.status]; + } else if (doc.status === "Scrapped") { return [__("Scrapped"), "grey", "status,=,Scrapped"]; diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index 3f7e945994..fb6e174fba 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -4,11 +4,29 @@ import frappe from frappe import _ -from frappe.utils import cint, flt, getdate, today +from frappe.utils import ( + add_months, + cint, + flt, + get_last_day, + get_link_to_form, + getdate, + is_last_day_of_the_month, + nowdate, + today, +) +from frappe.utils.user import get_users_with_role from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_checks_for_pl_and_bs_accounts, ) +from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry +from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import ( + get_asset_depr_schedule_doc, + get_asset_depr_schedule_name, + get_temp_asset_depr_schedule_doc, + make_new_active_asset_depr_schedules_and_cancel_current_ones, +) def post_depreciation_entries(date=None): @@ -20,29 +38,58 @@ def post_depreciation_entries(date=None): if not date: date = today() - for asset in get_depreciable_assets(date): - make_depreciation_entry(asset, date) - frappe.db.commit() + + failed_asset_names = [] + + for asset_name in get_depreciable_assets(date): + asset_doc = frappe.get_doc("Asset", asset_name) + + try: + make_depreciation_entry_for_all_asset_depr_schedules(asset_doc, date) + frappe.db.commit() + except Exception as e: + frappe.db.rollback() + failed_asset_names.append(asset_name) + + if failed_asset_names: + set_depr_entry_posting_status_for_failed_assets(failed_asset_names) + notify_depr_entry_posting_error(failed_asset_names) + + frappe.db.commit() def get_depreciable_assets(date): return frappe.db.sql_list( """select distinct a.name - from tabAsset a, `tabDepreciation Schedule` ds - where a.name = ds.parent and a.docstatus=1 and ds.schedule_date<=%s and a.calculate_depreciation = 1 + from tabAsset a, `tabAsset Depreciation Schedule` ads, `tabDepreciation Schedule` ds + where a.name = ads.asset and ads.name = ds.parent and a.docstatus=1 and ads.docstatus=1 and a.status in ('Submitted', 'Partially Depreciated') + and a.calculate_depreciation = 1 + and ds.schedule_date<=%s and ifnull(ds.journal_entry, '')=''""", date, ) +def make_depreciation_entry_for_all_asset_depr_schedules(asset_doc, date=None): + for row in asset_doc.get("finance_books"): + asset_depr_schedule_name = get_asset_depr_schedule_name( + asset_doc.name, "Active", row.finance_book + ) + make_depreciation_entry(asset_depr_schedule_name, date) + + @frappe.whitelist() -def make_depreciation_entry(asset_name, date=None): +def make_depreciation_entry(asset_depr_schedule_name, date=None): frappe.has_permission("Journal Entry", throw=True) if not date: date = today() + asset_depr_schedule_doc = frappe.get_doc("Asset Depreciation Schedule", asset_depr_schedule_name) + + asset_name = asset_depr_schedule_doc.asset + asset = frappe.get_doc("Asset", asset_name) ( fixed_asset_account, @@ -58,14 +105,14 @@ def make_depreciation_entry(asset_name, date=None): accounting_dimensions = get_checks_for_pl_and_bs_accounts() - for d in asset.get("schedules"): + for d in asset_depr_schedule_doc.get("depreciation_schedule"): if not d.journal_entry and getdate(d.schedule_date) <= getdate(date): je = frappe.new_doc("Journal Entry") je.voucher_type = "Depreciation Entry" je.naming_series = depreciation_series je.posting_date = d.schedule_date je.company = asset.company - je.finance_book = d.finance_book + je.finance_book = asset_depr_schedule_doc.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( @@ -116,14 +163,16 @@ def make_depreciation_entry(asset_name, date=None): d.db_set("journal_entry", je.name) - idx = cint(d.finance_book_id) - finance_books = asset.get("finance_books")[idx - 1] - finance_books.value_after_depreciation -= d.depreciation_amount - finance_books.db_update() + idx = cint(asset_depr_schedule_doc.finance_book_id) + row = asset.get("finance_books")[idx - 1] + row.value_after_depreciation -= d.depreciation_amount + row.db_update() + + asset.db_set("depr_entry_posting_status", "Successful") asset.set_status() - return asset + return asset_depr_schedule_doc def get_depreciation_accounts(asset): @@ -184,17 +233,62 @@ def get_credit_and_debit_accounts(accumulated_depreciation_account, depreciation return credit_account, debit_account +def set_depr_entry_posting_status_for_failed_assets(failed_asset_names): + for asset_name in failed_asset_names: + frappe.db.set_value("Asset", asset_name, "depr_entry_posting_status", "Failed") + + +def notify_depr_entry_posting_error(failed_asset_names): + recipients = get_users_with_role("Accounts Manager") + + if not recipients: + recipients = get_users_with_role("System Manager") + + subject = _("Error while posting depreciation entries") + + asset_links = get_comma_separated_asset_links(failed_asset_names) + + message = ( + _("Hi,") + + "
" + + _("The following assets have failed to post depreciation entries: {0}").format(asset_links) + + "." + ) + + frappe.sendmail(recipients=recipients, subject=subject, message=message) + + +def get_comma_separated_asset_links(asset_names): + asset_links = [] + + for asset_name in asset_names: + asset_links.append(get_link_to_form("Asset", asset_name)) + + asset_links = ", ".join(asset_links) + + return asset_links + + @frappe.whitelist() def scrap_asset(asset_name): asset = frappe.get_doc("Asset", asset_name) if asset.docstatus != 1: frappe.throw(_("Asset {0} must be submitted").format(asset.name)) - elif asset.status in ("Cancelled", "Sold", "Scrapped"): + elif asset.status in ("Cancelled", "Sold", "Scrapped", "Capitalized", "Decapitalized"): frappe.throw( _("Asset {0} cannot be scrapped, as it is already {1}").format(asset.name, asset.status) ) + date = today() + + notes = _("This schedule was created when Asset {0} was scrapped.").format( + get_link_to_form(asset.doctype, asset.name) + ) + + depreciate_asset(asset, date, notes) + asset.reload() + depreciation_series = frappe.get_cached_value( "Company", asset.company, "series_for_depreciation_entry" ) @@ -202,7 +296,7 @@ def scrap_asset(asset_name): je = frappe.new_doc("Journal Entry") je.voucher_type = "Journal Entry" je.naming_series = depreciation_series - je.posting_date = today() + je.posting_date = date je.company = asset.company je.remark = "Scrap Entry for asset {0}".format(asset_name) @@ -213,7 +307,7 @@ def scrap_asset(asset_name): je.flags.ignore_permissions = True je.submit() - frappe.db.set_value("Asset", asset_name, "disposal_date", today()) + frappe.db.set_value("Asset", asset_name, "disposal_date", date) frappe.db.set_value("Asset", asset_name, "journal_entry_for_scrap", je.name) asset.set_status("Scrapped") @@ -224,8 +318,16 @@ def scrap_asset(asset_name): def restore_asset(asset_name): asset = frappe.get_doc("Asset", asset_name) + reverse_depreciation_entry_made_after_disposal(asset, asset.disposal_date) + je = asset.journal_entry_for_scrap + notes = _("This schedule was created when Asset {0} was restored.").format( + get_link_to_form(asset.doctype, asset.name) + ) + + reset_depreciation_schedule(asset, asset.disposal_date, notes) + asset.db_set("disposal_date", None) asset.db_set("journal_entry_for_scrap", None) @@ -234,7 +336,99 @@ def restore_asset(asset_name): asset.set_status() -def get_gl_entries_on_asset_regain(asset, selling_amount=0, finance_book=None): +def depreciate_asset(asset_doc, date, notes): + asset_doc.flags.ignore_validate_update_after_submit = True + + make_new_active_asset_depr_schedules_and_cancel_current_ones( + asset_doc, notes, date_of_disposal=date + ) + + asset_doc.save() + + make_depreciation_entry_for_all_asset_depr_schedules(asset_doc, date) + + +def reset_depreciation_schedule(asset_doc, date, notes): + asset_doc.flags.ignore_validate_update_after_submit = True + + make_new_active_asset_depr_schedules_and_cancel_current_ones( + asset_doc, notes, date_of_return=date + ) + + modify_depreciation_schedule_for_asset_repairs(asset_doc, notes) + + asset_doc.save() + + +def modify_depreciation_schedule_for_asset_repairs(asset, notes): + asset_repairs = frappe.get_all( + "Asset Repair", filters={"asset": asset.name}, fields=["name", "increase_in_asset_life"] + ) + + for repair in asset_repairs: + if repair.increase_in_asset_life: + asset_repair = frappe.get_doc("Asset Repair", repair.name) + asset_repair.modify_depreciation_schedule() + make_new_active_asset_depr_schedules_and_cancel_current_ones(asset, notes) + + +def reverse_depreciation_entry_made_after_disposal(asset, date): + for row in asset.get("finance_books"): + asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset.name, "Active", row.finance_book) + + for schedule_idx, schedule in enumerate(asset_depr_schedule_doc.get("depreciation_schedule")): + if schedule.schedule_date == date: + if not disposal_was_made_on_original_schedule_date( + schedule_idx, row, date + ) or disposal_happens_in_the_future(date): + + reverse_journal_entry = make_reverse_journal_entry(schedule.journal_entry) + reverse_journal_entry.posting_date = nowdate() + frappe.flags.is_reverse_depr_entry = True + reverse_journal_entry.submit() + + frappe.flags.is_reverse_depr_entry = False + asset_depr_schedule_doc.flags.ignore_validate_update_after_submit = True + asset.flags.ignore_validate_update_after_submit = True + schedule.journal_entry = None + depreciation_amount = get_depreciation_amount_in_je(reverse_journal_entry) + row.value_after_depreciation += depreciation_amount + asset_depr_schedule_doc.save() + asset.save() + + +def get_depreciation_amount_in_je(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 + + +# if the invoice had been posted on the date the depreciation was initially supposed to happen, the depreciation shouldn't be undone +def disposal_was_made_on_original_schedule_date(schedule_idx, row, posting_date_of_disposal): + orginal_schedule_date = add_months( + row.depreciation_start_date, schedule_idx * cint(row.frequency_of_depreciation) + ) + + if is_last_day_of_the_month(row.depreciation_start_date): + orginal_schedule_date = get_last_day(orginal_schedule_date) + + if orginal_schedule_date == posting_date_of_disposal: + return True + + return False + + +def disposal_happens_in_the_future(posting_date_of_disposal): + if posting_date_of_disposal > getdate(): + return True + + return False + + +def get_gl_entries_on_asset_regain( + asset, selling_amount=0, finance_book=None, voucher_type=None, voucher_no=None +): ( fixed_asset_account, asset, @@ -246,28 +440,45 @@ def get_gl_entries_on_asset_regain(asset, selling_amount=0, finance_book=None): ) = get_asset_details(asset, finance_book) gl_entries = [ - { - "account": fixed_asset_account, - "debit_in_account_currency": asset.gross_purchase_amount, - "debit": asset.gross_purchase_amount, - "cost_center": depreciation_cost_center, - }, - { - "account": accumulated_depr_account, - "credit_in_account_currency": accumulated_depr_amount, - "credit": accumulated_depr_amount, - "cost_center": depreciation_cost_center, - }, + asset.get_gl_dict( + { + "account": fixed_asset_account, + "debit_in_account_currency": asset.gross_purchase_amount, + "debit": asset.gross_purchase_amount, + "cost_center": depreciation_cost_center, + "posting_date": getdate(), + }, + item=asset, + ), + asset.get_gl_dict( + { + "account": accumulated_depr_account, + "credit_in_account_currency": accumulated_depr_amount, + "credit": accumulated_depr_amount, + "cost_center": depreciation_cost_center, + "posting_date": getdate(), + }, + item=asset, + ), ] profit_amount = abs(flt(value_after_depreciation)) - abs(flt(selling_amount)) if profit_amount: - get_profit_gl_entries(profit_amount, gl_entries, disposal_account, depreciation_cost_center) + get_profit_gl_entries( + asset, profit_amount, gl_entries, disposal_account, depreciation_cost_center + ) + + if voucher_type and voucher_no: + for entry in gl_entries: + entry["voucher_type"] = voucher_type + entry["voucher_no"] = voucher_no return gl_entries -def get_gl_entries_on_asset_disposal(asset, selling_amount=0, finance_book=None): +def get_gl_entries_on_asset_disposal( + asset, selling_amount=0, finance_book=None, voucher_type=None, voucher_no=None +): ( fixed_asset_account, asset, @@ -279,23 +490,38 @@ def get_gl_entries_on_asset_disposal(asset, selling_amount=0, finance_book=None) ) = get_asset_details(asset, finance_book) gl_entries = [ - { - "account": fixed_asset_account, - "credit_in_account_currency": asset.gross_purchase_amount, - "credit": asset.gross_purchase_amount, - "cost_center": depreciation_cost_center, - }, - { - "account": accumulated_depr_account, - "debit_in_account_currency": accumulated_depr_amount, - "debit": accumulated_depr_amount, - "cost_center": depreciation_cost_center, - }, + asset.get_gl_dict( + { + "account": fixed_asset_account, + "credit_in_account_currency": asset.gross_purchase_amount, + "credit": asset.gross_purchase_amount, + "cost_center": depreciation_cost_center, + "posting_date": getdate(), + }, + item=asset, + ), + asset.get_gl_dict( + { + "account": accumulated_depr_account, + "debit_in_account_currency": accumulated_depr_amount, + "debit": accumulated_depr_amount, + "cost_center": depreciation_cost_center, + "posting_date": getdate(), + }, + item=asset, + ), ] profit_amount = flt(selling_amount) - flt(value_after_depreciation) if profit_amount: - get_profit_gl_entries(profit_amount, gl_entries, disposal_account, depreciation_cost_center) + get_profit_gl_entries( + asset, profit_amount, gl_entries, disposal_account, depreciation_cost_center + ) + + if voucher_type and voucher_no: + for entry in gl_entries: + entry["voucher_type"] = voucher_type + entry["voucher_no"] = voucher_no return gl_entries @@ -307,18 +533,8 @@ def get_asset_details(asset, finance_book=None): disposal_account, depreciation_cost_center = get_disposal_account_and_cost_center(asset.company) depreciation_cost_center = asset.cost_center or depreciation_cost_center - idx = 1 - if finance_book: - for d in asset.finance_books: - if d.finance_book == finance_book: - idx = d.idx - break + value_after_depreciation = asset.get_value_after_depreciation(finance_book) - value_after_depreciation = ( - asset.finance_books[idx - 1].value_after_depreciation - if asset.calculate_depreciation - else asset.value_after_depreciation - ) accumulated_depr_amount = flt(asset.gross_purchase_amount) - flt(value_after_depreciation) return ( @@ -332,15 +548,21 @@ def get_asset_details(asset, finance_book=None): ) -def get_profit_gl_entries(profit_amount, gl_entries, disposal_account, depreciation_cost_center): +def get_profit_gl_entries( + asset, profit_amount, gl_entries, disposal_account, depreciation_cost_center +): debit_or_credit = "debit" if profit_amount < 0 else "credit" gl_entries.append( - { - "account": disposal_account, - "cost_center": depreciation_cost_center, - debit_or_credit: abs(profit_amount), - debit_or_credit + "_in_account_currency": abs(profit_amount), - } + asset.get_gl_dict( + { + "account": disposal_account, + "cost_center": depreciation_cost_center, + debit_or_credit: abs(profit_amount), + debit_or_credit + "_in_account_currency": abs(profit_amount), + "posting_date": getdate(), + }, + item=asset, + ) ) @@ -358,3 +580,33 @@ def get_disposal_account_and_cost_center(company): frappe.throw(_("Please set 'Asset Depreciation Cost Center' in Company {0}").format(company)) return disposal_account, depreciation_cost_center + + +@frappe.whitelist() +def get_value_after_depreciation_on_disposal_date(asset, disposal_date, finance_book=None): + asset_doc = frappe.get_doc("Asset", asset) + + if not asset_doc.calculate_depreciation: + return flt(asset_doc.value_after_depreciation) + + idx = 1 + if finance_book: + for d in asset.finance_books: + if d.finance_book == finance_book: + idx = d.idx + break + + row = asset_doc.finance_books[idx - 1] + + temp_asset_depreciation_schedule = get_temp_asset_depr_schedule_doc( + asset_doc, row, getdate(disposal_date) + ) + + accumulated_depr_amount = temp_asset_depreciation_schedule.get("depreciation_schedule")[ + -1 + ].accumulated_depreciation_amount + + return flt( + flt(asset_doc.gross_purchase_amount) - accumulated_depr_amount, + asset_doc.precision("gross_purchase_amount"), + ) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index 986b7001ff..9a152638f9 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -4,15 +4,34 @@ import unittest import frappe -from frappe.utils import add_days, add_months, cstr, flt, get_last_day, getdate, nowdate +from frappe.utils import ( + add_days, + add_months, + cstr, + flt, + get_first_day, + get_last_day, + getdate, + is_last_day_of_the_month, + nowdate, +) +from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice -from erpnext.assets.doctype.asset.asset import make_sales_invoice, split_asset +from erpnext.assets.doctype.asset.asset import ( + make_sales_invoice, + split_asset, + update_maintenance_status, +) from erpnext.assets.doctype.asset.depreciation import ( post_depreciation_entries, restore_asset, scrap_asset, ) +from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import ( + get_asset_depr_schedule_doc, + get_depr_schedule, +) from erpnext.stock.doctype.purchase_receipt.purchase_receipt import ( make_purchase_invoice as make_invoice, ) @@ -178,28 +197,67 @@ class TestAsset(AssetSetup): self.assertEqual(doc.items[0].is_fixed_asset, 1) def test_scrap_asset(self): + date = nowdate() + purchase_date = add_months(get_first_day(date), -2) + asset = create_asset( calculate_depreciation=1, - available_for_use_date="2020-01-01", - purchase_date="2020-01-01", + available_for_use_date=purchase_date, + purchase_date=purchase_date, expected_value_after_useful_life=10000, total_number_of_depreciations=10, frequency_of_depreciation=1, submit=1, ) - post_depreciation_entries(date=add_months("2020-01-01", 4)) + first_asset_depr_schedule = get_asset_depr_schedule_doc(asset.name, "Active") + self.assertEquals(first_asset_depr_schedule.status, "Active") + + post_depreciation_entries(date=add_months(purchase_date, 2)) + asset.load_from_db() + + accumulated_depr_amount = flt( + asset.gross_purchase_amount - asset.finance_books[0].value_after_depreciation, + asset.precision("gross_purchase_amount"), + ) + self.assertEquals(accumulated_depr_amount, 18000.0) scrap_asset(asset.name) - asset.load_from_db() + first_asset_depr_schedule.load_from_db() + + second_asset_depr_schedule = get_asset_depr_schedule_doc(asset.name, "Active") + self.assertEquals(second_asset_depr_schedule.status, "Active") + self.assertEquals(first_asset_depr_schedule.status, "Cancelled") + + accumulated_depr_amount = flt( + asset.gross_purchase_amount - asset.finance_books[0].value_after_depreciation, + asset.precision("gross_purchase_amount"), + ) + pro_rata_amount, _, _ = asset.get_pro_rata_amt( + asset.finance_books[0], 9000, get_last_day(add_months(purchase_date, 1)), date + ) + pro_rata_amount = flt(pro_rata_amount, asset.precision("gross_purchase_amount")) + self.assertEquals( + accumulated_depr_amount, + flt(18000.0 + pro_rata_amount, asset.precision("gross_purchase_amount")), + ) + self.assertEqual(asset.status, "Scrapped") self.assertTrue(asset.journal_entry_for_scrap) expected_gle = ( - ("_Test Accumulated Depreciations - _TC", 36000.0, 0.0), + ( + "_Test Accumulated Depreciations - _TC", + flt(18000.0 + pro_rata_amount, asset.precision("gross_purchase_amount")), + 0.0, + ), ("_Test Fixed Asset - _TC", 0.0, 100000.0), - ("_Test Gain/Loss on Asset Disposal - _TC", 64000.0, 0.0), + ( + "_Test Gain/Loss on Asset Disposal - _TC", + flt(82000.0 - pro_rata_amount, asset.precision("gross_purchase_amount")), + 0.0, + ), ) gle = frappe.db.sql( @@ -211,12 +269,91 @@ class TestAsset(AssetSetup): self.assertSequenceEqual(gle, expected_gle) restore_asset(asset.name) + second_asset_depr_schedule.load_from_db() + + third_asset_depr_schedule = get_asset_depr_schedule_doc(asset.name, "Active") + self.assertEquals(third_asset_depr_schedule.status, "Active") + self.assertEquals(second_asset_depr_schedule.status, "Cancelled") asset.load_from_db() self.assertFalse(asset.journal_entry_for_scrap) self.assertEqual(asset.status, "Partially Depreciated") + accumulated_depr_amount = flt( + asset.gross_purchase_amount - asset.finance_books[0].value_after_depreciation, + asset.precision("gross_purchase_amount"), + ) + this_month_depr_amount = 9000.0 if is_last_day_of_the_month(date) else 0 + + self.assertEquals(accumulated_depr_amount, 18000.0 + this_month_depr_amount) + def test_gle_made_by_asset_sale(self): + date = nowdate() + purchase_date = add_months(get_first_day(date), -2) + + asset = create_asset( + calculate_depreciation=1, + available_for_use_date=purchase_date, + purchase_date=purchase_date, + expected_value_after_useful_life=10000, + total_number_of_depreciations=10, + frequency_of_depreciation=1, + submit=1, + ) + + first_asset_depr_schedule = get_asset_depr_schedule_doc(asset.name, "Active") + self.assertEquals(first_asset_depr_schedule.status, "Active") + + post_depreciation_entries(date=add_months(purchase_date, 2)) + + si = make_sales_invoice(asset=asset.name, item_code="Macbook Pro", company="_Test Company") + si.customer = "_Test Customer" + si.due_date = nowdate() + si.get("items")[0].rate = 25000 + si.insert() + si.submit() + + self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold") + + first_asset_depr_schedule.load_from_db() + + second_asset_depr_schedule = get_asset_depr_schedule_doc(asset.name, "Active") + self.assertEquals(second_asset_depr_schedule.status, "Active") + self.assertEquals(first_asset_depr_schedule.status, "Cancelled") + + pro_rata_amount, _, _ = asset.get_pro_rata_amt( + asset.finance_books[0], 9000, get_last_day(add_months(purchase_date, 1)), date + ) + pro_rata_amount = flt(pro_rata_amount, asset.precision("gross_purchase_amount")) + + expected_gle = ( + ( + "_Test Accumulated Depreciations - _TC", + flt(18000.0 + pro_rata_amount, asset.precision("gross_purchase_amount")), + 0.0, + ), + ("_Test Fixed Asset - _TC", 0.0, 100000.0), + ( + "_Test Gain/Loss on Asset Disposal - _TC", + flt(57000.0 - pro_rata_amount, asset.precision("gross_purchase_amount")), + 0.0, + ), + ("Debtors - _TC", 25000.0, 0.0), + ) + + gle = frappe.db.sql( + """select account, debit, credit from `tabGL Entry` + where voucher_type='Sales Invoice' and voucher_no = %s + order by account""", + si.name, + ) + + self.assertSequenceEqual(gle, expected_gle) + + si.cancel() + self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Partially Depreciated") + + def test_asset_with_maintenance_required_status_after_sale(self): asset = create_asset( calculate_depreciation=1, available_for_use_date="2020-06-06", @@ -224,6 +361,7 @@ class TestAsset(AssetSetup): expected_value_after_useful_life=10000, total_number_of_depreciations=3, frequency_of_depreciation=10, + maintenance_required=1, depreciation_start_date="2020-12-31", submit=1, ) @@ -239,24 +377,9 @@ class TestAsset(AssetSetup): self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold") - expected_gle = ( - ("_Test Accumulated Depreciations - _TC", 20490.2, 0.0), - ("_Test Fixed Asset - _TC", 0.0, 100000.0), - ("_Test Gain/Loss on Asset Disposal - _TC", 54509.8, 0.0), - ("Debtors - _TC", 25000.0, 0.0), - ) + update_maintenance_status() - gle = frappe.db.sql( - """select account, debit, credit from `tabGL Entry` - where voucher_type='Sales Invoice' and voucher_no = %s - order by account""", - si.name, - ) - - self.assertSequenceEqual(gle, expected_gle) - - si.cancel() - self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Partially Depreciated") + self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold") def test_asset_splitting(self): asset = create_asset( @@ -274,6 +397,9 @@ class TestAsset(AssetSetup): submit=1, ) + first_asset_depr_schedule = get_asset_depr_schedule_doc(asset.name, "Active") + self.assertEquals(first_asset_depr_schedule.status, "Active") + post_depreciation_entries(date="2021-01-01") self.assertEqual(asset.asset_quantity, 10) @@ -282,21 +408,31 @@ class TestAsset(AssetSetup): new_asset = split_asset(asset.name, 2) asset.load_from_db() + first_asset_depr_schedule.load_from_db() + + second_asset_depr_schedule = get_asset_depr_schedule_doc(asset.name, "Active") + first_asset_depr_schedule_of_new_asset = get_asset_depr_schedule_doc(new_asset.name, "Active") + self.assertEquals(second_asset_depr_schedule.status, "Active") + self.assertEquals(first_asset_depr_schedule_of_new_asset.status, "Active") + self.assertEquals(first_asset_depr_schedule.status, "Cancelled") + + depr_schedule_of_asset = second_asset_depr_schedule.get("depreciation_schedule") + depr_schedule_of_new_asset = first_asset_depr_schedule_of_new_asset.get("depreciation_schedule") self.assertEqual(new_asset.asset_quantity, 2) self.assertEqual(new_asset.gross_purchase_amount, 24000) self.assertEqual(new_asset.opening_accumulated_depreciation, 4000) self.assertEqual(new_asset.split_from, asset.name) - self.assertEqual(new_asset.schedules[0].depreciation_amount, 4000) - self.assertEqual(new_asset.schedules[1].depreciation_amount, 4000) + self.assertEqual(depr_schedule_of_new_asset[0].depreciation_amount, 4000) + self.assertEqual(depr_schedule_of_new_asset[1].depreciation_amount, 4000) self.assertEqual(asset.asset_quantity, 8) self.assertEqual(asset.gross_purchase_amount, 96000) self.assertEqual(asset.opening_accumulated_depreciation, 16000) - self.assertEqual(asset.schedules[0].depreciation_amount, 16000) - self.assertEqual(asset.schedules[1].depreciation_amount, 16000) + self.assertEqual(depr_schedule_of_asset[0].depreciation_amount, 16000) + self.assertEqual(depr_schedule_of_asset[1].depreciation_amount, 16000) - journal_entry = asset.schedules[0].journal_entry + journal_entry = depr_schedule_of_asset[0].journal_entry jv = frappe.get_doc("Journal Entry", journal_entry) self.assertEqual(jv.accounts[0].credit_in_account_currency, 16000) @@ -533,7 +669,7 @@ class TestDepreciationMethods(AssetSetup): schedules = [ [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount] - for d in asset.get("schedules") + for d in get_depr_schedule(asset.name, "Draft") ] self.assertEqual(schedules, expected_schedules) @@ -555,7 +691,7 @@ class TestDepreciationMethods(AssetSetup): expected_schedules = [["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") + for d in get_depr_schedule(asset.name, "Draft") ] self.assertEqual(schedules, expected_schedules) @@ -582,7 +718,7 @@ class TestDepreciationMethods(AssetSetup): schedules = [ [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount] - for d in asset.get("schedules") + for d in get_depr_schedule(asset.name, "Draft") ] self.assertEqual(schedules, expected_schedules) @@ -607,7 +743,7 @@ class TestDepreciationMethods(AssetSetup): schedules = [ [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount] - for d in asset.get("schedules") + for d in get_depr_schedule(asset.name, "Draft") ] self.assertEqual(schedules, expected_schedules) @@ -637,7 +773,7 @@ class TestDepreciationMethods(AssetSetup): flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2), ] - for d in asset.get("schedules") + for d in get_depr_schedule(asset.name, "Draft") ] self.assertEqual(schedules, expected_schedules) @@ -669,7 +805,7 @@ class TestDepreciationMethods(AssetSetup): flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2), ] - for d in asset.get("schedules") + for d in get_depr_schedule(asset.name, "Draft") ] self.assertEqual(schedules, expected_schedules) @@ -702,7 +838,7 @@ class TestDepreciationMethods(AssetSetup): flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2), ] - for d in asset.get("schedules") + for d in get_depr_schedule(asset.name, "Draft") ] self.assertEqual(schedules, expected_schedules) @@ -735,7 +871,7 @@ class TestDepreciationMethods(AssetSetup): flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2), ] - for d in asset.get("schedules") + for d in get_depr_schedule(asset.name, "Draft") ] self.assertEqual(schedules, expected_schedules) @@ -758,7 +894,7 @@ class TestDepreciationBasics(AssetSetup): ["2022-12-31", 30000, 90000], ] - for i, schedule in enumerate(asset.schedules): + for i, schedule in enumerate(get_depr_schedule(asset.name, "Active")): self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date) self.assertEqual(expected_values[i][1], schedule.depreciation_amount) self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount) @@ -781,16 +917,13 @@ class TestDepreciationBasics(AssetSetup): ["2023-01-01", 15000, 90000], ] - for i, schedule in enumerate(asset.schedules): + for i, schedule in enumerate(get_depr_schedule(asset.name, "Active")): self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date) self.assertEqual(expected_values[i][1], schedule.depreciation_amount) self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount) def test_get_depreciation_amount(self): """Tests if get_depreciation_amount() returns the right value.""" - - from erpnext.assets.doctype.asset.asset import get_depreciation_amount - asset = create_asset(item_code="Macbook Pro", available_for_use_date="2019-12-31") asset.calculate_depreciation = 1 @@ -805,11 +938,11 @@ class TestDepreciationBasics(AssetSetup): }, ) - depreciation_amount = get_depreciation_amount(asset, 100000, asset.finance_books[0]) + depreciation_amount = asset.get_depreciation_amount(100000, asset.finance_books[0]) self.assertEqual(depreciation_amount, 30000) - def test_make_depreciation_schedule(self): - """Tests if make_depreciation_schedule() returns the right values.""" + def test_make_depr_schedule(self): + """Tests if make_depr_schedule() returns the right values.""" asset = create_asset( item_code="Macbook Pro", @@ -824,7 +957,7 @@ class TestDepreciationBasics(AssetSetup): expected_values = [["2020-12-31", 30000.0], ["2021-12-31", 30000.0], ["2022-12-31", 30000.0]] - for i, schedule in enumerate(asset.schedules): + for i, schedule in enumerate(get_depr_schedule(asset.name, "Draft")): self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date) self.assertEqual(expected_values[i][1], schedule.depreciation_amount) @@ -844,7 +977,7 @@ class TestDepreciationBasics(AssetSetup): expected_values = [30000.0, 60000.0, 90000.0] - for i, schedule in enumerate(asset.schedules): + for i, schedule in enumerate(get_depr_schedule(asset.name, "Draft")): self.assertEqual(expected_values[i], schedule.accumulated_depreciation_amount) def test_check_is_pro_rata(self): @@ -1024,9 +1157,11 @@ class TestDepreciationBasics(AssetSetup): post_depreciation_entries(date="2021-06-01") asset.load_from_db() - self.assertTrue(asset.schedules[0].journal_entry) - self.assertFalse(asset.schedules[1].journal_entry) - self.assertFalse(asset.schedules[2].journal_entry) + depr_schedule = get_depr_schedule(asset.name, "Active") + + self.assertTrue(depr_schedule[0].journal_entry) + self.assertFalse(depr_schedule[1].journal_entry) + self.assertFalse(depr_schedule[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.""" @@ -1045,7 +1180,7 @@ class TestDepreciationBasics(AssetSetup): post_depreciation_entries(date="2021-06-01") asset.load_from_db() - je = frappe.get_doc("Journal Entry", asset.schedules[0].journal_entry) + je = frappe.get_doc("Journal Entry", get_depr_schedule(asset.name, "Active")[0].journal_entry) accounting_entries = [ {"account": entry.account, "debit": entry.debit, "credit": entry.credit} for entry in je.accounts @@ -1081,7 +1216,7 @@ class TestDepreciationBasics(AssetSetup): post_depreciation_entries(date="2021-06-01") asset.load_from_db() - je = frappe.get_doc("Journal Entry", asset.schedules[0].journal_entry) + je = frappe.get_doc("Journal Entry", get_depr_schedule(asset.name, "Active")[0].journal_entry) accounting_entries = [ {"account": entry.account, "debit": entry.debit, "credit": entry.credit} for entry in je.accounts @@ -1100,8 +1235,8 @@ class TestDepreciationBasics(AssetSetup): 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.""" + def test_clear_depr_schedule(self): + """Tests if clear_depr_schedule() works as expected.""" asset = create_asset( item_code="Macbook Pro", @@ -1117,17 +1252,20 @@ class TestDepreciationBasics(AssetSetup): post_depreciation_entries(date="2021-06-01") asset.load_from_db() - asset.clear_depreciation_schedule() + asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset.name, "Active") - self.assertEqual(len(asset.schedules), 1) + asset_depr_schedule_doc.clear_depr_schedule() - def test_clear_depreciation_schedule_for_multiple_finance_books(self): + self.assertEqual(len(asset_depr_schedule_doc.get("depreciation_schedule")), 1) + + def test_clear_depr_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", { + "finance_book": "Test Finance Book 1", "depreciation_method": "Straight Line", "frequency_of_depreciation": 1, "total_number_of_depreciations": 3, @@ -1138,6 +1276,7 @@ class TestDepreciationBasics(AssetSetup): asset.append( "finance_books", { + "finance_book": "Test Finance Book 2", "depreciation_method": "Straight Line", "frequency_of_depreciation": 1, "total_number_of_depreciations": 6, @@ -1148,6 +1287,7 @@ class TestDepreciationBasics(AssetSetup): asset.append( "finance_books", { + "finance_book": "Test Finance Book 3", "depreciation_method": "Straight Line", "frequency_of_depreciation": 12, "total_number_of_depreciations": 3, @@ -1160,15 +1300,23 @@ class TestDepreciationBasics(AssetSetup): post_depreciation_entries(date="2020-04-01") asset.load_from_db() - asset.clear_depreciation_schedule() + asset_depr_schedule_doc_1 = get_asset_depr_schedule_doc( + asset.name, "Active", "Test Finance Book 1" + ) + asset_depr_schedule_doc_1.clear_depr_schedule() + self.assertEqual(len(asset_depr_schedule_doc_1.get("depreciation_schedule")), 3) - self.assertEqual(len(asset.schedules), 6) + asset_depr_schedule_doc_2 = get_asset_depr_schedule_doc( + asset.name, "Active", "Test Finance Book 2" + ) + asset_depr_schedule_doc_2.clear_depr_schedule() + self.assertEqual(len(asset_depr_schedule_doc_2.get("depreciation_schedule")), 3) - for schedule in asset.schedules: - if schedule.idx <= 3: - self.assertEqual(schedule.finance_book_id, "1") - else: - self.assertEqual(schedule.finance_book_id, "2") + asset_depr_schedule_doc_3 = get_asset_depr_schedule_doc( + asset.name, "Active", "Test Finance Book 3" + ) + asset_depr_schedule_doc_3.clear_depr_schedule() + self.assertEqual(len(asset_depr_schedule_doc_3.get("depreciation_schedule")), 0) 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) @@ -1177,6 +1325,7 @@ class TestDepreciationBasics(AssetSetup): asset.append( "finance_books", { + "finance_book": "Test Finance Book 1", "depreciation_method": "Straight Line", "frequency_of_depreciation": 12, "total_number_of_depreciations": 3, @@ -1187,6 +1336,7 @@ class TestDepreciationBasics(AssetSetup): asset.append( "finance_books", { + "finance_book": "Test Finance Book 2", "depreciation_method": "Straight Line", "frequency_of_depreciation": 12, "total_number_of_depreciations": 6, @@ -1196,13 +1346,15 @@ class TestDepreciationBasics(AssetSetup): ) asset.save() - self.assertEqual(len(asset.schedules), 9) + asset_depr_schedule_doc_1 = get_asset_depr_schedule_doc( + asset.name, "Draft", "Test Finance Book 1" + ) + self.assertEqual(len(asset_depr_schedule_doc_1.get("depreciation_schedule")), 3) - for schedule in asset.schedules: - if schedule.idx <= 3: - self.assertEqual(schedule.finance_book_id, 1) - else: - self.assertEqual(schedule.finance_book_id, 2) + asset_depr_schedule_doc_2 = get_asset_depr_schedule_doc( + asset.name, "Draft", "Test Finance Book 2" + ) + self.assertEqual(len(asset_depr_schedule_doc_2.get("depreciation_schedule")), 6) def test_depreciation_entry_cancellation(self): asset = create_asset( @@ -1222,12 +1374,12 @@ class TestDepreciationBasics(AssetSetup): asset.load_from_db() # cancel depreciation entry - depr_entry = asset.get("schedules")[0].journal_entry + depr_entry = get_depr_schedule(asset.name, "Active")[0].journal_entry self.assertTrue(depr_entry) + frappe.get_doc("Journal Entry", depr_entry).cancel() - asset.load_from_db() - depr_entry = asset.get("schedules")[0].journal_entry + depr_entry = get_depr_schedule(asset.name, "Active")[0].journal_entry self.assertFalse(depr_entry) def test_asset_expected_value_after_useful_life(self): @@ -1242,7 +1394,7 @@ class TestDepreciationBasics(AssetSetup): ) accumulated_depreciation_after_full_schedule = max( - d.accumulated_depreciation_amount for d in asset.get("schedules") + d.accumulated_depreciation_amount for d in get_depr_schedule(asset.name, "Draft") ) asset_value_after_full_schedule = flt(asset.gross_purchase_amount) - flt( @@ -1273,7 +1425,7 @@ class TestDepreciationBasics(AssetSetup): asset.load_from_db() # check depreciation entry series - self.assertEqual(asset.get("schedules")[0].journal_entry[:4], "DEPR") + self.assertEqual(get_depr_schedule(asset.name, "Active")[0].journal_entry[:4], "DEPR") expected_gle = ( ("_Test Accumulated Depreciations - _TC", 0.0, 30000.0), @@ -1343,9 +1495,39 @@ class TestDepreciationBasics(AssetSetup): "2020-07-15", ] - for i, schedule in enumerate(asset.schedules): + for i, schedule in enumerate(get_depr_schedule(asset.name, "Active")): self.assertEqual(getdate(expected_dates[i]), getdate(schedule.schedule_date)) + def test_manual_depreciation_for_existing_asset(self): + asset = create_asset( + item_code="Macbook Pro", + is_existing_asset=1, + purchase_date="2020-01-30", + available_for_use_date="2020-01-30", + submit=1, + ) + + self.assertEqual(asset.status, "Submitted") + self.assertEqual(asset.get("value_after_depreciation"), 100000) + + jv = make_journal_entry( + "_Test Depreciations - _TC", "_Test Accumulated Depreciations - _TC", 100, save=False + ) + for d in jv.accounts: + d.reference_type = "Asset" + d.reference_name = asset.name + jv.voucher_type = "Depreciation Entry" + jv.insert() + jv.submit() + + asset.reload() + self.assertEqual(asset.get("value_after_depreciation"), 99900) + + jv.cancel() + + asset.reload() + self.assertEqual(asset.get("value_after_depreciation"), 100000) + def create_asset_data(): if not frappe.db.exists("Asset Category", "Computers"): @@ -1357,6 +1539,15 @@ def create_asset_data(): if not frappe.db.exists("Location", "Test Location"): frappe.get_doc({"doctype": "Location", "location_name": "Test Location"}).insert() + if not frappe.db.exists("Finance Book", "Test Finance Book 1"): + frappe.get_doc({"doctype": "Finance Book", "finance_book_name": "Test Finance Book 1"}).insert() + + if not frappe.db.exists("Finance Book", "Test Finance Book 2"): + frappe.get_doc({"doctype": "Finance Book", "finance_book_name": "Test Finance Book 2"}).insert() + + if not frappe.db.exists("Finance Book", "Test Finance Book 3"): + frappe.get_doc({"doctype": "Finance Book", "finance_book_name": "Test Finance Book 3"}).insert() + def create_asset(**args): args = frappe._dict(args) @@ -1376,12 +1567,14 @@ def create_asset(**args): "number_of_depreciations_booked": args.number_of_depreciations_booked or 0, "gross_purchase_amount": args.gross_purchase_amount or 100000, "purchase_receipt_amount": args.purchase_receipt_amount or 100000, + "maintenance_required": args.maintenance_required or 0, "warehouse": args.warehouse or "_Test Warehouse - _TC", "available_for_use_date": args.available_for_use_date or "2020-06-06", "location": args.location or "Test Location", "asset_owner": args.asset_owner or "Company", "is_existing_asset": args.is_existing_asset or 1, "asset_quantity": args.get("asset_quantity") or 1, + "depr_entry_posting_status": args.depr_entry_posting_status or "", } ) @@ -1425,6 +1618,16 @@ def create_asset_category(): "depreciation_expense_account": "_Test Depreciations - _TC", }, ) + asset_category.append( + "accounts", + { + "company_name": "_Test Company with perpetual inventory", + "fixed_asset_account": "_Test Fixed Asset - TCP1", + "accumulated_depreciation_account": "_Test Accumulated Depreciations - TCP1", + "depreciation_expense_account": "_Test Depreciations - TCP1", + }, + ) + asset_category.insert() @@ -1454,12 +1657,14 @@ def create_fixed_asset_item(item_code=None, auto_create_assets=1, is_grouped_ass return item -def set_depreciation_settings_in_company(): - company = frappe.get_doc("Company", "_Test Company") - company.accumulated_depreciation_account = "_Test Accumulated Depreciations - _TC" - company.depreciation_expense_account = "_Test Depreciations - _TC" - company.disposal_account = "_Test Gain/Loss on Asset Disposal - _TC" - company.depreciation_cost_center = "_Test Cost Center - _TC" +def set_depreciation_settings_in_company(company=None): + if not company: + company = "_Test Company" + company = frappe.get_doc("Company", company) + company.accumulated_depreciation_account = "_Test Accumulated Depreciations - " + company.abbr + company.depreciation_expense_account = "_Test Depreciations - " + company.abbr + company.disposal_account = "_Test Gain/Loss on Asset Disposal - " + company.abbr + company.depreciation_cost_center = "Main - " + company.abbr company.save() # Enable booking asset depreciation entry automatically diff --git a/erpnext/regional/doctype/ksa_vat_setting/__init__.py b/erpnext/assets/doctype/asset_capitalization/__init__.py similarity index 100% rename from erpnext/regional/doctype/ksa_vat_setting/__init__.py rename to erpnext/assets/doctype/asset_capitalization/__init__.py diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js new file mode 100644 index 0000000000..9c7f70b0e5 --- /dev/null +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js @@ -0,0 +1,417 @@ +// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.provide("erpnext.assets"); + + +erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.stock.StockController { + setup() { + this.setup_posting_date_time_check(); + } + + onload() { + this.setup_queries(); + } + + refresh() { + erpnext.hide_company(); + this.show_general_ledger(); + if ((this.frm.doc.stock_items && this.frm.doc.stock_items.length) || !this.frm.doc.target_is_fixed_asset) { + this.show_stock_ledger(); + } + } + + setup_queries() { + var me = this; + + me.setup_warehouse_query(); + + me.frm.set_query("target_item_code", function() { + if (me.frm.doc.entry_type == "Capitalization") { + return erpnext.queries.item({"is_stock_item": 0, "is_fixed_asset": 1}); + } else { + return erpnext.queries.item({"is_stock_item": 1, "is_fixed_asset": 0}); + } + }); + + me.frm.set_query("target_asset", function() { + var filters = {}; + + if (me.frm.doc.target_item_code) { + filters['item_code'] = me.frm.doc.target_item_code; + } + + filters['status'] = ["not in", ["Draft", "Scrapped", "Sold", "Capitalized", "Decapitalized"]]; + filters['docstatus'] = 1; + + return { + filters: filters + }; + }); + + me.frm.set_query("asset", "asset_items", function() { + var filters = { + 'status': ["not in", ["Draft", "Scrapped", "Sold", "Capitalized", "Decapitalized"]], + 'docstatus': 1 + }; + + if (me.frm.doc.target_asset) { + filters['name'] = ['!=', me.frm.doc.target_asset]; + } + + return { + filters: filters + }; + }); + + me.frm.set_query("item_code", "stock_items", function() { + return erpnext.queries.item({"is_stock_item": 1}); + }); + + me.frm.set_query("item_code", "service_items", function() { + return erpnext.queries.item({"is_stock_item": 0, "is_fixed_asset": 0}); + }); + + me.frm.set_query('batch_no', 'stock_items', function(doc, cdt, cdn) { + var item = locals[cdt][cdn]; + if (!item.item_code) { + frappe.throw(__("Please enter Item Code to get Batch Number")); + } else { + var filters = { + 'item_code': item.item_code, + 'posting_date': me.frm.doc.posting_date || frappe.datetime.nowdate(), + 'warehouse': item.warehouse + }; + + return { + query: "erpnext.controllers.queries.get_batch_no", + filters: filters + }; + } + }); + + me.frm.set_query('expense_account', 'service_items', function() { + return { + filters: { + "account_type": ['in', ["Tax", "Expense Account", "Income Account", "Expenses Included In Valuation", "Expenses Included In Asset Valuation"]], + "is_group": 0, + "company": me.frm.doc.company + } + }; + }); + } + + target_item_code() { + return this.get_target_item_details(); + } + + target_asset() { + return this.get_target_asset_details(); + } + + item_code(doc, cdt, cdn) { + var row = frappe.get_doc(cdt, cdn); + if (cdt === "Asset Capitalization Stock Item") { + this.get_consumed_stock_item_details(row); + } else if (cdt == "Asset Capitalization Service Item") { + this.get_service_item_details(row); + } + } + + warehouse(doc, cdt, cdn) { + var row = frappe.get_doc(cdt, cdn); + if (cdt === "Asset Capitalization Stock Item") { + this.get_warehouse_details(row); + } + } + + asset(doc, cdt, cdn) { + var row = frappe.get_doc(cdt, cdn); + if (cdt === "Asset Capitalization Asset Item") { + this.get_consumed_asset_details(row); + } + } + + posting_date() { + if (this.frm.doc.posting_date) { + frappe.run_serially([ + () => this.get_all_item_warehouse_details(), + () => this.get_all_asset_values() + ]); + } + } + + posting_time() { + if (this.frm.doc.posting_time) { + this.get_all_item_warehouse_details(); + } + } + + finance_book(doc, cdt, cdn) { + if (cdt === "Asset Capitalization Asset Item") { + var row = frappe.get_doc(cdt, cdn); + this.get_consumed_asset_details(row); + } else { + this.get_all_asset_values(); + } + } + + stock_qty() { + this.calculate_totals(); + } + + qty() { + this.calculate_totals(); + } + + target_qty() { + this.calculate_totals(); + } + + rate() { + this.calculate_totals(); + } + + company() { + var me = this; + + if (me.frm.doc.company) { + frappe.model.set_value(me.frm.doc.doctype, me.frm.doc.name, "cost_center", null); + $.each(me.frm.doc.stock_items || [], function (i, d) { + frappe.model.set_value(d.doctype, d.name, "cost_center", null); + }); + $.each(me.frm.doc.asset_items || [], function (i, d) { + frappe.model.set_value(d.doctype, d.name, "cost_center", null); + }); + $.each(me.frm.doc.service_items || [], function (i, d) { + frappe.model.set_value(d.doctype, d.name, "cost_center", null); + }); + } + + erpnext.accounts.dimensions.update_dimension(me.frm, me.frm.doctype); + } + + stock_items_add(doc, cdt, cdn) { + erpnext.accounts.dimensions.copy_dimension_from_first_row(this.frm, cdt, cdn, 'stock_items'); + } + + asset_items_add(doc, cdt, cdn) { + erpnext.accounts.dimensions.copy_dimension_from_first_row(this.frm, cdt, cdn, 'asset_items'); + } + + serivce_items_add(doc, cdt, cdn) { + erpnext.accounts.dimensions.copy_dimension_from_first_row(this.frm, cdt, cdn, 'service_items'); + } + + get_target_item_details() { + var me = this; + + if (me.frm.doc.target_item_code) { + return me.frm.call({ + method: "erpnext.assets.doctype.asset_capitalization.asset_capitalization.get_target_item_details", + child: me.frm.doc, + args: { + item_code: me.frm.doc.target_item_code, + company: me.frm.doc.company, + }, + callback: function (r) { + if (!r.exc) { + me.frm.refresh_fields(); + } + } + }); + } + } + + get_target_asset_details() { + var me = this; + + if (me.frm.doc.target_asset) { + return me.frm.call({ + method: "erpnext.assets.doctype.asset_capitalization.asset_capitalization.get_target_asset_details", + child: me.frm.doc, + args: { + asset: me.frm.doc.target_asset, + company: me.frm.doc.company, + }, + callback: function (r) { + if (!r.exc) { + me.frm.refresh_fields(); + } + } + }); + } + } + + get_consumed_stock_item_details(row) { + var me = this; + + if (row && row.item_code) { + return me.frm.call({ + method: "erpnext.assets.doctype.asset_capitalization.asset_capitalization.get_consumed_stock_item_details", + child: row, + args: { + args: { + item_code: row.item_code, + warehouse: row.warehouse, + stock_qty: flt(row.stock_qty), + doctype: me.frm.doc.doctype, + name: me.frm.doc.name, + company: me.frm.doc.company, + posting_date: me.frm.doc.posting_date, + posting_time: me.frm.doc.posting_time, + } + }, + callback: function (r) { + if (!r.exc) { + me.calculate_totals(); + } + } + }); + } + } + + get_consumed_asset_details(row) { + var me = this; + + if (row && row.asset) { + return me.frm.call({ + method: "erpnext.assets.doctype.asset_capitalization.asset_capitalization.get_consumed_asset_details", + child: row, + args: { + args: { + asset: row.asset, + doctype: me.frm.doc.doctype, + name: me.frm.doc.name, + company: me.frm.doc.company, + finance_book: row.finance_book || me.frm.doc.finance_book, + posting_date: me.frm.doc.posting_date, + posting_time: me.frm.doc.posting_time, + } + }, + callback: function (r) { + if (!r.exc) { + me.calculate_totals(); + } + } + }); + } + } + + get_service_item_details(row) { + var me = this; + + if (row && row.item_code) { + return me.frm.call({ + method: "erpnext.assets.doctype.asset_capitalization.asset_capitalization.get_service_item_details", + child: row, + args: { + args: { + item_code: row.item_code, + qty: flt(row.qty), + expense_account: row.expense_account, + company: me.frm.doc.company, + } + }, + callback: function (r) { + if (!r.exc) { + me.calculate_totals(); + } + } + }); + } + } + + get_warehouse_details(item) { + var me = this; + if (item.item_code && item.warehouse) { + me.frm.call({ + method: "erpnext.assets.doctype.asset_capitalization.asset_capitalization.get_warehouse_details", + child: item, + args: { + args: { + 'item_code': item.item_code, + 'warehouse': cstr(item.warehouse), + 'qty': flt(item.stock_qty), + 'serial_no': item.serial_no, + 'posting_date': me.frm.doc.posting_date, + 'posting_time': me.frm.doc.posting_time, + 'company': me.frm.doc.company, + 'voucher_type': me.frm.doc.doctype, + 'voucher_no': me.frm.doc.name, + 'allow_zero_valuation': 1 + } + }, + callback: function(r) { + if (!r.exc) { + me.calculate_totals(); + } + } + }); + } + } + + get_all_item_warehouse_details() { + var me = this; + return me.frm.call({ + method: "set_warehouse_details", + doc: me.frm.doc, + callback: function(r) { + if (!r.exc) { + me.calculate_totals(); + } + } + }); + } + + get_all_asset_values() { + var me = this; + return me.frm.call({ + method: "set_asset_values", + doc: me.frm.doc, + callback: function(r) { + if (!r.exc) { + me.calculate_totals(); + } + } + }); + } + + calculate_totals() { + var me = this; + + me.frm.doc.stock_items_total = 0; + me.frm.doc.asset_items_total = 0; + me.frm.doc.service_items_total = 0; + + $.each(me.frm.doc.stock_items || [], function (i, d) { + d.amount = flt(flt(d.stock_qty) * flt(d.valuation_rate), precision('amount', d)); + me.frm.doc.stock_items_total += d.amount; + }); + + $.each(me.frm.doc.asset_items || [], function (i, d) { + d.asset_value = flt(flt(d.asset_value), precision('asset_value', d)); + me.frm.doc.asset_items_total += d.asset_value; + }); + + $.each(me.frm.doc.service_items || [], function (i, d) { + d.amount = flt(flt(d.qty) * flt(d.rate), precision('amount', d)); + me.frm.doc.service_items_total += d.amount; + }); + + me.frm.doc.stock_items_total = flt(me.frm.doc.stock_items_total, precision('stock_items_total')); + me.frm.doc.asset_items_total = flt(me.frm.doc.asset_items_total, precision('asset_items_total')); + me.frm.doc.service_items_total = flt(me.frm.doc.service_items_total, precision('service_items_total')); + + me.frm.doc.total_value = me.frm.doc.stock_items_total + me.frm.doc.asset_items_total + me.frm.doc.service_items_total; + me.frm.doc.total_value = flt(me.frm.doc.total_value, precision('total_value')); + + me.frm.doc.target_qty = flt(me.frm.doc.target_qty, precision('target_qty')); + me.frm.doc.target_incoming_rate = me.frm.doc.target_qty ? me.frm.doc.total_value / flt(me.frm.doc.target_qty) + : me.frm.doc.total_value; + + me.frm.refresh_fields(); + } +}; + +cur_frm.cscript = new erpnext.assets.AssetCapitalization({frm: cur_frm}); diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.json b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.json new file mode 100644 index 0000000000..d1be5752d6 --- /dev/null +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.json @@ -0,0 +1,381 @@ +{ + "actions": [], + "autoname": "naming_series:", + "creation": "2021-09-04 13:38:04.217187", + "doctype": "DocType", + "document_type": "Document", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "title", + "naming_series", + "entry_type", + "target_item_code", + "target_item_name", + "target_is_fixed_asset", + "target_has_batch_no", + "target_has_serial_no", + "column_break_9", + "target_asset", + "target_asset_name", + "target_warehouse", + "target_qty", + "target_stock_uom", + "target_batch_no", + "target_serial_no", + "column_break_5", + "company", + "finance_book", + "posting_date", + "posting_time", + "set_posting_time", + "amended_from", + "section_break_16", + "stock_items", + "stock_items_total", + "section_break_26", + "asset_items", + "asset_items_total", + "service_expenses_section", + "service_items", + "service_items_total", + "totals_section", + "total_value", + "column_break_36", + "target_incoming_rate", + "accounting_dimensions_section", + "cost_center", + "dimension_col_break", + "target_fixed_asset_account" + ], + "fields": [ + { + "fieldname": "title", + "fieldtype": "Data", + "hidden": 1, + "label": "Title" + }, + { + "fieldname": "target_item_code", + "fieldtype": "Link", + "in_standard_filter": 1, + "label": "Target Item Code", + "options": "Item", + "reqd": 1 + }, + { + "depends_on": "eval:doc.target_item_code && doc.target_item_name != doc.target_item_code", + "fetch_from": "target_item_code.item_name", + "fieldname": "target_item_name", + "fieldtype": "Data", + "label": "Target Item Name", + "read_only": 1 + }, + { + "default": "0", + "fetch_from": "target_item_code.is_fixed_asset", + "fieldname": "target_is_fixed_asset", + "fieldtype": "Check", + "hidden": 1, + "label": "Target Is Fixed Asset", + "read_only": 1 + }, + { + "fieldname": "column_break_5", + "fieldtype": "Column Break" + }, + { + "depends_on": "eval:doc.entry_type=='Capitalization'", + "fieldname": "target_asset", + "fieldtype": "Link", + "in_standard_filter": 1, + "label": "Target Asset", + "mandatory_depends_on": "eval:doc.entry_type=='Capitalization'", + "no_copy": 1, + "options": "Asset" + }, + { + "depends_on": "eval:doc.entry_type=='Capitalization'", + "fetch_from": "target_asset.asset_name", + "fieldname": "target_asset_name", + "fieldtype": "Data", + "label": "Asset Name", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "column_break_9", + "fieldtype": "Column Break" + }, + { + "fetch_from": "asset.company", + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "reqd": 1 + }, + { + "default": "Today", + "fieldname": "posting_date", + "fieldtype": "Date", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Posting Date", + "no_copy": 1, + "reqd": 1, + "search_index": 1 + }, + { + "default": "Now", + "fieldname": "posting_time", + "fieldtype": "Time", + "label": "Posting Time", + "no_copy": 1, + "reqd": 1 + }, + { + "default": "0", + "depends_on": "eval:doc.docstatus==0", + "fieldname": "set_posting_time", + "fieldtype": "Check", + "label": "Edit Posting Date and Time" + }, + { + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Series", + "options": "ACC-ASC-.YYYY.-", + "reqd": 1 + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Asset Capitalization", + "print_hide": 1, + "read_only": 1 + }, + { + "depends_on": "eval:doc.docstatus == 0 || (doc.stock_items && doc.stock_items.length)", + "fieldname": "section_break_16", + "fieldtype": "Section Break", + "label": "Consumed Stock Items" + }, + { + "fieldname": "stock_items", + "fieldtype": "Table", + "label": "Stock Items", + "options": "Asset Capitalization Stock Item" + }, + { + "depends_on": "eval:doc.entry_type=='Decapitalization'", + "fieldname": "target_warehouse", + "fieldtype": "Link", + "label": "Target Warehouse", + "mandatory_depends_on": "eval:doc.entry_type=='Decapitalization'", + "options": "Warehouse" + }, + { + "depends_on": "target_has_batch_no", + "fieldname": "target_batch_no", + "fieldtype": "Link", + "label": "Target Batch No", + "options": "Batch" + }, + { + "default": "1", + "fieldname": "target_qty", + "fieldtype": "Float", + "label": "Target Qty", + "read_only_depends_on": "target_is_fixed_asset" + }, + { + "fetch_from": "target_item_code.stock_uom", + "fieldname": "target_stock_uom", + "fieldtype": "Link", + "label": "Stock UOM", + "options": "UOM", + "read_only": 1 + }, + { + "default": "0", + "fetch_from": "target_item_code.has_batch_no", + "fieldname": "target_has_batch_no", + "fieldtype": "Check", + "hidden": 1, + "label": "Target Has Batch No", + "read_only": 1 + }, + { + "default": "0", + "fetch_from": "target_item_code.has_serial_no", + "fieldname": "target_has_serial_no", + "fieldtype": "Check", + "hidden": 1, + "label": "Target Has Serial No", + "read_only": 1 + }, + { + "depends_on": "target_has_serial_no", + "fieldname": "target_serial_no", + "fieldtype": "Small Text", + "label": "Target Serial No" + }, + { + "depends_on": "eval:doc.docstatus == 0 || (doc.asset_items && doc.asset_items.length)", + "fieldname": "section_break_26", + "fieldtype": "Section Break", + "label": "Consumed Asset Items" + }, + { + "fieldname": "asset_items", + "fieldtype": "Table", + "label": "Assets", + "options": "Asset Capitalization Asset Item" + }, + { + "default": "Capitalization", + "fieldname": "entry_type", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Entry Type", + "options": "Capitalization\nDecapitalization", + "reqd": 1 + }, + { + "fieldname": "stock_items_total", + "fieldtype": "Currency", + "label": "Consumed Stock Total Value", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "asset_items_total", + "fieldtype": "Currency", + "label": "Consumed Asset Total Value", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "finance_book", + "fieldtype": "Link", + "label": "Finance Book", + "options": "Finance Book" + }, + { + "depends_on": "eval:doc.docstatus == 0 || (doc.service_items && doc.service_items.length)", + "fieldname": "service_expenses_section", + "fieldtype": "Section Break", + "label": "Service Expenses" + }, + { + "fieldname": "service_items", + "fieldtype": "Table", + "label": "Services", + "options": "Asset Capitalization Service Item" + }, + { + "fieldname": "service_items_total", + "fieldtype": "Currency", + "label": "Service Expense Total Amount", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "totals_section", + "fieldtype": "Section Break", + "label": "Totals" + }, + { + "fieldname": "total_value", + "fieldtype": "Currency", + "label": "Total Value", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "column_break_36", + "fieldtype": "Column Break" + }, + { + "fieldname": "target_incoming_rate", + "fieldtype": "Currency", + "label": "Target Incoming Rate", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "collapsible": 1, + "fieldname": "accounting_dimensions_section", + "fieldtype": "Section Break", + "label": "Accounting Dimensions" + }, + { + "fieldname": "cost_center", + "fieldtype": "Link", + "label": "Cost Center", + "options": "Cost Center" + }, + { + "fieldname": "dimension_col_break", + "fieldtype": "Column Break" + }, + { + "fieldname": "target_fixed_asset_account", + "fieldtype": "Link", + "label": "Target Fixed Asset Account", + "options": "Account", + "read_only": 1 + } + ], + "index_web_pages_for_search": 1, + "is_submittable": 1, + "links": [], + "modified": "2022-09-12 15:09:40.771332", + "modified_by": "Administrator", + "module": "Assets", + "name": "Asset Capitalization", + "naming_rule": "By \"Naming Series\" field", + "owner": "Administrator", + "permissions": [ + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Manufacturing Manager", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Quality Manager", + "share": 1, + "submit": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "title_field": "title", + "track_changes": 1, + "track_seen": 1 +} \ No newline at end of file diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py new file mode 100644 index 0000000000..5b910dbb2e --- /dev/null +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py @@ -0,0 +1,773 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import json + +import frappe + +# import erpnext +from frappe import _ +from frappe.utils import cint, flt, get_link_to_form + +import erpnext +from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciation +from erpnext.assets.doctype.asset.depreciation import ( + depreciate_asset, + get_gl_entries_on_asset_disposal, + get_value_after_depreciation_on_disposal_date, + reset_depreciation_schedule, + reverse_depreciation_entry_made_after_disposal, +) +from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account +from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import ( + make_new_active_asset_depr_schedules_and_cancel_current_ones, +) +from erpnext.controllers.stock_controller import StockController +from erpnext.setup.doctype.brand.brand import get_brand_defaults +from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults +from erpnext.stock import get_warehouse_account_map +from erpnext.stock.doctype.item.item import get_item_defaults +from erpnext.stock.get_item_details import ( + get_default_cost_center, + get_default_expense_account, + get_item_warehouse, +) +from erpnext.stock.stock_ledger import get_previous_sle +from erpnext.stock.utils import get_incoming_rate + +force_fields = [ + "target_item_name", + "target_asset_name", + "item_name", + "asset_name", + "target_is_fixed_asset", + "target_has_serial_no", + "target_has_batch_no", + "target_stock_uom", + "stock_uom", + "target_fixed_asset_account", + "fixed_asset_account", + "valuation_rate", +] + + +class AssetCapitalization(StockController): + def validate(self): + self.validate_posting_time() + self.set_missing_values(for_validate=True) + self.validate_target_item() + self.validate_target_asset() + self.validate_consumed_stock_item() + self.validate_consumed_asset_item() + self.validate_service_item() + self.set_warehouse_details() + self.set_asset_values() + self.calculate_totals() + self.set_title() + + def before_submit(self): + self.validate_source_mandatory() + + def on_submit(self): + self.update_stock_ledger() + self.make_gl_entries() + self.update_target_asset() + + def on_cancel(self): + self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation") + self.update_stock_ledger() + self.make_gl_entries() + self.update_target_asset() + + def set_title(self): + self.title = self.target_asset_name or self.target_item_name or self.target_item_code + + def set_missing_values(self, for_validate=False): + target_item_details = get_target_item_details(self.target_item_code, self.company) + for k, v in target_item_details.items(): + if self.meta.has_field(k) and (not self.get(k) or k in force_fields): + self.set(k, v) + + # Remove asset if item not a fixed asset + if not self.target_is_fixed_asset: + self.target_asset = None + + target_asset_details = get_target_asset_details(self.target_asset, self.company) + for k, v in target_asset_details.items(): + if self.meta.has_field(k) and (not self.get(k) or k in force_fields): + self.set(k, v) + + for d in self.stock_items: + args = self.as_dict() + args.update(d.as_dict()) + args.doctype = self.doctype + args.name = self.name + consumed_stock_item_details = get_consumed_stock_item_details(args) + for k, v in consumed_stock_item_details.items(): + if d.meta.has_field(k) and (not d.get(k) or k in force_fields): + d.set(k, v) + + for d in self.asset_items: + args = self.as_dict() + args.update(d.as_dict()) + args.doctype = self.doctype + args.name = self.name + args.finance_book = d.get("finance_book") or self.get("finance_book") + consumed_asset_details = get_consumed_asset_details(args) + for k, v in consumed_asset_details.items(): + if d.meta.has_field(k) and (not d.get(k) or k in force_fields): + d.set(k, v) + + for d in self.service_items: + args = self.as_dict() + args.update(d.as_dict()) + args.doctype = self.doctype + args.name = self.name + service_item_details = get_service_item_details(args) + for k, v in service_item_details.items(): + if d.meta.has_field(k) and (not d.get(k) or k in force_fields): + d.set(k, v) + + def validate_target_item(self): + target_item = frappe.get_cached_doc("Item", self.target_item_code) + + if not target_item.is_fixed_asset and not target_item.is_stock_item: + frappe.throw( + _("Target Item {0} is neither a Fixed Asset nor a Stock Item").format(target_item.name) + ) + + if self.entry_type == "Capitalization" and not target_item.is_fixed_asset: + frappe.throw(_("Target Item {0} must be a Fixed Asset item").format(target_item.name)) + elif self.entry_type == "Decapitalization" and not target_item.is_stock_item: + frappe.throw(_("Target Item {0} must be a Stock Item").format(target_item.name)) + + if target_item.is_fixed_asset: + self.target_qty = 1 + if flt(self.target_qty) <= 0: + frappe.throw(_("Target Qty must be a positive number")) + + if not target_item.is_stock_item: + self.target_warehouse = None + if not target_item.is_fixed_asset: + self.target_asset = None + self.target_fixed_asset_account = None + if not target_item.has_batch_no: + self.target_batch_no = None + if not target_item.has_serial_no: + self.target_serial_no = "" + + if target_item.is_stock_item and not self.target_warehouse: + frappe.throw(_("Target Warehouse is mandatory for Decapitalization")) + + self.validate_item(target_item) + + def validate_target_asset(self): + if self.target_asset: + target_asset = self.get_asset_for_validation(self.target_asset) + + if target_asset.item_code != self.target_item_code: + frappe.throw( + _("Asset {0} does not belong to Item {1}").format(self.target_asset, self.target_item_code) + ) + + self.validate_asset(target_asset) + + def validate_consumed_stock_item(self): + for d in self.stock_items: + if d.item_code: + item = frappe.get_cached_doc("Item", d.item_code) + + if not item.is_stock_item: + frappe.throw(_("Row #{0}: Item {1} is not a stock item").format(d.idx, d.item_code)) + + if flt(d.stock_qty) <= 0: + frappe.throw(_("Row #{0}: Qty must be a positive number").format(d.idx)) + + self.validate_item(item) + + def validate_consumed_asset_item(self): + for d in self.asset_items: + if d.asset: + if d.asset == self.target_asset: + frappe.throw( + _("Row #{0}: Consumed Asset {1} cannot be the same as the Target Asset").format( + d.idx, d.asset + ) + ) + + asset = self.get_asset_for_validation(d.asset) + self.validate_asset(asset) + + def validate_service_item(self): + for d in self.service_items: + if d.item_code: + item = frappe.get_cached_doc("Item", d.item_code) + + if item.is_stock_item or item.is_fixed_asset: + frappe.throw(_("Row #{0}: Item {1} is not a service item").format(d.idx, d.item_code)) + + if flt(d.qty) <= 0: + frappe.throw(_("Row #{0}: Qty must be a positive number").format(d.idx)) + + if flt(d.rate) <= 0: + frappe.throw(_("Row #{0}: Amount must be a positive number").format(d.idx)) + + self.validate_item(item) + + if not d.cost_center: + d.cost_center = frappe.get_cached_value("Company", self.company, "cost_center") + + def validate_source_mandatory(self): + if not self.target_is_fixed_asset and not self.get("asset_items"): + frappe.throw(_("Consumed Asset Items is mandatory for Decapitalization")) + + if not self.get("stock_items") and not self.get("asset_items"): + frappe.throw(_("Consumed Stock Items or Consumed Asset Items is mandatory for Capitalization")) + + def validate_item(self, item): + from erpnext.stock.doctype.item.item import validate_end_of_life + + validate_end_of_life(item.name, item.end_of_life, item.disabled) + + def get_asset_for_validation(self, asset): + return frappe.db.get_value( + "Asset", asset, ["name", "item_code", "company", "status", "docstatus"], as_dict=1 + ) + + def validate_asset(self, asset): + if asset.status in ("Draft", "Scrapped", "Sold", "Capitalized", "Decapitalized"): + frappe.throw(_("Asset {0} is {1}").format(asset.name, asset.status)) + + if asset.docstatus == 0: + frappe.throw(_("Asset {0} is Draft").format(asset.name)) + if asset.docstatus == 2: + frappe.throw(_("Asset {0} is cancelled").format(asset.name)) + + if asset.company != self.company: + frappe.throw(_("Asset {0} does not belong to company {1}").format(asset.name, self.company)) + + @frappe.whitelist() + def set_warehouse_details(self): + for d in self.get("stock_items"): + if d.item_code and d.warehouse: + args = self.get_args_for_incoming_rate(d) + warehouse_details = get_warehouse_details(args) + d.update(warehouse_details) + + @frappe.whitelist() + def set_asset_values(self): + for d in self.get("asset_items"): + if d.asset: + finance_book = d.get("finance_book") or self.get("finance_book") + d.current_asset_value = flt( + get_asset_value_after_depreciation(d.asset, finance_book=finance_book) + ) + d.asset_value = get_value_after_depreciation_on_disposal_date( + d.asset, self.posting_date, finance_book=finance_book + ) + + def get_args_for_incoming_rate(self, item): + return frappe._dict( + { + "item_code": item.item_code, + "warehouse": item.warehouse, + "posting_date": self.posting_date, + "posting_time": self.posting_time, + "qty": -1 * flt(item.stock_qty), + "serial_no": item.serial_no, + "batch_no": item.batch_no, + "voucher_type": self.doctype, + "voucher_no": self.name, + "company": self.company, + "allow_zero_valuation": cint(item.get("allow_zero_valuation_rate")), + } + ) + + def calculate_totals(self): + self.stock_items_total = 0 + self.asset_items_total = 0 + self.service_items_total = 0 + + for d in self.stock_items: + d.amount = flt(flt(d.stock_qty) * flt(d.valuation_rate), d.precision("amount")) + self.stock_items_total += d.amount + + for d in self.asset_items: + d.asset_value = flt(flt(d.asset_value), d.precision("asset_value")) + self.asset_items_total += d.asset_value + + for d in self.service_items: + d.amount = flt(flt(d.qty) * flt(d.rate), d.precision("amount")) + self.service_items_total += d.amount + + self.stock_items_total = flt(self.stock_items_total, self.precision("stock_items_total")) + self.asset_items_total = flt(self.asset_items_total, self.precision("asset_items_total")) + self.service_items_total = flt(self.service_items_total, self.precision("service_items_total")) + + self.total_value = self.stock_items_total + self.asset_items_total + self.service_items_total + self.total_value = flt(self.total_value, self.precision("total_value")) + + self.target_qty = flt(self.target_qty, self.precision("target_qty")) + self.target_incoming_rate = self.total_value / self.target_qty + + def update_stock_ledger(self): + sl_entries = [] + + for d in self.stock_items: + sle = self.get_sl_entries( + d, + { + "actual_qty": -flt(d.stock_qty), + }, + ) + sl_entries.append(sle) + + if self.entry_type == "Decapitalization" and not self.target_is_fixed_asset: + sle = self.get_sl_entries( + self, + { + "item_code": self.target_item_code, + "warehouse": self.target_warehouse, + "batch_no": self.target_batch_no, + "serial_no": self.target_serial_no, + "actual_qty": flt(self.target_qty), + "incoming_rate": flt(self.target_incoming_rate), + }, + ) + sl_entries.append(sle) + + # reverse sl entries if cancel + if self.docstatus == 2: + sl_entries.reverse() + + if sl_entries: + self.make_sl_entries(sl_entries) + + def make_gl_entries(self, gl_entries=None, from_repost=False): + from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries + + if self.docstatus == 1: + if not gl_entries: + gl_entries = self.get_gl_entries() + + if gl_entries: + make_gl_entries(gl_entries, from_repost=from_repost) + elif self.docstatus == 2: + make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) + + def get_gl_entries( + self, warehouse_account=None, default_expense_account=None, default_cost_center=None + ): + # Stock GL Entries + gl_entries = [] + + self.warehouse_account = warehouse_account + if not self.warehouse_account: + self.warehouse_account = get_warehouse_account_map(self.company) + + precision = self.get_debit_field_precision() + self.sle_map = self.get_stock_ledger_details() + + target_account = self.get_target_account() + target_against = set() + + self.get_gl_entries_for_consumed_stock_items( + gl_entries, target_account, target_against, precision + ) + self.get_gl_entries_for_consumed_asset_items( + gl_entries, target_account, target_against, precision + ) + self.get_gl_entries_for_consumed_service_items( + gl_entries, target_account, target_against, precision + ) + + self.get_gl_entries_for_target_item(gl_entries, target_against, precision) + return gl_entries + + def get_target_account(self): + if self.target_is_fixed_asset: + return self.target_fixed_asset_account + else: + return self.warehouse_account[self.target_warehouse]["account"] + + def get_gl_entries_for_consumed_stock_items( + self, gl_entries, target_account, target_against, precision + ): + # Consumed Stock Items + for item_row in self.stock_items: + sle_list = self.sle_map.get(item_row.name) + if sle_list: + for sle in sle_list: + stock_value_difference = flt(sle.stock_value_difference, precision) + + if erpnext.is_perpetual_inventory_enabled(self.company): + account = self.warehouse_account[sle.warehouse]["account"] + else: + account = self.get_company_default("default_expense_account") + + target_against.add(account) + gl_entries.append( + self.get_gl_dict( + { + "account": account, + "against": target_account, + "cost_center": item_row.cost_center, + "project": item_row.get("project") or self.get("project"), + "remarks": self.get("remarks") or "Accounting Entry for Stock", + "credit": -1 * stock_value_difference, + }, + self.warehouse_account[sle.warehouse]["account_currency"], + item=item_row, + ) + ) + + def get_gl_entries_for_consumed_asset_items( + self, gl_entries, target_account, target_against, precision + ): + # Consumed Assets + for item in self.asset_items: + asset = self.get_asset(item) + + if asset.calculate_depreciation: + notes = _( + "This schedule was created when Asset {0} was consumed through Asset Capitalization {1}." + ).format( + get_link_to_form(asset.doctype, asset.name), get_link_to_form(self.doctype, self.get("name")) + ) + depreciate_asset(asset, self.posting_date, notes) + asset.reload() + + fixed_asset_gl_entries = get_gl_entries_on_asset_disposal( + asset, + item.asset_value, + item.get("finance_book") or self.get("finance_book"), + self.get("doctype"), + self.get("name"), + ) + + asset.db_set("disposal_date", self.posting_date) + + self.set_consumed_asset_status(asset) + + for gle in fixed_asset_gl_entries: + gle["against"] = target_account + gl_entries.append(self.get_gl_dict(gle, item=item)) + target_against.add(gle["account"]) + + def get_gl_entries_for_consumed_service_items( + self, gl_entries, target_account, target_against, precision + ): + # Service Expenses + for item_row in self.service_items: + expense_amount = flt(item_row.amount, precision) + target_against.add(item_row.expense_account) + + gl_entries.append( + self.get_gl_dict( + { + "account": item_row.expense_account, + "against": target_account, + "cost_center": item_row.cost_center, + "project": item_row.get("project") or self.get("project"), + "remarks": self.get("remarks") or "Accounting Entry for Stock", + "credit": expense_amount, + }, + item=item_row, + ) + ) + + def get_gl_entries_for_target_item(self, gl_entries, target_against, precision): + if self.target_is_fixed_asset: + # Capitalization + gl_entries.append( + self.get_gl_dict( + { + "account": self.target_fixed_asset_account, + "against": ", ".join(target_against), + "remarks": self.get("remarks") or _("Accounting Entry for Asset"), + "debit": flt(self.total_value, precision), + "cost_center": self.get("cost_center"), + }, + item=self, + ) + ) + else: + # Target Stock Item + sle_list = self.sle_map.get(self.name) + for sle in sle_list: + stock_value_difference = flt(sle.stock_value_difference, precision) + account = self.warehouse_account[sle.warehouse]["account"] + + gl_entries.append( + self.get_gl_dict( + { + "account": account, + "against": ", ".join(target_against), + "cost_center": self.cost_center, + "project": self.get("project"), + "remarks": self.get("remarks") or "Accounting Entry for Stock", + "debit": stock_value_difference, + }, + self.warehouse_account[sle.warehouse]["account_currency"], + item=self, + ) + ) + + def update_target_asset(self): + total_target_asset_value = flt(self.total_value, self.precision("total_value")) + if self.docstatus == 1 and self.entry_type == "Capitalization": + asset_doc = frappe.get_doc("Asset", self.target_asset) + asset_doc.purchase_date = self.posting_date + asset_doc.gross_purchase_amount = total_target_asset_value + asset_doc.purchase_receipt_amount = total_target_asset_value + notes = _( + "This schedule was created when target Asset {0} was updated through Asset Capitalization {1}." + ).format( + get_link_to_form(asset_doc.doctype, asset_doc.name), get_link_to_form(self.doctype, self.name) + ) + make_new_active_asset_depr_schedules_and_cancel_current_ones(asset_doc, notes) + asset_doc.flags.ignore_validate_update_after_submit = True + asset_doc.save() + elif self.docstatus == 2: + for item in self.asset_items: + asset = self.get_asset(item) + asset.db_set("disposal_date", None) + self.set_consumed_asset_status(asset) + + if asset.calculate_depreciation: + reverse_depreciation_entry_made_after_disposal(asset, self.posting_date) + notes = _( + "This schedule was created when Asset {0} was restored on Asset Capitalization {1}'s cancellation." + ).format( + get_link_to_form(asset.doctype, asset.name), get_link_to_form(self.doctype, self.name) + ) + reset_depreciation_schedule(asset, self.posting_date, notes) + + def get_asset(self, item): + asset = frappe.get_doc("Asset", item.asset) + self.check_finance_books(item, asset) + return asset + + def set_consumed_asset_status(self, asset): + if self.docstatus == 1: + asset.set_status("Capitalized" if self.target_is_fixed_asset else "Decapitalized") + else: + asset.set_status() + + +@frappe.whitelist() +def get_target_item_details(item_code=None, company=None): + out = frappe._dict() + + # Get Item Details + item = frappe._dict() + if item_code: + item = frappe.get_cached_doc("Item", item_code) + + # Set Item Details + out.target_item_name = item.item_name + out.target_stock_uom = item.stock_uom + out.target_is_fixed_asset = cint(item.is_fixed_asset) + out.target_has_batch_no = cint(item.has_batch_no) + out.target_has_serial_no = cint(item.has_serial_no) + + if out.target_is_fixed_asset: + out.target_qty = 1 + out.target_warehouse = None + else: + out.target_asset = None + + if not out.target_has_batch_no: + out.target_batch_no = None + if not out.target_has_serial_no: + out.target_serial_no = "" + + # Cost Center + item_defaults = get_item_defaults(item.name, company) + item_group_defaults = get_item_group_defaults(item.name, company) + brand_defaults = get_brand_defaults(item.name, company) + out.cost_center = get_default_cost_center( + frappe._dict({"item_code": item.name, "company": company}), + item_defaults, + item_group_defaults, + brand_defaults, + ) + + return out + + +@frappe.whitelist() +def get_target_asset_details(asset=None, company=None): + out = frappe._dict() + + # Get Asset Details + asset_details = frappe._dict() + if asset: + asset_details = frappe.db.get_value("Asset", asset, ["asset_name", "item_code"], as_dict=1) + if not asset_details: + frappe.throw(_("Asset {0} does not exist").format(asset)) + + # Re-set item code from Asset + out.target_item_code = asset_details.item_code + + # Set Asset Details + out.asset_name = asset_details.asset_name + + if asset_details.item_code: + out.target_fixed_asset_account = get_asset_category_account( + "fixed_asset_account", item=asset_details.item_code, company=company + ) + else: + out.target_fixed_asset_account = None + + return out + + +@frappe.whitelist() +def get_consumed_stock_item_details(args): + if isinstance(args, str): + args = json.loads(args) + + args = frappe._dict(args) + out = frappe._dict() + + item = frappe._dict() + if args.item_code: + item = frappe.get_cached_doc("Item", args.item_code) + + out.item_name = item.item_name + out.batch_no = None + out.serial_no = "" + + out.stock_qty = flt(args.stock_qty) or 1 + out.stock_uom = item.stock_uom + + out.warehouse = get_item_warehouse(item, args, overwrite_warehouse=True) if item else None + + # Cost Center + item_defaults = get_item_defaults(item.name, args.company) + item_group_defaults = get_item_group_defaults(item.name, args.company) + brand_defaults = get_brand_defaults(item.name, args.company) + out.cost_center = get_default_cost_center( + args, item_defaults, item_group_defaults, brand_defaults + ) + + if args.item_code and out.warehouse: + incoming_rate_args = frappe._dict( + { + "item_code": args.item_code, + "warehouse": out.warehouse, + "posting_date": args.posting_date, + "posting_time": args.posting_time, + "qty": -1 * flt(out.stock_qty), + "voucher_type": args.doctype, + "voucher_no": args.name, + "company": args.company, + "serial_no": args.serial_no, + "batch_no": args.batch_no, + } + ) + out.update(get_warehouse_details(incoming_rate_args)) + else: + out.valuation_rate = 0 + out.actual_qty = 0 + + return out + + +@frappe.whitelist() +def get_warehouse_details(args): + if isinstance(args, str): + args = json.loads(args) + + args = frappe._dict(args) + + out = {} + if args.warehouse and args.item_code: + out = { + "actual_qty": get_previous_sle(args).get("qty_after_transaction") or 0, + "valuation_rate": get_incoming_rate(args, raise_error_if_no_rate=False), + } + return out + + +@frappe.whitelist() +def get_consumed_asset_details(args): + if isinstance(args, str): + args = json.loads(args) + + args = frappe._dict(args) + out = frappe._dict() + + asset_details = frappe._dict() + if args.asset: + asset_details = frappe.db.get_value( + "Asset", args.asset, ["asset_name", "item_code", "item_name"], as_dict=1 + ) + if not asset_details: + frappe.throw(_("Asset {0} does not exist").format(args.asset)) + + out.item_code = asset_details.item_code + out.asset_name = asset_details.asset_name + out.item_name = asset_details.item_name + + if args.asset: + out.current_asset_value = flt( + get_asset_value_after_depreciation(args.asset, finance_book=args.finance_book) + ) + out.asset_value = get_value_after_depreciation_on_disposal_date( + args.asset, args.posting_date, finance_book=args.finance_book + ) + else: + out.current_asset_value = 0 + out.asset_value = 0 + + # Account + if asset_details.item_code: + out.fixed_asset_account = get_asset_category_account( + "fixed_asset_account", item=asset_details.item_code, company=args.company + ) + else: + out.fixed_asset_account = None + + # Cost Center + if asset_details.item_code: + item = frappe.get_cached_doc("Item", asset_details.item_code) + item_defaults = get_item_defaults(item.name, args.company) + item_group_defaults = get_item_group_defaults(item.name, args.company) + brand_defaults = get_brand_defaults(item.name, args.company) + out.cost_center = get_default_cost_center( + args, item_defaults, item_group_defaults, brand_defaults + ) + + return out + + +@frappe.whitelist() +def get_service_item_details(args): + if isinstance(args, str): + args = json.loads(args) + + args = frappe._dict(args) + out = frappe._dict() + + item = frappe._dict() + if args.item_code: + item = frappe.get_cached_doc("Item", args.item_code) + + out.item_name = item.item_name + out.qty = flt(args.qty) or 1 + out.uom = item.purchase_uom or item.stock_uom + + item_defaults = get_item_defaults(item.name, args.company) + item_group_defaults = get_item_group_defaults(item.name, args.company) + brand_defaults = get_brand_defaults(item.name, args.company) + + out.expense_account = get_default_expense_account( + args, item_defaults, item_group_defaults, brand_defaults + ) + out.cost_center = get_default_cost_center( + args, item_defaults, item_group_defaults, brand_defaults + ) + + return out diff --git a/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py new file mode 100644 index 0000000000..4d519a60be --- /dev/null +++ b/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py @@ -0,0 +1,510 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import unittest + +import frappe +from frappe.utils import cint, flt, getdate, now_datetime + +from erpnext.assets.doctype.asset.depreciation import post_depreciation_entries +from erpnext.assets.doctype.asset.test_asset import ( + create_asset, + create_asset_data, + set_depreciation_settings_in_company, +) +from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import ( + get_asset_depr_schedule_doc, +) +from erpnext.stock.doctype.item.test_item import create_item + + +class TestAssetCapitalization(unittest.TestCase): + def setUp(self): + set_depreciation_settings_in_company() + create_asset_data() + create_asset_capitalization_data() + frappe.db.sql("delete from `tabTax Rule`") + + def test_capitalization_with_perpetual_inventory(self): + company = "_Test Company with perpetual inventory" + set_depreciation_settings_in_company(company=company) + + # Variables + consumed_asset_value = 100000 + + stock_rate = 1000 + stock_qty = 2 + stock_amount = 2000 + + service_rate = 500 + service_qty = 2 + service_amount = 1000 + + total_amount = 103000 + + # Create assets + target_asset = create_asset( + asset_name="Asset Capitalization Target Asset", + submit=1, + warehouse="Stores - TCP1", + company=company, + ) + consumed_asset = create_asset( + asset_name="Asset Capitalization Consumable Asset", + asset_value=consumed_asset_value, + submit=1, + warehouse="Stores - TCP1", + company=company, + ) + + # Create and submit Asset Captitalization + asset_capitalization = create_asset_capitalization( + entry_type="Capitalization", + target_asset=target_asset.name, + stock_qty=stock_qty, + stock_rate=stock_rate, + consumed_asset=consumed_asset.name, + service_qty=service_qty, + service_rate=service_rate, + service_expense_account="Expenses Included In Asset Valuation - TCP1", + company=company, + submit=1, + ) + + # Test Asset Capitalization values + self.assertEqual(asset_capitalization.entry_type, "Capitalization") + self.assertEqual(asset_capitalization.target_qty, 1) + + self.assertEqual(asset_capitalization.stock_items[0].valuation_rate, stock_rate) + self.assertEqual(asset_capitalization.stock_items[0].amount, stock_amount) + self.assertEqual(asset_capitalization.stock_items_total, stock_amount) + + self.assertEqual(asset_capitalization.asset_items[0].asset_value, consumed_asset_value) + self.assertEqual(asset_capitalization.asset_items_total, consumed_asset_value) + + self.assertEqual(asset_capitalization.service_items[0].amount, service_amount) + self.assertEqual(asset_capitalization.service_items_total, service_amount) + + self.assertEqual(asset_capitalization.total_value, total_amount) + self.assertEqual(asset_capitalization.target_incoming_rate, total_amount) + + # Test Target Asset values + target_asset.reload() + self.assertEqual(target_asset.gross_purchase_amount, total_amount) + self.assertEqual(target_asset.purchase_receipt_amount, total_amount) + + # Test Consumed Asset values + self.assertEqual(consumed_asset.db_get("status"), "Capitalized") + + # Test General Ledger Entries + expected_gle = { + "_Test Fixed Asset - TCP1": 3000, + "Expenses Included In Asset Valuation - TCP1": -1000, + "_Test Warehouse - TCP1": -2000, + } + actual_gle = get_actual_gle_dict(asset_capitalization.name) + + self.assertEqual(actual_gle, expected_gle) + + # Test Stock Ledger Entries + expected_sle = { + ("Capitalization Source Stock Item", "_Test Warehouse - TCP1"): { + "actual_qty": -stock_qty, + "stock_value_difference": -stock_amount, + } + } + actual_sle = get_actual_sle_dict(asset_capitalization.name) + self.assertEqual(actual_sle, expected_sle) + + # Cancel Asset Capitalization and make test entries and status are reversed + asset_capitalization.cancel() + self.assertEqual(consumed_asset.db_get("status"), "Submitted") + self.assertFalse(get_actual_gle_dict(asset_capitalization.name)) + self.assertFalse(get_actual_sle_dict(asset_capitalization.name)) + + def test_capitalization_with_periodical_inventory(self): + company = "_Test Company" + # Variables + consumed_asset_value = 100000 + + stock_rate = 1000 + stock_qty = 2 + stock_amount = 2000 + + service_rate = 500 + service_qty = 2 + service_amount = 1000 + + total_amount = 103000 + + # Create assets + target_asset = create_asset( + asset_name="Asset Capitalization Target Asset", + submit=1, + warehouse="Stores - _TC", + company=company, + ) + consumed_asset = create_asset( + asset_name="Asset Capitalization Consumable Asset", + asset_value=consumed_asset_value, + submit=1, + warehouse="Stores - _TC", + company=company, + ) + + # Create and submit Asset Captitalization + asset_capitalization = create_asset_capitalization( + entry_type="Capitalization", + target_asset=target_asset.name, + stock_qty=stock_qty, + stock_rate=stock_rate, + consumed_asset=consumed_asset.name, + service_qty=service_qty, + service_rate=service_rate, + service_expense_account="Expenses Included In Asset Valuation - _TC", + company=company, + submit=1, + ) + + # Test Asset Capitalization values + self.assertEqual(asset_capitalization.entry_type, "Capitalization") + self.assertEqual(asset_capitalization.target_qty, 1) + + self.assertEqual(asset_capitalization.stock_items[0].valuation_rate, stock_rate) + self.assertEqual(asset_capitalization.stock_items[0].amount, stock_amount) + self.assertEqual(asset_capitalization.stock_items_total, stock_amount) + + self.assertEqual(asset_capitalization.asset_items[0].asset_value, consumed_asset_value) + self.assertEqual(asset_capitalization.asset_items_total, consumed_asset_value) + + self.assertEqual(asset_capitalization.service_items[0].amount, service_amount) + self.assertEqual(asset_capitalization.service_items_total, service_amount) + + self.assertEqual(asset_capitalization.total_value, total_amount) + self.assertEqual(asset_capitalization.target_incoming_rate, total_amount) + + # Test Target Asset values + target_asset.reload() + self.assertEqual(target_asset.gross_purchase_amount, total_amount) + self.assertEqual(target_asset.purchase_receipt_amount, total_amount) + + # Test Consumed Asset values + self.assertEqual(consumed_asset.db_get("status"), "Capitalized") + + # Test General Ledger Entries + default_expense_account = frappe.db.get_value("Company", company, "default_expense_account") + expected_gle = { + "_Test Fixed Asset - _TC": 3000, + "Expenses Included In Asset Valuation - _TC": -1000, + default_expense_account: -2000, + } + actual_gle = get_actual_gle_dict(asset_capitalization.name) + + self.assertEqual(actual_gle, expected_gle) + + # Test Stock Ledger Entries + expected_sle = { + ("Capitalization Source Stock Item", "_Test Warehouse - _TC"): { + "actual_qty": -stock_qty, + "stock_value_difference": -stock_amount, + } + } + actual_sle = get_actual_sle_dict(asset_capitalization.name) + self.assertEqual(actual_sle, expected_sle) + + # Cancel Asset Capitalization and make test entries and status are reversed + asset_capitalization.cancel() + self.assertEqual(consumed_asset.db_get("status"), "Submitted") + self.assertFalse(get_actual_gle_dict(asset_capitalization.name)) + self.assertFalse(get_actual_sle_dict(asset_capitalization.name)) + + def test_decapitalization_with_depreciation(self): + # Variables + purchase_date = "2020-01-01" + depreciation_start_date = "2020-12-31" + capitalization_date = "2021-06-30" + + total_number_of_depreciations = 3 + expected_value_after_useful_life = 10_000 + consumed_asset_purchase_value = 100_000 + consumed_asset_current_value = 70_000 + consumed_asset_value_before_disposal = 55_000 + + target_qty = 10 + target_incoming_rate = 5500 + + depreciation_before_disposal_amount = 15_000 + accumulated_depreciation = 45_000 + + # to accomodate for depreciation on disposal calculation minor difference + consumed_asset_value_before_disposal = 55_123.29 + target_incoming_rate = 5512.329 + depreciation_before_disposal_amount = 14_876.71 + accumulated_depreciation = 44_876.71 + + # Create assets + consumed_asset = create_depreciation_asset( + asset_name="Asset Capitalization Consumable Asset", + asset_value=consumed_asset_purchase_value, + purchase_date=purchase_date, + depreciation_start_date=depreciation_start_date, + depreciation_method="Straight Line", + total_number_of_depreciations=total_number_of_depreciations, + frequency_of_depreciation=12, + expected_value_after_useful_life=expected_value_after_useful_life, + company="_Test Company with perpetual inventory", + submit=1, + ) + + first_asset_depr_schedule = get_asset_depr_schedule_doc(consumed_asset.name, "Active") + self.assertEquals(first_asset_depr_schedule.status, "Active") + + # Create and submit Asset Captitalization + asset_capitalization = create_asset_capitalization( + entry_type="Decapitalization", + posting_date=capitalization_date, # half a year + target_item_code="Capitalization Target Stock Item", + target_qty=target_qty, + consumed_asset=consumed_asset.name, + company="_Test Company with perpetual inventory", + submit=1, + ) + + # Test Asset Capitalization values + self.assertEqual(asset_capitalization.entry_type, "Decapitalization") + + self.assertEqual( + asset_capitalization.asset_items[0].current_asset_value, consumed_asset_current_value + ) + self.assertEqual( + asset_capitalization.asset_items[0].asset_value, consumed_asset_value_before_disposal + ) + self.assertEqual(asset_capitalization.asset_items_total, consumed_asset_value_before_disposal) + + self.assertEqual(asset_capitalization.total_value, consumed_asset_value_before_disposal) + self.assertEqual(asset_capitalization.target_incoming_rate, target_incoming_rate) + + # Test Consumed Asset values + consumed_asset.reload() + self.assertEqual(consumed_asset.status, "Decapitalized") + + first_asset_depr_schedule.load_from_db() + + second_asset_depr_schedule = get_asset_depr_schedule_doc(consumed_asset.name, "Active") + self.assertEquals(second_asset_depr_schedule.status, "Active") + self.assertEquals(first_asset_depr_schedule.status, "Cancelled") + + depr_schedule_of_consumed_asset = second_asset_depr_schedule.get("depreciation_schedule") + + consumed_depreciation_schedule = [ + d + for d in depr_schedule_of_consumed_asset + if getdate(d.schedule_date) == getdate(capitalization_date) + ] + self.assertTrue( + consumed_depreciation_schedule and consumed_depreciation_schedule[0].journal_entry + ) + self.assertEqual( + consumed_depreciation_schedule[0].depreciation_amount, depreciation_before_disposal_amount + ) + + # Test General Ledger Entries + expected_gle = { + "_Test Warehouse - TCP1": consumed_asset_value_before_disposal, + "_Test Accumulated Depreciations - TCP1": accumulated_depreciation, + "_Test Fixed Asset - TCP1": -consumed_asset_purchase_value, + } + actual_gle = get_actual_gle_dict(asset_capitalization.name) + self.assertEqual(actual_gle, expected_gle) + + # Cancel Asset Capitalization and make test entries and status are reversed + asset_capitalization.reload() + asset_capitalization.cancel() + self.assertEqual(consumed_asset.db_get("status"), "Partially Depreciated") + self.assertFalse(get_actual_gle_dict(asset_capitalization.name)) + self.assertFalse(get_actual_sle_dict(asset_capitalization.name)) + + +def create_asset_capitalization_data(): + create_item( + "Capitalization Target Stock Item", is_stock_item=1, is_fixed_asset=0, is_purchase_item=0 + ) + create_item( + "Capitalization Source Stock Item", is_stock_item=1, is_fixed_asset=0, is_purchase_item=0 + ) + create_item( + "Capitalization Source Service Item", is_stock_item=0, is_fixed_asset=0, is_purchase_item=0 + ) + + +def create_asset_capitalization(**args): + from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + + args = frappe._dict(args) + + now = now_datetime() + target_asset = frappe.get_doc("Asset", args.target_asset) if args.target_asset else frappe._dict() + target_item_code = target_asset.item_code or args.target_item_code + company = target_asset.company or args.company or "_Test Company" + warehouse = args.warehouse or create_warehouse("_Test Warehouse", company=company) + target_warehouse = args.target_warehouse or warehouse + source_warehouse = args.source_warehouse or warehouse + + asset_capitalization = frappe.new_doc("Asset Capitalization") + asset_capitalization.update( + { + "entry_type": args.entry_type or "Capitalization", + "company": company, + "posting_date": args.posting_date or now.strftime("%Y-%m-%d"), + "posting_time": args.posting_time or now.strftime("%H:%M:%S.%f"), + "target_item_code": target_item_code, + "target_asset": target_asset.name, + "target_warehouse": target_warehouse, + "target_qty": flt(args.target_qty) or 1, + "target_batch_no": args.target_batch_no, + "target_serial_no": args.target_serial_no, + "finance_book": args.finance_book, + } + ) + + if args.posting_date or args.posting_time: + asset_capitalization.set_posting_time = 1 + + if flt(args.stock_rate): + asset_capitalization.append( + "stock_items", + { + "item_code": args.stock_item or "Capitalization Source Stock Item", + "warehouse": source_warehouse, + "stock_qty": flt(args.stock_qty) or 1, + "batch_no": args.stock_batch_no, + "serial_no": args.stock_serial_no, + }, + ) + + if args.consumed_asset: + asset_capitalization.append( + "asset_items", + { + "asset": args.consumed_asset, + }, + ) + + if flt(args.service_rate): + asset_capitalization.append( + "service_items", + { + "item_code": args.service_item or "Capitalization Source Service Item", + "expense_account": args.service_expense_account, + "qty": flt(args.service_qty) or 1, + "rate": flt(args.service_rate), + }, + ) + + if args.submit: + create_stock_reconciliation(asset_capitalization, stock_rate=args.stock_rate) + + asset_capitalization.insert() + + if args.submit: + asset_capitalization.submit() + + return asset_capitalization + + +def create_stock_reconciliation(asset_capitalization, stock_rate=0): + from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( + EmptyStockReconciliationItemsError, + create_stock_reconciliation, + ) + + if not asset_capitalization.get("stock_items"): + return + + try: + create_stock_reconciliation( + item_code=asset_capitalization.stock_items[0].item_code, + warehouse=asset_capitalization.stock_items[0].warehouse, + qty=flt(asset_capitalization.stock_items[0].stock_qty), + rate=flt(stock_rate), + company=asset_capitalization.company, + ) + except EmptyStockReconciliationItemsError: + pass + + +def create_depreciation_asset(**args): + args = frappe._dict(args) + + asset = frappe.new_doc("Asset") + asset.is_existing_asset = 1 + asset.calculate_depreciation = 1 + asset.asset_owner = "Company" + + asset.company = args.company or "_Test Company" + asset.item_code = args.item_code or "Macbook Pro" + asset.asset_name = args.asset_name or asset.item_code + asset.location = args.location or "Test Location" + + asset.purchase_date = args.purchase_date or "2020-01-01" + asset.available_for_use_date = args.available_for_use_date or asset.purchase_date + + asset.gross_purchase_amount = args.asset_value or 100000 + asset.purchase_receipt_amount = asset.gross_purchase_amount + + finance_book = asset.append("finance_books") + finance_book.depreciation_start_date = args.depreciation_start_date or "2020-12-31" + finance_book.depreciation_method = args.depreciation_method or "Straight Line" + finance_book.total_number_of_depreciations = cint(args.total_number_of_depreciations) or 3 + finance_book.frequency_of_depreciation = cint(args.frequency_of_depreciation) or 12 + finance_book.expected_value_after_useful_life = flt(args.expected_value_after_useful_life) + + if args.submit: + asset.submit() + + frappe.db.set_value("Company", "_Test Company", "series_for_depreciation_entry", "DEPR-") + post_depreciation_entries(date=finance_book.depreciation_start_date) + asset.load_from_db() + + return asset + + +def get_actual_gle_dict(name): + return dict( + frappe.db.sql( + """ + select account, sum(debit-credit) as diff + from `tabGL Entry` + where voucher_type = 'Asset Capitalization' and voucher_no = %s + group by account + having diff != 0 + """, + name, + ) + ) + + +def get_actual_sle_dict(name): + sles = frappe.db.sql( + """ + select + item_code, warehouse, + sum(actual_qty) as actual_qty, + sum(stock_value_difference) as stock_value_difference + from `tabStock Ledger Entry` + where voucher_type = 'Asset Capitalization' and voucher_no = %s + group by item_code, warehouse + having actual_qty != 0 + """, + name, + as_dict=1, + ) + + sle_dict = {} + for d in sles: + sle_dict[(d.item_code, d.warehouse)] = { + "actual_qty": d.actual_qty, + "stock_value_difference": d.stock_value_difference, + } + + return sle_dict diff --git a/erpnext/regional/doctype/product_tax_category/__init__.py b/erpnext/assets/doctype/asset_capitalization_asset_item/__init__.py similarity index 100% rename from erpnext/regional/doctype/product_tax_category/__init__.py rename to erpnext/assets/doctype/asset_capitalization_asset_item/__init__.py diff --git a/erpnext/assets/doctype/asset_capitalization_asset_item/asset_capitalization_asset_item.json b/erpnext/assets/doctype/asset_capitalization_asset_item/asset_capitalization_asset_item.json new file mode 100644 index 0000000000..ebaaffbad1 --- /dev/null +++ b/erpnext/assets/doctype/asset_capitalization_asset_item/asset_capitalization_asset_item.json @@ -0,0 +1,128 @@ +{ + "actions": [], + "creation": "2021-09-05 15:52:10.124538", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "asset", + "asset_name", + "finance_book", + "column_break_3", + "item_code", + "item_name", + "section_break_6", + "current_asset_value", + "asset_value", + "column_break_9", + "accounting_dimensions_section", + "cost_center", + "dimension_col_break", + "fixed_asset_account" + ], + "fields": [ + { + "fieldname": "asset", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Asset", + "options": "Asset", + "reqd": 1 + }, + { + "fetch_from": "asset.asset_name", + "fieldname": "asset_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Asset Name", + "read_only": 1 + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fetch_from": "asset.item_code", + "fieldname": "item_code", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Item Code", + "options": "Item", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "item_name", + "fieldtype": "Data", + "label": "Item Name", + "read_only": 1 + }, + { + "fieldname": "section_break_6", + "fieldtype": "Section Break", + "label": "Value" + }, + { + "default": "0", + "fieldname": "asset_value", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Asset Value", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "column_break_9", + "fieldtype": "Column Break" + }, + { + "fieldname": "fixed_asset_account", + "fieldtype": "Link", + "label": "Fixed Asset Account", + "options": "Account", + "read_only": 1 + }, + { + "fieldname": "cost_center", + "fieldtype": "Link", + "label": "Cost Center", + "options": "Cost Center" + }, + { + "collapsible": 1, + "fieldname": "accounting_dimensions_section", + "fieldtype": "Section Break", + "label": "Accounting Dimensions" + }, + { + "fieldname": "dimension_col_break", + "fieldtype": "Column Break" + }, + { + "fieldname": "finance_book", + "fieldtype": "Link", + "label": "Finance Book", + "options": "Finance Book" + }, + { + "fieldname": "current_asset_value", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Current Asset Value", + "options": "Company:company:default_currency", + "read_only": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-09-12 14:30:02.915132", + "modified_by": "Administrator", + "module": "Assets", + "name": "Asset Capitalization Asset Item", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/regional/doctype/product_tax_category/product_tax_category.py b/erpnext/assets/doctype/asset_capitalization_asset_item/asset_capitalization_asset_item.py similarity index 80% rename from erpnext/regional/doctype/product_tax_category/product_tax_category.py rename to erpnext/assets/doctype/asset_capitalization_asset_item/asset_capitalization_asset_item.py index b6be9e0920..ba356d6b9f 100644 --- a/erpnext/regional/doctype/product_tax_category/product_tax_category.py +++ b/erpnext/assets/doctype/asset_capitalization_asset_item/asset_capitalization_asset_item.py @@ -5,5 +5,5 @@ from frappe.model.document import Document -class ProductTaxCategory(Document): +class AssetCapitalizationAssetItem(Document): pass diff --git a/erpnext/regional/print_format/ksa_pos_invoice/__init__.py b/erpnext/assets/doctype/asset_capitalization_service_item/__init__.py similarity index 100% rename from erpnext/regional/print_format/ksa_pos_invoice/__init__.py rename to erpnext/assets/doctype/asset_capitalization_service_item/__init__.py diff --git a/erpnext/assets/doctype/asset_capitalization_service_item/asset_capitalization_service_item.json b/erpnext/assets/doctype/asset_capitalization_service_item/asset_capitalization_service_item.json new file mode 100644 index 0000000000..0ae1c1428e --- /dev/null +++ b/erpnext/assets/doctype/asset_capitalization_service_item/asset_capitalization_service_item.json @@ -0,0 +1,122 @@ +{ + "actions": [], + "creation": "2021-09-06 13:32:08.642060", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "item_code", + "item_name", + "column_break_3", + "expense_account", + "section_break_6", + "qty", + "uom", + "column_break_9", + "rate", + "amount", + "accounting_dimensions_section", + "cost_center", + "dimension_col_break" + ], + "fields": [ + { + "bold": 1, + "fieldname": "item_code", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Item Code", + "options": "Item" + }, + { + "fetch_from": "item_code.item_name", + "fieldname": "item_name", + "fieldtype": "Data", + "label": "Item Name", + "read_only": 1 + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "expense_account", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Expense Account", + "options": "Account", + "reqd": 1 + }, + { + "fieldname": "section_break_6", + "fieldtype": "Section Break", + "label": "Qty and Rate" + }, + { + "columns": 1, + "default": "1", + "fieldname": "qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Qty", + "non_negative": 1 + }, + { + "columns": 1, + "fetch_from": "stock_item_code.stock_uom", + "fieldname": "uom", + "fieldtype": "Link", + "in_list_view": 1, + "label": "UOM", + "options": "UOM" + }, + { + "fieldname": "column_break_9", + "fieldtype": "Column Break" + }, + { + "fieldname": "rate", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Rate", + "options": "Company:company:default_currency" + }, + { + "default": "0", + "fieldname": "amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Amount", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "collapsible": 1, + "fieldname": "accounting_dimensions_section", + "fieldtype": "Section Break", + "label": "Accounting Dimensions" + }, + { + "fieldname": "cost_center", + "fieldtype": "Link", + "label": "Cost Center", + "options": "Cost Center" + }, + { + "fieldname": "dimension_col_break", + "fieldtype": "Column Break" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-09-08 15:52:08.598100", + "modified_by": "Administrator", + "module": "Assets", + "name": "Asset Capitalization Service Item", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/assets/doctype/asset_capitalization_service_item/asset_capitalization_service_item.py b/erpnext/assets/doctype/asset_capitalization_service_item/asset_capitalization_service_item.py new file mode 100644 index 0000000000..28d018ee39 --- /dev/null +++ b/erpnext/assets/doctype/asset_capitalization_service_item/asset_capitalization_service_item.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 AssetCapitalizationServiceItem(Document): + pass diff --git a/erpnext/regional/print_format/ksa_vat_invoice/__init__.py b/erpnext/assets/doctype/asset_capitalization_stock_item/__init__.py similarity index 100% rename from erpnext/regional/print_format/ksa_vat_invoice/__init__.py rename to erpnext/assets/doctype/asset_capitalization_stock_item/__init__.py diff --git a/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.json b/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.json new file mode 100644 index 0000000000..14eb0f6ef2 --- /dev/null +++ b/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.json @@ -0,0 +1,156 @@ +{ + "actions": [], + "creation": "2021-09-05 15:23:23.492310", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "item_code", + "item_name", + "column_break_3", + "warehouse", + "section_break_6", + "stock_qty", + "stock_uom", + "actual_qty", + "column_break_9", + "valuation_rate", + "amount", + "batch_and_serial_no_section", + "batch_no", + "column_break_13", + "serial_no", + "accounting_dimensions_section", + "cost_center", + "dimension_col_break" + ], + "fields": [ + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "warehouse", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Warehouse", + "options": "Warehouse", + "reqd": 1 + }, + { + "fieldname": "batch_no", + "fieldtype": "Link", + "label": "Batch No", + "options": "Batch" + }, + { + "fieldname": "section_break_6", + "fieldtype": "Section Break", + "label": "Qty and Rate" + }, + { + "columns": 1, + "fieldname": "stock_qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Qty", + "non_negative": 1 + }, + { + "columns": 1, + "fetch_from": "stock_item_code.stock_uom", + "fieldname": "stock_uom", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Stock UOM", + "options": "UOM", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "column_break_9", + "fieldtype": "Column Break" + }, + { + "fieldname": "valuation_rate", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Valuation Rate", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "default": "0", + "fieldname": "amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Amount", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "batch_and_serial_no_section", + "fieldtype": "Section Break", + "label": "Batch and Serial No" + }, + { + "fieldname": "column_break_13", + "fieldtype": "Column Break" + }, + { + "fieldname": "serial_no", + "fieldtype": "Small Text", + "label": "Serial No" + }, + { + "fieldname": "item_code", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Item Code", + "options": "Item", + "reqd": 1 + }, + { + "fetch_from": "item_code.item_name", + "fieldname": "item_name", + "fieldtype": "Data", + "label": "Item Name", + "read_only": 1 + }, + { + "fieldname": "actual_qty", + "fieldtype": "Float", + "label": "Actual Qty in Warehouse", + "no_copy": 1, + "read_only": 1 + }, + { + "collapsible": 1, + "fieldname": "accounting_dimensions_section", + "fieldtype": "Section Break", + "label": "Accounting Dimensions" + }, + { + "fieldname": "cost_center", + "fieldtype": "Link", + "label": "Cost Center", + "options": "Cost Center" + }, + { + "fieldname": "dimension_col_break", + "fieldtype": "Column Break" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-09-08 15:56:20.230548", + "modified_by": "Administrator", + "module": "Assets", + "name": "Asset Capitalization Stock Item", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/erpnext_integrations/doctype/taxjar_nexus/taxjar_nexus.py b/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.py similarity index 80% rename from erpnext/erpnext_integrations/doctype/taxjar_nexus/taxjar_nexus.py rename to erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.py index c24aa8ca7d..5d6f98d5cf 100644 --- a/erpnext/erpnext_integrations/doctype/taxjar_nexus/taxjar_nexus.py +++ b/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.py @@ -5,5 +5,5 @@ from frappe.model.document import Document -class TaxJarNexus(Document): +class AssetCapitalizationStockItem(Document): pass diff --git a/erpnext/regional/report/ksa_vat/__init__.py b/erpnext/assets/doctype/asset_depreciation_schedule/__init__.py similarity index 100% rename from erpnext/regional/report/ksa_vat/__init__.py rename to erpnext/assets/doctype/asset_depreciation_schedule/__init__.py diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.js b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.js new file mode 100644 index 0000000000..c28b2b3b6a --- /dev/null +++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.js @@ -0,0 +1,51 @@ +// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +frappe.provide("erpnext.asset"); + +frappe.ui.form.on('Asset Depreciation Schedule', { + onload: function(frm) { + frm.events.make_schedules_editable(frm); + }, + + make_schedules_editable: function(frm) { + var is_editable = frm.doc.depreciation_method == "Manual" ? true : false; + + frm.toggle_enable("depreciation_schedule", is_editable); + frm.fields_dict["depreciation_schedule"].grid.toggle_enable("schedule_date", is_editable); + frm.fields_dict["depreciation_schedule"].grid.toggle_enable("depreciation_amount", is_editable); + } +}); + +frappe.ui.form.on('Depreciation Schedule', { + make_depreciation_entry: function(frm, cdt, cdn) { + var row = locals[cdt][cdn]; + if (!row.journal_entry) { + frappe.call({ + method: "erpnext.assets.doctype.asset.depreciation.make_depreciation_entry", + args: { + "asset_depr_schedule_name": frm.doc.name, + "date": row.schedule_date + }, + callback: function(r) { + frappe.model.sync(r.message); + frm.refresh(); + } + }) + } + }, + + depreciation_amount: function(frm, cdt, cdn) { + erpnext.asset.set_accumulated_depreciation(frm); + } +}); + +erpnext.asset.set_accumulated_depreciation = function(frm) { + if(frm.doc.depreciation_method != "Manual") return; + + var accumulated_depreciation = flt(frm.doc.opening_accumulated_depreciation); + $.each(frm.doc.schedules || [], function(i, row) { + accumulated_depreciation += flt(row.depreciation_amount); + frappe.model.set_value(row.doctype, row.name, + "accumulated_depreciation_amount", accumulated_depreciation); + }) +}; diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.json b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.json new file mode 100644 index 0000000000..898c482079 --- /dev/null +++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.json @@ -0,0 +1,202 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "naming_series:", + "creation": "2022-10-31 15:03:35.424877", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "asset", + "naming_series", + "column_break_2", + "opening_accumulated_depreciation", + "finance_book", + "finance_book_id", + "depreciation_details_section", + "depreciation_method", + "total_number_of_depreciations", + "rate_of_depreciation", + "column_break_8", + "frequency_of_depreciation", + "expected_value_after_useful_life", + "depreciation_schedule_section", + "depreciation_schedule", + "details_section", + "notes", + "status", + "amended_from" + ], + "fields": [ + { + "fieldname": "asset", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Asset", + "options": "Asset", + "reqd": 1 + }, + { + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Naming Series", + "options": "ACC-ADS-.YYYY.-" + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Asset Depreciation Schedule", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, + { + "collapsible": 1, + "fieldname": "depreciation_details_section", + "fieldtype": "Section Break", + "label": "Depreciation Details" + }, + { + "fieldname": "finance_book", + "fieldtype": "Link", + "label": "Finance Book", + "options": "Finance Book" + }, + { + "fieldname": "depreciation_method", + "fieldtype": "Select", + "label": "Depreciation Method", + "options": "\nStraight Line\nDouble Declining Balance\nWritten Down Value\nManual", + "read_only": 1 + }, + { + "depends_on": "eval:doc.depreciation_method == 'Written Down Value'", + "description": "In Percentage", + "fieldname": "rate_of_depreciation", + "fieldtype": "Percent", + "label": "Rate of Depreciation", + "read_only": 1 + }, + { + "fieldname": "column_break_8", + "fieldtype": "Column Break" + }, + { + "depends_on": "total_number_of_depreciations", + "fieldname": "total_number_of_depreciations", + "fieldtype": "Int", + "label": "Total Number of Depreciations", + "read_only": 1 + }, + { + "fieldname": "depreciation_schedule_section", + "fieldtype": "Section Break", + "label": "Depreciation Schedule" + }, + { + "fieldname": "depreciation_schedule", + "fieldtype": "Table", + "label": "Depreciation Schedule", + "options": "Depreciation Schedule" + }, + { + "collapsible": 1, + "collapsible_depends_on": "notes", + "fieldname": "details_section", + "fieldtype": "Section Break", + "label": "Details" + }, + { + "fieldname": "notes", + "fieldtype": "Small Text", + "label": "Notes", + "read_only": 1 + }, + { + "fieldname": "status", + "fieldtype": "Select", + "hidden": 1, + "label": "Status", + "options": "Draft\nActive\nCancelled", + "read_only": 1 + }, + { + "depends_on": "frequency_of_depreciation", + "fieldname": "frequency_of_depreciation", + "fieldtype": "Int", + "label": "Frequency of Depreciation (Months)", + "read_only": 1 + }, + { + "fieldname": "expected_value_after_useful_life", + "fieldtype": "Currency", + "label": "Expected Value After Useful Life", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "finance_book_id", + "fieldtype": "Int", + "hidden": 1, + "label": "Finance Book Id", + "print_hide": 1, + "read_only": 1 + }, + { + "depends_on": "opening_accumulated_depreciation", + "fieldname": "opening_accumulated_depreciation", + "fieldtype": "Currency", + "label": "Opening Accumulated Depreciation", + "options": "Company:company:default_currency", + "read_only": 1 + } + ], + "index_web_pages_for_search": 1, + "is_submittable": 1, + "links": [], + "modified": "2023-01-16 21:08:21.421260", + "modified_by": "Administrator", + "module": "Assets", + "name": "Asset Depreciation Schedule", + "naming_rule": "By \"Naming Series\" field", + "owner": "Administrator", + "permissions": [ + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts User", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Quality Manager", + "share": 1, + "submit": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py new file mode 100644 index 0000000000..6f02662544 --- /dev/null +++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py @@ -0,0 +1,475 @@ +# Copyright (c) 2022, 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 add_days, add_months, cint, flt, get_last_day, is_last_day_of_the_month + + +class AssetDepreciationSchedule(Document): + def before_save(self): + if not self.finance_book_id: + self.prepare_draft_asset_depr_schedule_data_from_asset_name_and_fb_name( + self.asset, self.finance_book + ) + + def validate(self): + self.validate_another_asset_depr_schedule_does_not_exist() + + def validate_another_asset_depr_schedule_does_not_exist(self): + finance_book_filter = ["finance_book", "is", "not set"] + if self.finance_book: + finance_book_filter = ["finance_book", "=", self.finance_book] + + asset_depr_schedule = frappe.db.exists( + "Asset Depreciation Schedule", + [ + ["asset", "=", self.asset], + finance_book_filter, + ["docstatus", "<", 2], + ], + ) + + if asset_depr_schedule and asset_depr_schedule != self.name: + if self.finance_book: + frappe.throw( + _( + "Asset Depreciation Schedule {0} for Asset {1} and Finance Book {2} already exists." + ).format(asset_depr_schedule, self.asset, self.finance_book) + ) + else: + frappe.throw( + _("Asset Depreciation Schedule {0} for Asset {1} already exists.").format( + asset_depr_schedule, self.asset + ) + ) + + def on_submit(self): + self.db_set("status", "Active") + + def before_cancel(self): + if not self.flags.should_not_cancel_depreciation_entries: + self.cancel_depreciation_entries() + + def cancel_depreciation_entries(self): + for d in self.get("depreciation_schedule"): + if d.journal_entry: + frappe.get_doc("Journal Entry", d.journal_entry).cancel() + + def on_cancel(self): + self.db_set("status", "Cancelled") + + def prepare_draft_asset_depr_schedule_data_from_asset_name_and_fb_name(self, asset_name, fb_name): + asset_doc = frappe.get_doc("Asset", asset_name) + + finance_book_filter = ["finance_book", "is", "not set"] + if fb_name: + finance_book_filter = ["finance_book", "=", fb_name] + + asset_finance_book_name = frappe.db.get_value( + doctype="Asset Finance Book", + filters=[["parent", "=", asset_name], finance_book_filter], + ) + asset_finance_book_doc = frappe.get_doc("Asset Finance Book", asset_finance_book_name) + + self.prepare_draft_asset_depr_schedule_data(asset_doc, asset_finance_book_doc) + + def prepare_draft_asset_depr_schedule_data( + self, + asset_doc, + row, + date_of_disposal=None, + date_of_return=None, + update_asset_finance_book_row=True, + ): + self.set_draft_asset_depr_schedule_details(asset_doc, row) + self.make_depr_schedule(asset_doc, row, date_of_disposal, update_asset_finance_book_row) + self.set_accumulated_depreciation(row, date_of_disposal, date_of_return) + + def set_draft_asset_depr_schedule_details(self, asset_doc, row): + self.asset = asset_doc.name + self.finance_book = row.finance_book + self.finance_book_id = row.idx + self.opening_accumulated_depreciation = asset_doc.opening_accumulated_depreciation + self.depreciation_method = row.depreciation_method + self.total_number_of_depreciations = row.total_number_of_depreciations + self.frequency_of_depreciation = row.frequency_of_depreciation + self.rate_of_depreciation = row.rate_of_depreciation + self.expected_value_after_useful_life = row.expected_value_after_useful_life + self.status = "Draft" + + def make_depr_schedule( + self, asset_doc, row, date_of_disposal, update_asset_finance_book_row=True + ): + if row.depreciation_method != "Manual" and not self.get("depreciation_schedule"): + self.depreciation_schedule = [] + + if not asset_doc.available_for_use_date: + return + + start = self.clear_depr_schedule() + + self._make_depr_schedule(asset_doc, row, start, date_of_disposal, update_asset_finance_book_row) + + def clear_depr_schedule(self): + start = 0 + num_of_depreciations_completed = 0 + depr_schedule = [] + + for schedule in self.get("depreciation_schedule"): + if schedule.journal_entry: + num_of_depreciations_completed += 1 + depr_schedule.append(schedule) + else: + start = num_of_depreciations_completed + break + + self.depreciation_schedule = depr_schedule + + return start + + def _make_depr_schedule( + self, asset_doc, row, start, date_of_disposal, update_asset_finance_book_row + ): + asset_doc.validate_asset_finance_books(row) + + value_after_depreciation = _get_value_after_depreciation_for_making_schedule(asset_doc, row) + row.value_after_depreciation = value_after_depreciation + + if update_asset_finance_book_row: + row.db_update() + + number_of_pending_depreciations = cint(row.total_number_of_depreciations) - cint( + asset_doc.number_of_depreciations_booked + ) + + has_pro_rata = asset_doc.check_is_pro_rata(row) + if has_pro_rata: + number_of_pending_depreciations += 1 + + skip_row = False + should_get_last_day = is_last_day_of_the_month(row.depreciation_start_date) + + for n in range(start, number_of_pending_depreciations): + # If depreciation is already completed (for double declining balance) + if skip_row: + continue + + depreciation_amount = asset_doc.get_depreciation_amount(value_after_depreciation, row) + + if not has_pro_rata or n < cint(number_of_pending_depreciations) - 1: + schedule_date = add_months( + row.depreciation_start_date, n * cint(row.frequency_of_depreciation) + ) + + if should_get_last_day: + schedule_date = get_last_day(schedule_date) + + # 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, -row.frequency_of_depreciation + 1) + + # if asset is being sold or scrapped + if date_of_disposal: + from_date = asset_doc.available_for_use_date + if self.depreciation_schedule: + from_date = self.depreciation_schedule[-1].schedule_date + + depreciation_amount, days, months = asset_doc.get_pro_rata_amt( + row, depreciation_amount, from_date, date_of_disposal + ) + + if depreciation_amount > 0: + self.add_depr_schedule_row( + date_of_disposal, + depreciation_amount, + row.depreciation_method, + ) + + break + + # For first row + if has_pro_rata and not asset_doc.opening_accumulated_depreciation and n == 0: + from_date = add_days( + asset_doc.available_for_use_date, -1 + ) # needed to calc depr amount for available_for_use_date too + depreciation_amount, days, months = asset_doc.get_pro_rata_amt( + row, depreciation_amount, from_date, row.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(row.depreciation_start_date, -months + 1) + + # For last row + elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1: + if not asset_doc.flags.increase_in_asset_life: + # In case of increase_in_asset_life, the asset.to_date is already set on asset_repair submission + asset_doc.to_date = add_months( + asset_doc.available_for_use_date, + (n + asset_doc.number_of_depreciations_booked) * cint(row.frequency_of_depreciation), + ) + + depreciation_amount_without_pro_rata = depreciation_amount + + depreciation_amount, days, months = asset_doc.get_pro_rata_amt( + row, depreciation_amount, schedule_date, asset_doc.to_date + ) + + depreciation_amount = self.get_adjusted_depreciation_amount( + depreciation_amount_without_pro_rata, depreciation_amount + ) + + monthly_schedule_date = add_months(schedule_date, 1) + schedule_date = add_days(schedule_date, days) + last_schedule_date = schedule_date + + if not depreciation_amount: + continue + value_after_depreciation -= flt( + depreciation_amount, asset_doc.precision("gross_purchase_amount") + ) + + # Adjust depreciation amount in the last period based on the expected value after useful life + if row.expected_value_after_useful_life and ( + ( + n == cint(number_of_pending_depreciations) - 1 + and value_after_depreciation != row.expected_value_after_useful_life + ) + or value_after_depreciation < row.expected_value_after_useful_life + ): + depreciation_amount += value_after_depreciation - row.expected_value_after_useful_life + skip_row = True + + if depreciation_amount > 0: + self.add_depr_schedule_row( + schedule_date, + depreciation_amount, + row.depreciation_method, + ) + + # 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 + ): + if not self.opening_accumulated_depreciation: + depreciation_amount_for_first_row = self.get_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 + + def get_depreciation_amount_for_first_row(self): + return self.get("depreciation_schedule")[0].depreciation_amount + + def add_depr_schedule_row( + self, + schedule_date, + depreciation_amount, + depreciation_method, + ): + self.append( + "depreciation_schedule", + { + "schedule_date": schedule_date, + "depreciation_amount": depreciation_amount, + "depreciation_method": depreciation_method, + }, + ) + + def set_accumulated_depreciation( + self, + row, + date_of_disposal=None, + date_of_return=None, + ignore_booked_entry=False, + ): + straight_line_idx = [ + d.idx for d in self.get("depreciation_schedule") if d.depreciation_method == "Straight Line" + ] + + accumulated_depreciation = flt(self.opening_accumulated_depreciation) + value_after_depreciation = flt(row.value_after_depreciation) + + for i, d in enumerate(self.get("depreciation_schedule")): + if ignore_booked_entry and d.journal_entry: + continue + + depreciation_amount = flt(d.depreciation_amount, d.precision("depreciation_amount")) + value_after_depreciation -= flt(depreciation_amount) + + # for the last row, if depreciation method = Straight Line + if ( + straight_line_idx + and i == max(straight_line_idx) - 1 + and not date_of_disposal + and not date_of_return + ): + depreciation_amount += flt( + value_after_depreciation - flt(row.expected_value_after_useful_life), + d.precision("depreciation_amount"), + ) + + d.depreciation_amount = depreciation_amount + accumulated_depreciation += d.depreciation_amount + d.accumulated_depreciation_amount = flt( + accumulated_depreciation, d.precision("accumulated_depreciation_amount") + ) + + +def _get_value_after_depreciation_for_making_schedule(asset_doc, fb_row): + if asset_doc.docstatus == 1 and fb_row.value_after_depreciation: + value_after_depreciation = flt(fb_row.value_after_depreciation) + else: + value_after_depreciation = flt(asset_doc.gross_purchase_amount) - flt( + asset_doc.opening_accumulated_depreciation + ) + + return value_after_depreciation + + +def make_draft_asset_depr_schedules_if_not_present(asset_doc): + for row in asset_doc.get("finance_books"): + draft_asset_depr_schedule_name = get_asset_depr_schedule_name( + asset_doc.name, "Draft", row.finance_book + ) + + active_asset_depr_schedule_name = get_asset_depr_schedule_name( + asset_doc.name, "Active", row.finance_book + ) + + if not draft_asset_depr_schedule_name and not active_asset_depr_schedule_name: + make_draft_asset_depr_schedule(asset_doc, row) + + +def make_draft_asset_depr_schedules(asset_doc): + for row in asset_doc.get("finance_books"): + make_draft_asset_depr_schedule(asset_doc, row) + + +def make_draft_asset_depr_schedule(asset_doc, row): + asset_depr_schedule_doc = frappe.new_doc("Asset Depreciation Schedule") + + asset_depr_schedule_doc.prepare_draft_asset_depr_schedule_data(asset_doc, row) + + asset_depr_schedule_doc.insert() + + +def update_draft_asset_depr_schedules(asset_doc): + for row in asset_doc.get("finance_books"): + asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset_doc.name, "Draft", row.finance_book) + + if not asset_depr_schedule_doc: + continue + + asset_depr_schedule_doc.prepare_draft_asset_depr_schedule_data(asset_doc, row) + + asset_depr_schedule_doc.save() + + +def convert_draft_asset_depr_schedules_into_active(asset_doc): + for row in asset_doc.get("finance_books"): + asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset_doc.name, "Draft", row.finance_book) + + if not asset_depr_schedule_doc: + continue + + asset_depr_schedule_doc.submit() + + +def cancel_asset_depr_schedules(asset_doc): + for row in asset_doc.get("finance_books"): + asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset_doc.name, "Active", row.finance_book) + + if not asset_depr_schedule_doc: + continue + + asset_depr_schedule_doc.cancel() + + +def make_new_active_asset_depr_schedules_and_cancel_current_ones( + asset_doc, notes, date_of_disposal=None, date_of_return=None +): + for row in asset_doc.get("finance_books"): + current_asset_depr_schedule_doc = get_asset_depr_schedule_doc( + asset_doc.name, "Active", row.finance_book + ) + + if not current_asset_depr_schedule_doc: + frappe.throw( + _("Asset Depreciation Schedule not found for Asset {0} and Finance Book {1}").format( + asset_doc.name, row.finance_book + ) + ) + + new_asset_depr_schedule_doc = frappe.copy_doc(current_asset_depr_schedule_doc) + + new_asset_depr_schedule_doc.make_depr_schedule(asset_doc, row, date_of_disposal) + new_asset_depr_schedule_doc.set_accumulated_depreciation(row, date_of_disposal, date_of_return) + + new_asset_depr_schedule_doc.notes = notes + + current_asset_depr_schedule_doc.flags.should_not_cancel_depreciation_entries = True + current_asset_depr_schedule_doc.cancel() + + new_asset_depr_schedule_doc.submit() + + +def get_temp_asset_depr_schedule_doc( + asset_doc, row, date_of_disposal=None, date_of_return=None, update_asset_finance_book_row=False +): + asset_depr_schedule_doc = frappe.new_doc("Asset Depreciation Schedule") + + asset_depr_schedule_doc.prepare_draft_asset_depr_schedule_data( + asset_doc, + row, + date_of_disposal, + date_of_return, + update_asset_finance_book_row, + ) + + return asset_depr_schedule_doc + + +@frappe.whitelist() +def get_depr_schedule(asset_name, status, finance_book=None): + asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset_name, status, finance_book) + + if not asset_depr_schedule_doc: + return + + return asset_depr_schedule_doc.get("depreciation_schedule") + + +def get_asset_depr_schedule_doc(asset_name, status, finance_book=None): + asset_depr_schedule_name = get_asset_depr_schedule_name(asset_name, status, finance_book) + + if not asset_depr_schedule_name: + return + + asset_depr_schedule_doc = frappe.get_doc("Asset Depreciation Schedule", asset_depr_schedule_name) + + return asset_depr_schedule_doc + + +def get_asset_depr_schedule_name(asset_name, status, finance_book=None): + finance_book_filter = ["finance_book", "is", "not set"] + if finance_book: + finance_book_filter = ["finance_book", "=", finance_book] + + return frappe.db.get_value( + doctype="Asset Depreciation Schedule", + filters=[ + ["asset", "=", asset_name], + finance_book_filter, + ["status", "=", status], + ], + ) diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/test_asset_depreciation_schedule.py b/erpnext/assets/doctype/asset_depreciation_schedule/test_asset_depreciation_schedule.py new file mode 100644 index 0000000000..024121d394 --- /dev/null +++ b/erpnext/assets/doctype/asset_depreciation_schedule/test_asset_depreciation_schedule.py @@ -0,0 +1,27 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe +from frappe.tests.utils import FrappeTestCase + +from erpnext.assets.doctype.asset.test_asset import create_asset, create_asset_data +from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import ( + get_asset_depr_schedule_doc, +) + + +class TestAssetDepreciationSchedule(FrappeTestCase): + def setUp(self): + create_asset_data() + + def test_throw_error_if_another_asset_depr_schedule_exist(self): + asset = create_asset(item_code="Macbook Pro", calculate_depreciation=1, submit=1) + + first_asset_depr_schedule = get_asset_depr_schedule_doc(asset.name, "Active") + self.assertEquals(first_asset_depr_schedule.status, "Active") + + second_asset_depr_schedule = frappe.get_doc( + {"doctype": "Asset Depreciation Schedule", "asset": asset.name, "finance_book": None} + ) + + self.assertRaises(frappe.ValidationError, second_asset_depr_schedule.insert) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js index f5e4e723b4..f9ed2cc344 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.js +++ b/erpnext/assets/doctype/asset_repair/asset_repair.js @@ -76,7 +76,7 @@ frappe.ui.form.on('Asset Repair Consumed Item', { 'warehouse': frm.doc.warehouse, 'qty': item.consumed_quantity, 'serial_no': item.serial_no, - 'company': frm.doc.company + 'company': frm.doc.company, }; frappe.call({ diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index ba3189887c..accb5bf54b 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -238,7 +238,6 @@ "no_copy": 1 }, { - "depends_on": "eval:!doc.__islocal", "fieldname": "purchase_invoice", "fieldtype": "Link", "label": "Purchase Invoice", @@ -257,6 +256,7 @@ "fieldname": "stock_entry", "fieldtype": "Link", "label": "Stock Entry", + "no_copy": 1, "options": "Stock Entry", "read_only": 1 } @@ -264,10 +264,11 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-06-25 13:14:38.307723", + "modified": "2022-08-16 15:55:25.023471", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", + "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ { @@ -303,6 +304,7 @@ ], "sort_field": "modified", "sort_order": "DESC", + "states": [], "title_field": "asset_name", "track_changes": 1, "track_seen": 1 diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 5bf6011cf8..a7172a72c6 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -1,13 +1,17 @@ # Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt - import frappe from frappe import _ -from frappe.utils import add_months, cint, flt, getdate, time_diff_in_hours +from frappe.utils import add_months, cint, flt, get_link_to_form, getdate, time_diff_in_hours +import erpnext from erpnext.accounts.general_ledger import make_gl_entries from erpnext.assets.doctype.asset.asset import get_asset_account +from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import ( + get_depr_schedule, + make_new_active_asset_depr_schedules_and_cancel_current_ones, +) from erpnext.controllers.accounts_controller import AccountsController @@ -17,7 +21,7 @@ class AssetRepair(AccountsController): self.update_status() if self.get("stock_items"): - self.set_total_value() + self.set_stock_items_cost() self.calculate_total_repair_cost() def update_status(self): @@ -26,7 +30,7 @@ class AssetRepair(AccountsController): else: self.asset_doc.set_status() - def set_total_value(self): + def set_stock_items_cost(self): for item in self.get("stock_items"): item.total_value = flt(item.valuation_rate) * flt(item.consumed_quantity) @@ -52,8 +56,14 @@ class AssetRepair(AccountsController): ): self.modify_depreciation_schedule() + notes = _( + "This schedule was created when Asset {0} was repaired through Asset Repair {1}." + ).format( + get_link_to_form(self.asset_doc.doctype, self.asset_doc.name), + get_link_to_form(self.doctype, self.name), + ) self.asset_doc.flags.ignore_validate_update_after_submit = True - self.asset_doc.prepare_depreciation_data() + make_new_active_asset_depr_schedules_and_cancel_current_ones(self.asset_doc, notes) self.asset_doc.save() def before_cancel(self): @@ -66,16 +76,24 @@ class AssetRepair(AccountsController): if self.get("capitalize_repair_cost"): self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry") self.make_gl_entries(cancel=True) + self.db_set("stock_entry", None) if ( frappe.db.get_value("Asset", self.asset, "calculate_depreciation") and self.increase_in_asset_life ): self.revert_depreciation_schedule_on_cancellation() + notes = _("This schedule was created when Asset {0}'s Asset Repair {1} was cancelled.").format( + get_link_to_form(self.asset_doc.doctype, self.asset_doc.name), + get_link_to_form(self.doctype, self.name), + ) self.asset_doc.flags.ignore_validate_update_after_submit = True - self.asset_doc.prepare_depreciation_data() + make_new_active_asset_depr_schedules_and_cancel_current_ones(self.asset_doc, notes) self.asset_doc.save() + def after_delete(self): + frappe.get_doc("Asset", self.asset).set_status() + def check_repair_status(self): if self.repair_status == "Pending": frappe.throw(_("Please update Repair Status.")) @@ -133,6 +151,8 @@ class AssetRepair(AccountsController): "qty": stock_item.consumed_quantity, "basic_rate": stock_item.valuation_rate, "serial_no": stock_item.serial_no, + "cost_center": self.cost_center, + "project": self.project, }, ) @@ -142,72 +162,42 @@ class AssetRepair(AccountsController): self.db_set("stock_entry", stock_entry.name) def increase_stock_quantity(self): - stock_entry = frappe.get_doc("Stock Entry", self.stock_entry) - stock_entry.flags.ignore_links = True - stock_entry.cancel() + if self.stock_entry: + stock_entry = frappe.get_doc("Stock Entry", self.stock_entry) + stock_entry.flags.ignore_links = True + stock_entry.cancel() def make_gl_entries(self, cancel=False): - if flt(self.repair_cost) > 0: + if flt(self.total_repair_cost) > 0: gl_entries = self.get_gl_entries() make_gl_entries(gl_entries, cancel) def get_gl_entries(self): gl_entries = [] - repair_and_maintenance_account = frappe.db.get_value( - "Company", self.company, "repair_and_maintenance_account" - ) + fixed_asset_account = get_asset_account( "fixed_asset_account", asset=self.asset, company=self.company ) - expense_account = ( + self.get_gl_entries_for_repair_cost(gl_entries, fixed_asset_account) + self.get_gl_entries_for_consumed_items(gl_entries, fixed_asset_account) + + return gl_entries + + def get_gl_entries_for_repair_cost(self, gl_entries, fixed_asset_account): + if flt(self.repair_cost) <= 0: + return + + pi_expense_account = ( frappe.get_doc("Purchase Invoice", self.purchase_invoice).items[0].expense_account ) - gl_entries.append( - self.get_gl_dict( - { - "account": expense_account, - "credit": self.repair_cost, - "credit_in_account_currency": self.repair_cost, - "against": repair_and_maintenance_account, - "voucher_type": self.doctype, - "voucher_no": self.name, - "cost_center": self.cost_center, - "posting_date": getdate(), - "company": self.company, - }, - item=self, - ) - ) - - if self.get("stock_consumption"): - # creating GL Entries for each row in Stock Items based on the Stock Entry created for it - stock_entry = frappe.get_doc("Stock Entry", self.stock_entry) - for item in stock_entry.items: - gl_entries.append( - self.get_gl_dict( - { - "account": item.expense_account, - "credit": item.amount, - "credit_in_account_currency": item.amount, - "against": repair_and_maintenance_account, - "voucher_type": self.doctype, - "voucher_no": self.name, - "cost_center": self.cost_center, - "posting_date": getdate(), - "company": self.company, - }, - item=self, - ) - ) - gl_entries.append( self.get_gl_dict( { "account": fixed_asset_account, - "debit": self.total_repair_cost, - "debit_in_account_currency": self.total_repair_cost, - "against": expense_account, + "debit": self.repair_cost, + "debit_in_account_currency": self.repair_cost, + "against": pi_expense_account, "voucher_type": self.doctype, "voucher_no": self.name, "cost_center": self.cost_center, @@ -220,7 +210,75 @@ class AssetRepair(AccountsController): ) ) - return gl_entries + gl_entries.append( + self.get_gl_dict( + { + "account": pi_expense_account, + "credit": self.repair_cost, + "credit_in_account_currency": self.repair_cost, + "against": fixed_asset_account, + "voucher_type": self.doctype, + "voucher_no": self.name, + "cost_center": self.cost_center, + "posting_date": getdate(), + "company": self.company, + }, + item=self, + ) + ) + + def get_gl_entries_for_consumed_items(self, gl_entries, fixed_asset_account): + if not (self.get("stock_consumption") and self.get("stock_items")): + return + + # creating GL Entries for each row in Stock Items based on the Stock Entry created for it + stock_entry = frappe.get_doc("Stock Entry", self.stock_entry) + + default_expense_account = None + if not erpnext.is_perpetual_inventory_enabled(self.company): + default_expense_account = frappe.get_cached_value( + "Company", self.company, "default_expense_account" + ) + if not default_expense_account: + frappe.throw(_("Please set default Expense Account in Company {0}").format(self.company)) + + for item in stock_entry.items: + if flt(item.amount) > 0: + gl_entries.append( + self.get_gl_dict( + { + "account": item.expense_account or default_expense_account, + "credit": item.amount, + "credit_in_account_currency": item.amount, + "against": fixed_asset_account, + "voucher_type": self.doctype, + "voucher_no": self.name, + "cost_center": self.cost_center, + "posting_date": getdate(), + "company": self.company, + }, + item=self, + ) + ) + + gl_entries.append( + self.get_gl_dict( + { + "account": fixed_asset_account, + "debit": item.amount, + "debit_in_account_currency": item.amount, + "against": item.expense_account or default_expense_account, + "voucher_type": self.doctype, + "voucher_no": self.name, + "cost_center": self.cost_center, + "posting_date": getdate(), + "against_voucher_type": "Stock Entry", + "against_voucher": self.stock_entry, + "company": self.company, + }, + item=self, + ) + ) def modify_depreciation_schedule(self): for row in self.asset_doc.finance_books: @@ -238,8 +296,10 @@ class AssetRepair(AccountsController): asset.number_of_depreciations_booked ) + depr_schedule = get_depr_schedule(asset.name, "Active", row.finance_book) + # the Schedule Date in the final row of the old Depreciation Schedule - last_schedule_date = asset.schedules[len(asset.schedules) - 1].schedule_date + last_schedule_date = depr_schedule[len(depr_schedule) - 1].schedule_date # the Schedule Date in the final row of the new Depreciation Schedule asset.to_date = add_months(last_schedule_date, extra_months) @@ -269,8 +329,10 @@ class AssetRepair(AccountsController): asset.number_of_depreciations_booked ) + depr_schedule = get_depr_schedule(asset.name, "Active", row.finance_book) + # the Schedule Date in the final row of the modified Depreciation Schedule - last_schedule_date = asset.schedules[len(asset.schedules) - 1].schedule_date + last_schedule_date = depr_schedule[len(depr_schedule) - 1].schedule_date # the Schedule Date in the final row of the original Depreciation Schedule asset.to_date = add_months(last_schedule_date, -extra_months) diff --git a/erpnext/assets/doctype/asset_repair/test_asset_repair.py b/erpnext/assets/doctype/asset_repair/test_asset_repair.py index 4e7cf78090..a9d0b25755 100644 --- a/erpnext/assets/doctype/asset_repair/test_asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/test_asset_repair.py @@ -6,11 +6,18 @@ import unittest import frappe from frappe.utils import flt, nowdate +from erpnext.assets.doctype.asset.asset import ( + get_asset_account, + get_asset_value_after_depreciation, +) from erpnext.assets.doctype.asset.test_asset import ( create_asset, create_asset_data, set_depreciation_settings_in_company, ) +from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import ( + get_asset_depr_schedule_doc, +) from erpnext.stock.doctype.item.test_item import create_item @@ -105,48 +112,153 @@ class TestAssetRepair(unittest.TestCase): 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) + initial_asset_value = get_asset_value_after_depreciation(asset.name) asset_repair = create_asset_repair(asset=asset, stock_consumption=1, submit=1) asset.reload() - increase_in_asset_value = get_asset_value(asset) - initial_asset_value + increase_in_asset_value = get_asset_value_after_depreciation(asset.name) - initial_asset_value self.assertEqual(asset_repair.stock_items[0].total_value, increase_in_asset_value) def test_increase_in_asset_value_due_to_repair_cost_capitalisation(self): asset = create_asset(calculate_depreciation=1, submit=1) - initial_asset_value = get_asset_value(asset) + initial_asset_value = get_asset_value_after_depreciation(asset.name) asset_repair = create_asset_repair(asset=asset, capitalize_repair_cost=1, submit=1) asset.reload() - increase_in_asset_value = get_asset_value(asset) - initial_asset_value + increase_in_asset_value = get_asset_value_after_depreciation(asset.name) - initial_asset_value self.assertEqual(asset_repair.repair_cost, increase_in_asset_value) def test_purchase_invoice(self): asset_repair = create_asset_repair(capitalize_repair_cost=1, submit=1) self.assertTrue(asset_repair.purchase_invoice) - def test_gl_entries(self): - asset_repair = create_asset_repair(capitalize_repair_cost=1, submit=1) - gl_entry = frappe.get_last_doc("GL Entry") - self.assertEqual(asset_repair.name, gl_entry.voucher_no) + def test_gl_entries_with_perpetual_inventory(self): + set_depreciation_settings_in_company(company="_Test Company with perpetual inventory") + + asset_category = frappe.get_doc("Asset Category", "Computers") + asset_category.append( + "accounts", + { + "company_name": "_Test Company with perpetual inventory", + "fixed_asset_account": "_Test Fixed Asset - TCP1", + "accumulated_depreciation_account": "_Test Accumulated Depreciations - TCP1", + "depreciation_expense_account": "_Test Depreciations - TCP1", + }, + ) + asset_category.save() + + asset_repair = create_asset_repair( + capitalize_repair_cost=1, + stock_consumption=1, + warehouse="Stores - TCP1", + company="_Test Company with perpetual inventory", + submit=1, + ) + + gl_entries = frappe.db.sql( + """ + select + account, + sum(debit) as debit, + sum(credit) as credit + from `tabGL Entry` + where + voucher_type='Asset Repair' + and voucher_no=%s + group by + account + """, + asset_repair.name, + as_dict=1, + ) + + self.assertTrue(gl_entries) + + fixed_asset_account = get_asset_account( + "fixed_asset_account", asset=asset_repair.asset, company=asset_repair.company + ) + pi_expense_account = ( + frappe.get_doc("Purchase Invoice", asset_repair.purchase_invoice).items[0].expense_account + ) + stock_entry_expense_account = ( + frappe.get_doc("Stock Entry", asset_repair.stock_entry).get("items")[0].expense_account + ) + + expected_values = { + fixed_asset_account: [asset_repair.total_repair_cost, 0], + pi_expense_account: [0, asset_repair.repair_cost], + stock_entry_expense_account: [0, 100], + } + + for d in gl_entries: + self.assertEqual(expected_values[d.account][0], d.debit) + self.assertEqual(expected_values[d.account][1], d.credit) + + def test_gl_entries_with_periodical_inventory(self): + frappe.db.set_value( + "Company", "_Test Company", "default_expense_account", "Cost of Goods Sold - _TC" + ) + asset_repair = create_asset_repair( + capitalize_repair_cost=1, + stock_consumption=1, + submit=1, + ) + + gl_entries = frappe.db.sql( + """ + select + account, + sum(debit) as debit, + sum(credit) as credit + from `tabGL Entry` + where + voucher_type='Asset Repair' + and voucher_no=%s + group by + account + """, + asset_repair.name, + as_dict=1, + ) + + self.assertTrue(gl_entries) + + fixed_asset_account = get_asset_account( + "fixed_asset_account", asset=asset_repair.asset, company=asset_repair.company + ) + default_expense_account = frappe.get_cached_value( + "Company", asset_repair.company, "default_expense_account" + ) + + expected_values = {fixed_asset_account: [1100, 0], default_expense_account: [0, 1100]} + + for d in gl_entries: + self.assertEqual(expected_values[d.account][0], d.debit) + self.assertEqual(expected_values[d.account][1], d.credit) def test_increase_in_asset_life(self): asset = create_asset(calculate_depreciation=1, submit=1) + + first_asset_depr_schedule = get_asset_depr_schedule_doc(asset.name, "Active") + self.assertEquals(first_asset_depr_schedule.status, "Active") + initial_num_of_depreciations = num_of_depreciations(asset) create_asset_repair(asset=asset, capitalize_repair_cost=1, submit=1) + asset.reload() + first_asset_depr_schedule.load_from_db() + + second_asset_depr_schedule = get_asset_depr_schedule_doc(asset.name, "Active") + self.assertEquals(second_asset_depr_schedule.status, "Active") + self.assertEquals(first_asset_depr_schedule.status, "Cancelled") self.assertEqual((initial_num_of_depreciations + 1), num_of_depreciations(asset)) self.assertEqual( - asset.schedules[-1].accumulated_depreciation_amount, + second_asset_depr_schedule.get("depreciation_schedule")[-1].accumulated_depreciation_amount, asset.finance_books[0].value_after_depreciation, ) -def get_asset_value(asset): - return asset.finance_books[0].value_after_depreciation - - def num_of_depreciations(asset): return asset.finance_books[0].total_number_of_depreciations @@ -160,7 +272,7 @@ def create_asset_repair(**args): if args.asset: asset = args.asset else: - asset = create_asset(is_existing_asset=1, submit=1) + asset = create_asset(is_existing_asset=1, submit=1, company=args.company) asset_repair = frappe.new_doc("Asset Repair") asset_repair.update( { @@ -192,7 +304,7 @@ def create_asset_repair(**args): if args.submit: asset_repair.repair_status = "Completed" - asset_repair.cost_center = "_Test Cost Center - _TC" + asset_repair.cost_center = frappe.db.get_value("Company", asset.company, "cost_center") if args.stock_consumption: stock_entry = frappe.get_doc( @@ -204,6 +316,8 @@ def create_asset_repair(**args): "t_warehouse": asset_repair.warehouse, "item_code": asset_repair.stock_items[0].item_code, "qty": asset_repair.stock_items[0].consumed_quantity, + "basic_rate": args.rate if args.get("rate") is not None else 100, + "cost_center": asset_repair.cost_center, }, ) stock_entry.submit() @@ -213,7 +327,13 @@ def create_asset_repair(**args): asset_repair.repair_cost = 1000 if asset.calculate_depreciation: asset_repair.increase_in_asset_life = 12 - asset_repair.purchase_invoice = make_purchase_invoice().name + pi = make_purchase_invoice( + company=asset.company, + expense_account=frappe.db.get_value("Company", asset.company, "default_expense_account"), + cost_center=asset_repair.cost_center, + warehouse=asset_repair.warehouse, + ) + asset_repair.purchase_invoice = pi.name asset_repair.submit() return asset_repair 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 36f510b18e..ae0e1bda02 100644 --- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.js +++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.js @@ -47,7 +47,7 @@ frappe.ui.form.on('Asset Value Adjustment', { set_current_asset_value: function(frm) { if (frm.doc.asset) { frm.call({ - method: "erpnext.assets.doctype.asset_value_adjustment.asset_value_adjustment.get_current_asset_value", + method: "erpnext.assets.doctype.asset.asset.get_asset_value_after_depreciation", args: { asset: frm.doc.asset, finance_book: frm.doc.finance_book 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 59ab6a910d..31d6ffab5f 100644 --- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py +++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py @@ -5,13 +5,16 @@ import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import cint, date_diff, flt, formatdate, getdate +from frappe.utils import date_diff, flt, formatdate, get_link_to_form, getdate 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.asset import get_asset_value_after_depreciation from erpnext.assets.doctype.asset.depreciation import get_depreciation_accounts +from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import ( + get_asset_depr_schedule_doc, +) class AssetValueAdjustment(Document): @@ -42,7 +45,7 @@ class AssetValueAdjustment(Document): def set_current_asset_value(self): if not self.current_asset_value and self.asset: - self.current_asset_value = get_current_asset_value(self.asset, self.finance_book) + self.current_asset_value = get_asset_value_after_depreciation(self.asset, self.finance_book) def make_depreciation_entry(self): asset = frappe.get_doc("Asset", self.asset) @@ -61,7 +64,9 @@ class AssetValueAdjustment(Document): je.naming_series = depreciation_series je.posting_date = self.date je.company = self.company - je.remark = "Depreciation Entry against {0} worth {1}".format(self.asset, self.difference_amount) + je.remark = _("Depreciation Entry against {0} worth {1}").format( + self.asset, self.difference_amount + ) je.finance_book = self.finance_book credit_entry = { @@ -110,27 +115,54 @@ class AssetValueAdjustment(Document): for d in asset.finance_books: d.value_after_depreciation = asset_value + current_asset_depr_schedule_doc = get_asset_depr_schedule_doc( + asset.name, "Active", d.finance_book + ) + + new_asset_depr_schedule_doc = frappe.copy_doc(current_asset_depr_schedule_doc) + new_asset_depr_schedule_doc.status = "Draft" + new_asset_depr_schedule_doc.docstatus = 0 + + current_asset_depr_schedule_doc.flags.should_not_cancel_depreciation_entries = True + current_asset_depr_schedule_doc.cancel() + + if self.docstatus == 1: + notes = _( + "This schedule was created when Asset {0} was adjusted through Asset Value Adjustment {1}." + ).format( + get_link_to_form(asset.doctype, asset.name), + get_link_to_form(self.get("doctype"), self.get("name")), + ) + elif self.docstatus == 2: + notes = _( + "This schedule was created when Asset {0}'s Asset Value Adjustment {1} was cancelled." + ).format( + get_link_to_form(asset.doctype, asset.name), + get_link_to_form(self.get("doctype"), self.get("name")), + ) + new_asset_depr_schedule_doc.notes = notes + + new_asset_depr_schedule_doc.insert() + + depr_schedule = new_asset_depr_schedule_doc.get("depreciation_schedule") + if d.depreciation_method in ("Straight Line", "Manual"): - end_date = max(s.schedule_date for s in asset.schedules if cint(s.finance_book_id) == d.idx) + end_date = max(s.schedule_date for s in depr_schedule) total_days = date_diff(end_date, self.date) rate_per_day = flt(d.value_after_depreciation) / flt(total_days) from_date = self.date else: - no_of_depreciations = len( - [ - s.name for s in asset.schedules if (cint(s.finance_book_id) == d.idx and not s.journal_entry) - ] - ) + no_of_depreciations = len([s.name for s in depr_schedule if not s.journal_entry]) value_after_depreciation = d.value_after_depreciation - for data in asset.schedules: - if cint(data.finance_book_id) == d.idx and not data.journal_entry: + for data in depr_schedule: + if not data.journal_entry: if d.depreciation_method in ("Straight Line", "Manual"): days = date_diff(data.schedule_date, from_date) depreciation_amount = days * rate_per_day from_date = data.schedule_date else: - depreciation_amount = get_depreciation_amount(asset, value_after_depreciation, d) + depreciation_amount = asset.get_depreciation_amount(value_after_depreciation, d) if depreciation_amount: value_after_depreciation -= flt(depreciation_amount) @@ -138,16 +170,9 @@ class AssetValueAdjustment(Document): d.db_update() - asset.set_accumulated_depreciation(ignore_booked_entry=True) - for asset_data in asset.schedules: - if not asset_data.journal_entry: - asset_data.db_update() + new_asset_depr_schedule_doc.set_accumulated_depreciation(d, ignore_booked_entry=True) + for asset_data in depr_schedule: + if not asset_data.journal_entry: + asset_data.db_update() - -@frappe.whitelist() -def get_current_asset_value(asset, finance_book=None): - cond = {"parent": asset, "parenttype": "Asset"} - if finance_book: - cond.update({"finance_book": finance_book}) - - return frappe.db.get_value("Asset Finance Book", cond, "value_after_depreciation") + new_asset_depr_schedule_doc.submit() diff --git a/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py index 62c636624c..0b3dcba024 100644 --- a/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py +++ b/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py @@ -6,9 +6,10 @@ import unittest import frappe from frappe.utils import add_days, get_last_day, nowdate +from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciation from erpnext.assets.doctype.asset.test_asset import create_asset_data -from erpnext.assets.doctype.asset_value_adjustment.asset_value_adjustment import ( - get_current_asset_value, +from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import ( + get_asset_depr_schedule_doc, ) from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt @@ -43,7 +44,7 @@ class TestAssetValueAdjustment(unittest.TestCase): ) asset_doc.submit() - current_value = get_current_asset_value(asset_doc.name) + current_value = get_asset_value_after_depreciation(asset_doc.name) self.assertEqual(current_value, 100000.0) def test_asset_depreciation_value_adjustment(self): @@ -73,12 +74,21 @@ class TestAssetValueAdjustment(unittest.TestCase): ) asset_doc.submit() - current_value = get_current_asset_value(asset_doc.name) + first_asset_depr_schedule = get_asset_depr_schedule_doc(asset_doc.name, "Active") + self.assertEquals(first_asset_depr_schedule.status, "Active") + + current_value = get_asset_value_after_depreciation(asset_doc.name) adj_doc = make_asset_value_adjustment( asset=asset_doc.name, current_asset_value=current_value, new_asset_value=50000.0 ) adj_doc.submit() + first_asset_depr_schedule.load_from_db() + + second_asset_depr_schedule = get_asset_depr_schedule_doc(asset_doc.name, "Active") + self.assertEquals(second_asset_depr_schedule.status, "Active") + self.assertEquals(first_asset_depr_schedule.status, "Cancelled") + expected_gle = ( ("_Test Accumulated Depreciations - _TC", 0.0, 50000.0), ("_Test Depreciations - _TC", 50000.0, 0.0), diff --git a/erpnext/assets/doctype/depreciation_schedule/depreciation_schedule.json b/erpnext/assets/doctype/depreciation_schedule/depreciation_schedule.json index 35a2c9dd7f..882c4bf00b 100644 --- a/erpnext/assets/doctype/depreciation_schedule/depreciation_schedule.json +++ b/erpnext/assets/doctype/depreciation_schedule/depreciation_schedule.json @@ -1,318 +1,84 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 1, - "autoname": "", - "beta": 0, - "creation": "2016-03-02 15:11:01.278862", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Document", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "allow_rename": 1, + "creation": "2016-03-02 15:11:01.278862", + "doctype": "DocType", + "document_type": "Document", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "schedule_date", + "depreciation_amount", + "column_break_3", + "accumulated_depreciation_amount", + "journal_entry", + "make_depreciation_entry", + "depreciation_method" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "finance_book", - "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": "Finance Book", - "length": 0, - "no_copy": 0, - "options": "Finance Book", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "schedule_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Schedule Date", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "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": 1, - "in_standard_filter": 0, - "label": "Schedule Date", - "length": 0, - "no_copy": 1, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "depreciation_amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Depreciation Amount", + "options": "Company:company:default_currency", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "depreciation_amount", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Depreciation Amount", - "length": 0, - "no_copy": 1, - "options": "Company:company:default_currency", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_3", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "accumulated_depreciation_amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Accumulated Depreciation Amount", + "options": "Company:company:default_currency", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "accumulated_depreciation_amount", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Accumulated Depreciation Amount", - "length": 0, - "no_copy": 1, - "options": "Company:company:default_currency", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "depends_on": "eval:doc.docstatus==1", + "fieldname": "journal_entry", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Journal Entry", + "options": "Journal Entry", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:doc.docstatus==1", - "fieldname": "journal_entry", - "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": "Journal Entry", - "length": 0, - "no_copy": 1, - "options": "Journal Entry", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "allow_on_submit": 1, + "depends_on": "eval:(doc.docstatus==1 && !doc.journal_entry && doc.schedule_date <= get_today())", + "fieldname": "make_depreciation_entry", + "fieldtype": "Button", + "label": "Make Depreciation Entry" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:(doc.docstatus==1 && !doc.journal_entry && doc.schedule_date <= get_today())", - "fieldname": "make_depreciation_entry", - "fieldtype": "Button", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Make Depreciation Entry", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "finance_book_id", - "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": "Finance Book Id", - "length": 0, - "no_copy": 1, - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "depreciation_method", - "fieldtype": "Select", - "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": "Depreciation Method", - "length": 0, - "no_copy": 1, - "options": "\nStraight Line\nDouble Declining Balance\nWritten Down Value\nManual", - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "fieldname": "depreciation_method", + "fieldtype": "Select", + "hidden": 1, + "label": "Depreciation Method", + "options": "\nStraight Line\nDouble Declining Balance\nWritten Down Value\nManual", + "print_hide": 1, + "read_only": 1 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2018-05-10 15:12:41.679436", - "modified_by": "Administrator", - "module": "Assets", - "name": "Depreciation Schedule", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 0, - "track_seen": 0 + ], + "istable": 1, + "links": [], + "modified": "2022-12-06 20:35:50.264281", + "modified_by": "Administrator", + "module": "Assets", + "name": "Depreciation Schedule", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "states": [] } \ No newline at end of file diff --git a/erpnext/assets/doctype/location/location.py b/erpnext/assets/doctype/location/location.py index 0d87bb2bf4..5bff3dd8c9 100644 --- a/erpnext/assets/doctype/location/location.py +++ b/erpnext/assets/doctype/location/location.py @@ -200,11 +200,11 @@ def get_children(doctype, parent=None, location=None, is_root=False): name as value, is_group as expandable from - `tab{doctype}` comp + `tabLocation` comp where ifnull(parent_location, "")={parent} """.format( - doctype=doctype, parent=frappe.db.escape(parent) + parent=frappe.db.escape(parent) ), as_dict=1, ) diff --git a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py index 6b14dce084..59d43b1ea6 100644 --- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py +++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py @@ -4,6 +4,7 @@ import frappe from frappe import _ +from frappe.query_builder.functions import Sum from frappe.utils import cstr, flt, formatdate, getdate from erpnext.accounts.report.financial_statements import ( @@ -11,6 +12,8 @@ from erpnext.accounts.report.financial_statements import ( get_period_list, validate_fiscal_year, ) +from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciation +from erpnext.assets.doctype.asset.depreciation import get_depreciation_accounts def execute(filters=None): @@ -85,7 +88,9 @@ def get_data(filters): "asset_name", "status", "department", + "company", "cost_center", + "calculate_depreciation", "purchase_receipt", "asset_category", "purchase_date", @@ -97,12 +102,21 @@ def get_data(filters): ] assets_record = frappe.db.get_all("Asset", filters=conditions, fields=fields) + assets_linked_to_fb = frappe.db.get_all( + doctype="Asset Finance Book", + filters={"finance_book": filters.finance_book or ("is", "not set")}, + pluck="parent", + ) + for asset in assets_record: - asset_value = ( - asset.gross_purchase_amount - - flt(asset.opening_accumulated_depreciation) - - flt(depreciation_amount_map.get(asset.name)) - ) + if filters.finance_book: + if asset.asset_id not in assets_linked_to_fb: + continue + else: + if asset.calculate_depreciation and asset.asset_id not in assets_linked_to_fb: + continue + + asset_value = get_asset_value_after_depreciation(asset.asset_id, filters.finance_book) row = { "asset_id": asset.asset_id, "asset_name": asset.asset_name, @@ -113,7 +127,7 @@ def get_data(filters): or pi_supplier_map.get(asset.purchase_invoice), "gross_purchase_amount": asset.gross_purchase_amount, "opening_accumulated_depreciation": asset.opening_accumulated_depreciation, - "depreciated_amount": depreciation_amount_map.get(asset.asset_id) or 0.0, + "depreciated_amount": get_depreciation_amount_of_asset(asset, depreciation_amount_map, filters), "available_for_use_date": asset.available_for_use_date, "location": asset.location, "asset_category": asset.asset_category, @@ -137,6 +151,7 @@ def prepare_chart_data(data, filters): filters.filter_based_on, "Monthly", company=filters.company, + ignore_fiscal_year=True, ) for d in period_list: @@ -170,25 +185,61 @@ def prepare_chart_data(data, filters): } +def get_depreciation_amount_of_asset(asset, depreciation_amount_map, filters): + if asset.calculate_depreciation: + depr_amount = depreciation_amount_map.get(asset.asset_id) or 0.0 + else: + depr_amount = get_manual_depreciation_amount_of_asset(asset, filters) + + return flt(depr_amount, 2) + + def get_finance_book_value_map(filters): date = filters.to_date if filters.filter_based_on == "Date Range" else filters.year_end_date return frappe._dict( frappe.db.sql( """ Select - parent, SUM(depreciation_amount) - FROM `tabDepreciation Schedule` + ads.asset, SUM(depreciation_amount) + FROM `tabAsset Depreciation Schedule` ads, `tabDepreciation Schedule` ds WHERE - parentfield='schedules' - AND schedule_date<=%s - AND journal_entry IS NOT NULL - AND ifnull(finance_book, '')=%s - GROUP BY parent""", - (date, cstr(filters.finance_book or "")), + ds.parent = ads.name + AND ifnull(ads.finance_book, '')=%s + AND ads.docstatus=1 + AND ds.parentfield='depreciation_schedule' + AND ds.schedule_date<=%s + AND ds.journal_entry IS NOT NULL + GROUP BY ads.asset""", + (cstr(filters.finance_book or ""), date), ) ) +def get_manual_depreciation_amount_of_asset(asset, filters): + date = filters.to_date if filters.filter_based_on == "Date Range" else filters.year_end_date + + (_, _, depreciation_expense_account) = get_depreciation_accounts(asset) + + gle = frappe.qb.DocType("GL Entry") + + result = ( + frappe.qb.from_(gle) + .select(Sum(gle.debit)) + .where(gle.against_voucher == asset.asset_id) + .where(gle.account == depreciation_expense_account) + .where(gle.debit != 0) + .where(gle.is_cancelled == 0) + .where(gle.posting_date <= date) + ).run() + + if result and result[0] and result[0][0]: + depr_amount = result[0][0] + else: + depr_amount = 0 + + return depr_amount + + def get_purchase_receipt_supplier_map(): return frappe._dict( frappe.db.sql( diff --git a/erpnext/assets/workspace/assets/assets.json b/erpnext/assets/workspace/assets/assets.json index 26a6609b31..c07155e48a 100644 --- a/erpnext/assets/workspace/assets/assets.json +++ b/erpnext/assets/workspace/assets/assets.json @@ -130,6 +130,17 @@ "onboard": 0, "type": "Link" }, + { + "dependencies": "Asset", + "hidden": 0, + "is_query_report": 0, + "label": "Asset Capitalization", + "link_count": 0, + "link_to": "Asset Capitalization", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, { "hidden": 0, "is_query_report": 0, @@ -172,7 +183,7 @@ "type": "Link" } ], - "modified": "2022-01-13 17:25:41.730628", + "modified": "2022-01-13 18:25:41.730628", "modified_by": "Administrator", "module": "Assets", "name": "Assets", @@ -205,4 +216,4 @@ } ], "title": "Assets" -} \ No newline at end of file +} diff --git a/erpnext/bulk_transaction/doctype/bulk_transaction_log/test_bulk_transaction_log.py b/erpnext/bulk_transaction/doctype/bulk_transaction_log/test_bulk_transaction_log.py index 646dba51ce..c673be89b3 100644 --- a/erpnext/bulk_transaction/doctype/bulk_transaction_log/test_bulk_transaction_log.py +++ b/erpnext/bulk_transaction/doctype/bulk_transaction_log/test_bulk_transaction_log.py @@ -15,17 +15,6 @@ class TestBulkTransactionLog(unittest.TestCase): create_customer() create_item() - def test_for_single_record(self): - so_name = create_so() - transaction_processing([{"name": so_name}], "Sales Order", "Sales Invoice") - data = frappe.db.get_list( - "Sales Invoice", - filters={"posting_date": date.today(), "customer": "Bulk Customer"}, - fields=["*"], - ) - if not data: - self.fail("No Sales Invoice Created !") - def test_entry_in_log(self): so_name = create_so() transaction_processing([{"name": so_name}], "Sales Order", "Sales Invoice") diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json index 6c18a4650b..652dcf0d43 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.json +++ b/erpnext/buying/doctype/buying_settings/buying_settings.json @@ -9,8 +9,8 @@ "supplier_and_price_defaults_section", "supp_master_name", "supplier_group", - "column_break_4", "buying_price_list", + "column_break_4", "maintain_same_rate_action", "role_to_override_stop_action", "transaction_settings_section", @@ -20,7 +20,8 @@ "maintain_same_rate", "allow_multiple_items", "bill_for_rejected_quantity_in_purchase_invoice", - "enable_discount_accounting", + "disable_last_purchase_rate", + "show_pay_button", "subcontract", "backflush_raw_materials_of_subcontract_based_on", "column_break_11", @@ -72,11 +73,11 @@ }, { "fieldname": "subcontract", - "fieldtype": "Section Break", + "fieldtype": "Tab Break", "label": "Subcontracting Settings" }, { - "default": "Material Transferred for Subcontract", + "default": "BOM", "fieldname": "backflush_raw_materials_of_subcontract_based_on", "fieldtype": "Select", "label": "Backflush Raw Materials of Subcontract Based On", @@ -119,8 +120,8 @@ }, { "fieldname": "supplier_and_price_defaults_section", - "fieldtype": "Section Break", - "label": "Supplier and Price Defaults" + "fieldtype": "Tab Break", + "label": "Naming Series and Price Defaults" }, { "fieldname": "column_break_4", @@ -128,7 +129,7 @@ }, { "fieldname": "transaction_settings_section", - "fieldtype": "Section Break", + "fieldtype": "Tab Break", "label": "Transaction Settings" }, { @@ -137,10 +138,15 @@ }, { "default": "0", - "description": "If enabled, additional ledger entries will be made for discounts in a separate Discount Account", - "fieldname": "enable_discount_accounting", + "fieldname": "disable_last_purchase_rate", "fieldtype": "Check", - "label": "Enable Discount Accounting for Buying" + "label": "Disable Last Purchase Rate" + }, + { + "default": "1", + "fieldname": "show_pay_button", + "fieldtype": "Check", + "label": "Show Pay Button in Purchase Order Portal" } ], "icon": "fa fa-cog", @@ -148,7 +154,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2022-05-31 19:40:26.103909", + "modified": "2023-02-15 14:42:10.200679", "modified_by": "Administrator", "module": "Buying", "name": "Buying Settings", diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.py b/erpnext/buying/doctype/buying_settings/buying_settings.py index 7b18cdbedc..be1ebdeb64 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.py +++ b/erpnext/buying/doctype/buying_settings/buying_settings.py @@ -5,15 +5,10 @@ import frappe -from frappe.custom.doctype.property_setter.property_setter import make_property_setter from frappe.model.document import Document -from frappe.utils import cint class BuyingSettings(Document): - def on_update(self): - self.toggle_discount_accounting_fields() - def validate(self): for key in ["supplier_group", "supp_master_name", "maintain_same_rate", "buying_price_list"]: frappe.db.set_default(key, self.get(key, "")) @@ -26,60 +21,3 @@ class BuyingSettings(Document): self.get("supp_master_name") == "Naming Series", hide_name_field=False, ) - - def toggle_discount_accounting_fields(self): - enable_discount_accounting = cint(self.enable_discount_accounting) - - make_property_setter( - "Purchase Invoice Item", - "discount_account", - "hidden", - not (enable_discount_accounting), - "Check", - validate_fields_for_doctype=False, - ) - if enable_discount_accounting: - make_property_setter( - "Purchase Invoice Item", - "discount_account", - "mandatory_depends_on", - "eval: doc.discount_amount", - "Code", - validate_fields_for_doctype=False, - ) - else: - make_property_setter( - "Purchase Invoice Item", - "discount_account", - "mandatory_depends_on", - "", - "Code", - validate_fields_for_doctype=False, - ) - - make_property_setter( - "Purchase Invoice", - "additional_discount_account", - "hidden", - not (enable_discount_accounting), - "Check", - validate_fields_for_doctype=False, - ) - if enable_discount_accounting: - make_property_setter( - "Purchase Invoice", - "additional_discount_account", - "mandatory_depends_on", - "eval: doc.discount_amount", - "Code", - validate_fields_for_doctype=False, - ) - else: - make_property_setter( - "Purchase Invoice", - "additional_discount_account", - "mandatory_depends_on", - "", - "Code", - validate_fields_for_doctype=False, - ) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index fbb42fe2f6..47089f7d85 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -33,6 +33,7 @@ frappe.ui.form.on("Purchase Order", { frm.set_query("fg_item", "items", function() { return { filters: { + 'is_stock_item': 1, 'is_sub_contracted_item': 1, 'default_bom': ['!=', ''] } @@ -100,6 +101,11 @@ frappe.ui.form.on("Purchase Order", { erpnext.queries.setup_queries(frm, "Warehouse", function() { return erpnext.queries.warehouse(frm.doc); }); + + // On cancel and amending a purchase order with advance payment, reset advance paid amount + if (frm.is_new()) { + frm.set_value("advance_paid", 0) + } }, apply_tds: function(frm) { @@ -229,11 +235,11 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e cur_frm.add_custom_button(__('Purchase Invoice'), this.make_purchase_invoice, __('Create')); - if(flt(doc.per_billed)==0 && doc.status != "Delivered") { + if(flt(doc.per_billed) < 100 && doc.status != "Delivered") { cur_frm.add_custom_button(__('Payment'), cur_frm.cscript.make_payment_entry, __('Create')); } - if(flt(doc.per_billed)==0) { + if(flt(doc.per_billed) < 100) { this.frm.add_custom_button(__('Payment Request'), function() { me.make_payment_request() }, __('Create')); } @@ -295,131 +301,12 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e } make_stock_entry() { - var items = $.map(cur_frm.doc.items, function(d) { return d.bom ? d.item_code : false; }); - var me = this; - - if(items.length >= 1){ - me.raw_material_data = []; - me.show_dialog = 1; - let title = __('Transfer Material to Supplier'); - let fields = [ - {fieldtype:'Section Break', label: __('Raw Materials')}, - {fieldname: 'sub_con_rm_items', fieldtype: 'Table', label: __('Items'), - fields: [ - { - fieldtype:'Data', - fieldname:'item_code', - label: __('Item'), - read_only:1, - in_list_view:1 - }, - { - fieldtype:'Data', - fieldname:'rm_item_code', - label: __('Raw Material'), - read_only:1, - in_list_view:1 - }, - { - fieldtype:'Float', - read_only:1, - fieldname:'qty', - label: __('Quantity'), - read_only:1, - in_list_view:1 - }, - { - fieldtype:'Data', - read_only:1, - fieldname:'warehouse', - label: __('Reserve Warehouse'), - in_list_view:1 - }, - { - fieldtype:'Float', - read_only:1, - fieldname:'rate', - label: __('Rate'), - hidden:1 - }, - { - fieldtype:'Float', - read_only:1, - fieldname:'amount', - label: __('Amount'), - hidden:1 - }, - { - fieldtype:'Link', - read_only:1, - fieldname:'uom', - label: __('UOM'), - hidden:1 - } - ], - data: me.raw_material_data, - get_data: function() { - return me.raw_material_data; - } - } - ] - - me.dialog = new frappe.ui.Dialog({ - title: title, fields: fields - }); - - if (me.frm.doc['supplied_items']) { - me.frm.doc['supplied_items'].forEach((item, index) => { - if (item.rm_item_code && item.main_item_code && item.required_qty - item.supplied_qty != 0) { - me.raw_material_data.push ({ - 'name':item.name, - 'item_code': item.main_item_code, - 'rm_item_code': item.rm_item_code, - 'item_name': item.rm_item_code, - 'qty': item.required_qty - item.supplied_qty, - 'warehouse':item.reserve_warehouse, - 'rate':item.rate, - 'amount':item.amount, - 'stock_uom':item.stock_uom - }); - me.dialog.fields_dict.sub_con_rm_items.grid.refresh(); - } - }) - } - - me.dialog.get_field('sub_con_rm_items').check_all_rows() - - me.dialog.show() - this.dialog.set_primary_action(__('Transfer'), function() { - me.values = me.dialog.get_values(); - if(me.values) { - me.values.sub_con_rm_items.map((row,i) => { - if (!row.item_code || !row.rm_item_code || !row.warehouse || !row.qty || row.qty === 0) { - let row_id = i+1; - frappe.throw(__("Item Code, warehouse and quantity are required on row {0}", [row_id])); - } - }) - me._make_rm_stock_entry(me.dialog.fields_dict.sub_con_rm_items.grid.get_selected_children()) - me.dialog.hide() - } - }); - } - - me.dialog.get_close_btn().on('click', () => { - me.dialog.hide(); - }); - - } - - _make_rm_stock_entry(rm_items) { frappe.call({ method:"erpnext.controllers.subcontracting_controller.make_rm_stock_entry", args: { subcontract_order: cur_frm.doc.name, - rm_items: rm_items, order_doctype: cur_frm.doc.doctype - } - , + }, callback: function(r) { var doclist = frappe.model.sync(r.message); frappe.set_route("Form", doclist[0].doctype, doclist[0].name); diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index aa50487d78..29afc8476e 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -12,43 +12,24 @@ "title", "naming_series", "supplier", - "get_items_from_open_material_requests", "supplier_name", + "order_confirmation_no", + "order_confirmation_date", + "get_items_from_open_material_requests", + "column_break_7", + "transaction_date", + "schedule_date", + "column_break1", + "company", "apply_tds", "tax_withholding_category", "is_subcontracted", "supplier_warehouse", - "column_break1", - "company", - "transaction_date", - "schedule_date", - "order_confirmation_no", - "order_confirmation_date", "amended_from", "accounting_dimensions_section", "cost_center", "dimension_col_break", "project", - "drop_ship", - "customer", - "customer_name", - "column_break_19", - "customer_contact_person", - "customer_contact_display", - "customer_contact_mobile", - "customer_contact_email", - "section_addresses", - "supplier_address", - "address_display", - "contact_person", - "contact_display", - "contact_mobile", - "contact_email", - "col_break_address", - "shipping_address", - "shipping_address_display", - "billing_address", - "billing_address_display", "currency_and_price_list", "currency", "conversion_rate", @@ -57,21 +38,24 @@ "price_list_currency", "plc_conversion_rate", "ignore_pricing_rule", - "section_break_45", "before_items_section", "scan_barcode", + "set_from_warehouse", "items_col_break", "set_warehouse", "items_section", "items", "sb_last_purchase", "total_qty", + "total_net_weight", + "column_break_40", "base_total", "base_net_total", "column_break_26", - "total_net_weight", "total", "net_total", + "tax_withholding_net_total", + "base_tax_withholding_net_total", "section_break_48", "pricing_rules", "raw_material_details", @@ -79,13 +63,14 @@ "supplied_items", "taxes_section", "tax_category", - "column_break_50", - "shipping_rule", - "section_break_52", "taxes_and_charges", + "column_break_53", + "shipping_rule", + "column_break_50", + "incoterm", + "named_place", + "section_break_52", "taxes", - "sec_tax_breakup", - "other_charges_calculation", "totals", "base_taxes_and_charges_added", "base_taxes_and_charges_deducted", @@ -94,12 +79,6 @@ "taxes_and_charges_added", "taxes_and_charges_deducted", "total_taxes_and_charges", - "discount_section", - "apply_discount_on", - "base_discount_amount", - "column_break_45", - "additional_discount_percentage", - "discount_amount", "totals_section", "base_grand_total", "base_rounding_adjustment", @@ -112,37 +91,73 @@ "disable_rounded_total", "in_words", "advance_paid", + "discount_section", + "apply_discount_on", + "base_discount_amount", + "column_break_45", + "additional_discount_percentage", + "discount_amount", + "sec_tax_breakup", + "other_charges_calculation", + "address_and_contact_tab", + "section_addresses", + "supplier_address", + "address_display", + "col_break_address", + "contact_person", + "contact_display", + "contact_mobile", + "contact_email", + "shipping_address_section", + "shipping_address", + "column_break_99", + "shipping_address_display", + "company_billing_address_section", + "billing_address", + "column_break_103", + "billing_address_display", + "drop_ship", + "customer", + "customer_name", + "column_break_19", + "customer_contact_person", + "customer_contact_display", + "customer_contact_mobile", + "customer_contact_email", + "terms_tab", "payment_schedule_section", "payment_terms_template", "payment_schedule", + "terms_section_break", + "tc_name", + "terms", + "more_info_tab", "tracking_section", "status", "column_break_75", "per_billed", "per_received", - "terms_section_break", - "tc_name", - "terms", "column_break5", "letter_head", - "select_print_heading", - "column_break_86", - "language", "group_same_items", + "column_break_86", + "select_print_heading", + "language", "subscription_section", "from_date", "to_date", "column_break_97", "auto_repeat", "update_auto_repeat_reference", - "more_info", + "additional_info_section", + "is_internal_supplier", + "represents_company", "ref_sq", "column_break_74", "party_account_currency", - "is_internal_supplier", - "represents_company", "inter_company_order_reference", - "is_old_subcontracting_flow" + "is_old_subcontracting_flow", + "dashboard" ], "fields": [ { @@ -266,8 +281,9 @@ "read_only": 1 }, { + "depends_on": "eval:doc.customer", "fieldname": "drop_ship", - "fieldtype": "Section Break", + "fieldtype": "Tab Break", "label": "Drop Ship" }, { @@ -298,7 +314,6 @@ { "fieldname": "customer_contact_display", "fieldtype": "Small Text", - "hidden": 1, "label": "Customer Contact", "print_hide": 1 }, @@ -318,10 +333,9 @@ "print_hide": 1 }, { - "collapsible": 1, "fieldname": "section_addresses", "fieldtype": "Section Break", - "label": "Address and Contact" + "label": "Supplier Address" }, { "fieldname": "supplier_address", @@ -371,7 +385,7 @@ { "fieldname": "shipping_address", "fieldtype": "Link", - "label": "Company Shipping Address", + "label": "Shipping Address", "options": "Address", "print_hide": 1 }, @@ -440,12 +454,10 @@ "fieldname": "ignore_pricing_rule", "fieldtype": "Check", "label": "Ignore Pricing Rule", - "no_copy": 1, "permlevel": 1, "print_hide": 1 }, { - "description": "Sets 'Warehouse' in each row of the Items table.", "fieldname": "set_warehouse", "fieldtype": "Link", "label": "Set Target Warehouse", @@ -483,7 +495,6 @@ "allow_bulk_edit": 1, "fieldname": "items", "fieldtype": "Table", - "label": "Items", "oldfieldname": "po_details", "oldfieldtype": "Table", "options": "Purchase Order Item", @@ -570,6 +581,7 @@ "read_only": 1 }, { + "depends_on": "total_net_weight", "fieldname": "total_net_weight", "fieldtype": "Float", "label": "Total Net Weight", @@ -579,6 +591,8 @@ { "fieldname": "taxes_section", "fieldtype": "Section Break", + "hide_border": 1, + "label": "Taxes and Charges", "oldfieldtype": "Section Break", "options": "fa fa-money" }, @@ -633,7 +647,6 @@ { "fieldname": "totals", "fieldtype": "Section Break", - "label": "Taxes and Charges", "oldfieldtype": "Section Break", "options": "fa fa-money" }, @@ -698,7 +711,6 @@ "read_only": 1 }, { - "depends_on": "total_taxes_and_charges", "fieldname": "total_taxes_and_charges", "fieldtype": "Currency", "label": "Total Taxes and Charges", @@ -708,7 +720,6 @@ }, { "collapsible": 1, - "collapsible_depends_on": "apply_discount_on", "fieldname": "discount_section", "fieldtype": "Section Break", "label": "Additional Discount" @@ -773,7 +784,6 @@ "read_only": 1 }, { - "description": "In Words will be visible once you save the Purchase Order.", "fieldname": "base_in_words", "fieldtype": "Data", "label": "In Words (Company Currency)", @@ -851,7 +861,6 @@ "read_only": 1 }, { - "collapsible": 1, "fieldname": "payment_schedule_section", "fieldtype": "Section Break", "label": "Payment Terms" @@ -871,11 +880,10 @@ "print_hide": 1 }, { - "collapsible": 1, "collapsible_depends_on": "terms", "fieldname": "terms_section_break", "fieldtype": "Section Break", - "label": "Terms and Conditions", + "label": "Terms & Conditions", "oldfieldtype": "Section Break", "options": "fa fa-legal" }, @@ -895,13 +903,6 @@ "oldfieldname": "terms", "oldfieldtype": "Text Editor" }, - { - "collapsible": 1, - "fieldname": "more_info", - "fieldtype": "Section Break", - "label": "More Information", - "oldfieldtype": "Section Break" - }, { "default": "Draft", "fieldname": "status", @@ -1023,7 +1024,7 @@ "collapsible": 1, "fieldname": "subscription_section", "fieldtype": "Section Break", - "label": "Subscription Section" + "label": "Auto Repeat" }, { "allow_on_submit": 1, @@ -1098,7 +1099,9 @@ }, { "fieldname": "before_items_section", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "hide_border": 1, + "label": "Items" }, { "fieldname": "items_col_break", @@ -1109,7 +1112,8 @@ "fetch_from": "supplier.is_internal_supplier", "fieldname": "is_internal_supplier", "fieldtype": "Check", - "label": "Is Internal Supplier" + "label": "Is Internal Supplier", + "read_only": 1 }, { "fetch_from": "supplier.represents_company", @@ -1133,10 +1137,6 @@ "label": "Tax Withholding Category", "options": "Tax Withholding Category" }, - { - "fieldname": "section_break_45", - "fieldtype": "Section Break" - }, { "collapsible": 1, "fieldname": "accounting_dimensions_section", @@ -1166,13 +1166,112 @@ "hidden": 1, "label": "Is Old Subcontracting Flow", "read_only": 1 + }, + { + "depends_on": "is_internal_supplier", + "fieldname": "set_from_warehouse", + "fieldtype": "Link", + "label": "Set From Warehouse", + "options": "Warehouse" + }, + { + "fieldname": "terms_tab", + "fieldtype": "Tab Break", + "label": "Terms" + }, + { + "fieldname": "more_info_tab", + "fieldtype": "Tab Break", + "label": "More Info" + }, + { + "fieldname": "dashboard", + "fieldtype": "Tab Break", + "label": "Dashboard", + "show_dashboard": 1 + }, + { + "fieldname": "column_break_7", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_40", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_53", + "fieldtype": "Column Break" + }, + { + "fieldname": "address_and_contact_tab", + "fieldtype": "Tab Break", + "label": "Address & Contact" + }, + { + "fieldname": "company_billing_address_section", + "fieldtype": "Section Break", + "label": "Company Billing Address" + }, + { + "collapsible": 1, + "fieldname": "additional_info_section", + "fieldtype": "Section Break", + "label": "Additional Info", + "oldfieldtype": "Section Break" + }, + { + "default": "0", + "depends_on": "apply_tds", + "fieldname": "tax_withholding_net_total", + "fieldtype": "Currency", + "hidden": 1, + "label": "Tax Withholding Net Total", + "no_copy": 1, + "options": "currency", + "read_only": 1 + }, + { + "depends_on": "apply_tds", + "fieldname": "base_tax_withholding_net_total", + "fieldtype": "Currency", + "hidden": 1, + "label": "Base Tax Withholding Net Total", + "no_copy": 1, + "options": "Company:company:default_currency", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "column_break_99", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_103", + "fieldtype": "Column Break" + }, + { + "fieldname": "incoterm", + "fieldtype": "Link", + "label": "Incoterm", + "options": "Incoterm" + }, + { + "depends_on": "incoterm", + "fieldname": "named_place", + "fieldtype": "Data", + "label": "Named Place" + }, + { + "fieldname": "shipping_address_section", + "fieldtype": "Section Break", + "label": "Shipping Address" } ], "icon": "fa fa-file-text", "idx": 105, "is_submittable": 1, "links": [], - "modified": "2022-06-15 15:40:58.527065", + "modified": "2023-01-28 18:59:16.322824", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order", diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index cd58d25136..2415aec8cb 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -18,7 +18,7 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import ( from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import ( get_party_tax_withholding_details, ) -from erpnext.accounts.party import get_party_account_currency +from erpnext.accounts.party import get_party_account, get_party_account_currency from erpnext.buying.utils import check_on_hold_or_closed_status, validate_for_items from erpnext.controllers.buying_controller import BuyingController from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults @@ -207,31 +207,32 @@ class PurchaseOrder(BuyingController): ) def validate_fg_item_for_subcontracting(self): - if self.is_subcontracted and not self.is_old_subcontracting_flow: + if self.is_subcontracted: + if not self.is_old_subcontracting_flow: + for item in self.items: + if not item.fg_item: + frappe.throw( + _("Row #{0}: Finished Good Item is not specified for service item {1}").format( + item.idx, item.item_code + ) + ) + else: + if not frappe.get_value("Item", item.fg_item, "is_sub_contracted_item"): + frappe.throw( + _("Row #{0}: Finished Good Item {1} must be a sub-contracted item").format( + item.idx, item.fg_item + ) + ) + elif not frappe.get_value("Item", item.fg_item, "default_bom"): + frappe.throw( + _("Row #{0}: Default BOM not found for FG Item {1}").format(item.idx, item.fg_item) + ) + if not item.fg_item_qty: + frappe.throw(_("Row #{0}: Finished Good Item Qty can not be zero").format(item.idx)) + else: for item in self.items: - if not item.fg_item: - frappe.throw( - _("Row #{0}: Finished Good Item is not specified for service item {1}").format( - item.idx, item.item_code - ) - ) - else: - if not frappe.get_value("Item", item.fg_item, "is_sub_contracted_item"): - frappe.throw( - _( - "Row #{0}: Finished Good Item {1} must be a sub-contracted item for service item {2}" - ).format(item.idx, item.fg_item, item.item_code) - ) - elif not frappe.get_value("Item", item.fg_item, "default_bom"): - frappe.throw( - _("Row #{0}: Default BOM not found for FG Item {1}").format(item.idx, item.fg_item) - ) - if not item.fg_item_qty: - frappe.throw( - _("Row #{0}: Finished Good Item Qty is not specified for service item {0}").format( - item.idx, item.item_code - ) - ) + item.set("fg_item", None) + item.set("fg_item_qty", 0) def get_schedule_dates(self): for d in self.get("items"): @@ -349,7 +350,7 @@ class PurchaseOrder(BuyingController): update_linked_doc(self.doctype, self.name, self.inter_company_order_reference) def on_cancel(self): - self.ignore_linked_doctypes = "Payment Ledger Entry" + self.ignore_linked_doctypes = ("GL Entry", "Payment Ledger Entry") super(PurchaseOrder, self).on_cancel() if self.is_against_so(): @@ -361,7 +362,7 @@ class PurchaseOrder(BuyingController): self.update_reserved_qty_for_subcontract() self.check_on_hold_or_closed_status() - frappe.db.set(self, "status", "Cancelled") + self.db_set("status", "Cancelled") self.update_prevdoc_status() @@ -558,6 +559,7 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions target.set_advances() target.set_payment_schedule() + target.credit_to = get_party_account("Supplier", source.supplier, source.company) def update_item(obj, target, source_parent): target.amount = flt(obj.amount) - flt(obj.billed_amt) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order_dashboard.py b/erpnext/buying/doctype/purchase_order/purchase_order_dashboard.py index 01b55c00d6..05b5a8e7b8 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order_dashboard.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order_dashboard.py @@ -23,5 +23,6 @@ def get_data(): "items": ["Material Request", "Supplier Quotation", "Project", "Auto Repeat"], }, {"label": _("Sub-contracting"), "items": ["Subcontracting Order", "Stock Entry"]}, + {"label": _("Internal"), "items": ["Sales Order"]}, ], } diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index bd7e4e8d86..920486a78e 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -5,10 +5,13 @@ import json import frappe -from frappe.tests.utils import FrappeTestCase +from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import add_days, flt, getdate, nowdate +from frappe.utils.data import today from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry +from erpnext.accounts.party import get_due_date_from_template +from erpnext.buying.doctype.purchase_order.purchase_order import make_inter_company_sales_order from erpnext.buying.doctype.purchase_order.purchase_order import ( make_purchase_invoice as make_pi_from_po, ) @@ -683,6 +686,12 @@ class TestPurchaseOrder(FrappeTestCase): else: raise Exception + def test_default_payment_terms(self): + due_date = get_due_date_from_template( + "_Test Payment Term Template 1", "2023-02-03", None + ).strftime("%Y-%m-%d") + self.assertEqual(due_date, "2023-03-31") + def test_terms_are_not_copied_if_automatically_fetch_payment_terms_is_unchecked(self): po = create_purchase_order(do_not_save=1) po.payment_terms_template = "_Test Payment Term Template" @@ -707,13 +716,10 @@ class TestPurchaseOrder(FrappeTestCase): pi.insert() self.assertTrue(pi.get("payment_schedule")) + @change_settings("Accounts Settings", {"unlink_advance_payment_on_cancelation_of_order": 1}) def test_advance_payment_entry_unlink_against_purchase_order(self): from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry - frappe.db.set_value( - "Accounts Settings", "Accounts Settings", "unlink_advance_payment_on_cancelation_of_order", 1 - ) - po_doc = create_purchase_order() pe = get_payment_entry("Purchase Order", po_doc.name, bank_account="_Test Bank - _TC") @@ -733,9 +739,33 @@ class TestPurchaseOrder(FrappeTestCase): pe_doc = frappe.get_doc("Payment Entry", pe.name) pe_doc.cancel() - frappe.db.set_value( - "Accounts Settings", "Accounts Settings", "unlink_advance_payment_on_cancelation_of_order", 0 - ) + @change_settings("Accounts Settings", {"unlink_advance_payment_on_cancelation_of_order": 1}) + def test_advance_paid_upon_payment_entry_cancellation(self): + from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry + + po_doc = create_purchase_order(supplier="_Test Supplier USD", currency="USD", do_not_submit=1) + po_doc.conversion_rate = 80 + po_doc.submit() + + pe = get_payment_entry("Purchase Order", po_doc.name) + pe.mode_of_payment = "Cash" + pe.paid_from = "Cash - _TC" + pe.source_exchange_rate = 1 + pe.target_exchange_rate = 80 + pe.paid_amount = po_doc.base_grand_total + pe.save(ignore_permissions=True) + pe.submit() + + po_doc.reload() + self.assertEqual(po_doc.advance_paid, po_doc.grand_total) + self.assertEqual(po_doc.party_account_currency, "USD") + + pe_doc = frappe.get_doc("Payment Entry", pe.name) + pe_doc.cancel() + + po_doc.reload() + self.assertEqual(po_doc.advance_paid, 0) + self.assertEqual(po_doc.party_account_currency, "USD") def test_schedule_date(self): po = create_purchase_order(do_not_submit=True) @@ -796,6 +826,124 @@ class TestPurchaseOrder(FrappeTestCase): automatically_fetch_payment_terms(enable=0) + def test_internal_transfer_flow(self): + from erpnext.accounts.doctype.sales_invoice.sales_invoice import ( + make_inter_company_purchase_invoice, + ) + from erpnext.selling.doctype.sales_order.sales_order import ( + make_delivery_note, + make_sales_invoice, + ) + from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt + + frappe.db.set_value("Selling Settings", None, "maintain_same_sales_rate", 1) + frappe.db.set_value("Buying Settings", None, "maintain_same_rate", 1) + + prepare_data_for_internal_transfer() + supplier = "_Test Internal Supplier 2" + + mr = make_material_request( + qty=2, company="_Test Company with perpetual inventory", warehouse="Stores - TCP1" + ) + + po = create_purchase_order( + company="_Test Company with perpetual inventory", + supplier=supplier, + warehouse="Stores - TCP1", + from_warehouse="_Test Internal Warehouse New 1 - TCP1", + qty=2, + rate=1, + material_request=mr.name, + material_request_item=mr.items[0].name, + ) + + so = make_inter_company_sales_order(po.name) + so.items[0].delivery_date = today() + self.assertEqual(so.items[0].warehouse, "_Test Internal Warehouse New 1 - TCP1") + self.assertTrue(so.items[0].purchase_order) + self.assertTrue(so.items[0].purchase_order_item) + so.submit() + + dn = make_delivery_note(so.name) + dn.items[0].target_warehouse = "_Test Internal Warehouse GIT - TCP1" + self.assertEqual(dn.items[0].warehouse, "_Test Internal Warehouse New 1 - TCP1") + self.assertTrue(dn.items[0].purchase_order) + self.assertTrue(dn.items[0].purchase_order_item) + + self.assertEqual(po.items[0].name, dn.items[0].purchase_order_item) + dn.submit() + + pr = make_inter_company_purchase_receipt(dn.name) + self.assertEqual(pr.items[0].warehouse, "Stores - TCP1") + self.assertTrue(pr.items[0].purchase_order) + self.assertTrue(pr.items[0].purchase_order_item) + self.assertEqual(po.items[0].name, pr.items[0].purchase_order_item) + pr.submit() + + si = make_sales_invoice(so.name) + self.assertEqual(si.items[0].warehouse, "_Test Internal Warehouse New 1 - TCP1") + self.assertTrue(si.items[0].purchase_order) + self.assertTrue(si.items[0].purchase_order_item) + si.submit() + + pi = make_inter_company_purchase_invoice(si.name) + self.assertTrue(pi.items[0].purchase_order) + self.assertTrue(pi.items[0].po_detail) + pi.submit() + mr.reload() + + po.load_from_db() + self.assertEqual(po.status, "Completed") + self.assertEqual(mr.status, "Received") + + def test_variant_item_po(self): + po = create_purchase_order(item_code="_Test Variant Item", qty=1, rate=100, do_not_save=1) + + self.assertRaises(frappe.ValidationError, po.save) + + +def prepare_data_for_internal_transfer(): + from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier + from erpnext.selling.doctype.customer.test_customer import create_internal_customer + from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt + from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + + company = "_Test Company with perpetual inventory" + + create_internal_customer( + "_Test Internal Customer 2", + company, + company, + ) + + create_internal_supplier( + "_Test Internal Supplier 2", + company, + company, + ) + + warehouse = create_warehouse("_Test Internal Warehouse New 1", company=company) + + create_warehouse("_Test Internal Warehouse GIT", company=company) + + make_purchase_receipt(company=company, warehouse=warehouse, qty=2, rate=100) + + if not frappe.db.get_value("Company", company, "unrealized_profit_loss_account"): + account = "Unrealized Profit and Loss - TCP1" + if not frappe.db.exists("Account", account): + frappe.get_doc( + { + "doctype": "Account", + "account_name": "Unrealized Profit and Loss", + "parent_account": "Direct Income - TCP1", + "company": company, + "is_group": 0, + "account_type": "Income Account", + } + ).insert() + + frappe.db.set_value("Company", company, "unrealized_profit_loss_account", account) + def make_pr_against_po(po, received_qty=0): pr = make_purchase_receipt(po) @@ -847,16 +995,19 @@ def create_purchase_order(**args): { "item_code": args.item or args.item_code or "_Test Item", "warehouse": args.warehouse or "_Test Warehouse - _TC", + "from_warehouse": args.from_warehouse, "qty": args.qty or 10, "rate": args.rate or 500, "schedule_date": add_days(nowdate(), 1), "include_exploded_items": args.get("include_exploded_items", 1), "against_blanket_order": args.against_blanket_order, + "material_request": args.material_request, + "material_request_item": args.material_request_item, }, ) - po.set_missing_values() if not args.do_not_save: + po.set_missing_values() po.insert() if not args.do_not_submit: if po.is_subcontracted: diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json index 1a9845396f..c645b04e12 100644 --- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json +++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json @@ -10,12 +10,14 @@ "item_code", "supplier_part_no", "item_name", + "brand", "product_bundle", "fg_item", "fg_item_qty", "column_break_4", "schedule_date", "expected_delivery_date", + "item_group", "section_break_5", "description", "col_break1", @@ -51,6 +53,7 @@ "pricing_rules", "stock_uom_rate", "is_free_item", + "apply_tds", "section_break_29", "net_rate", "net_amount", @@ -58,9 +61,12 @@ "base_net_rate", "base_net_amount", "warehouse_and_reference", + "from_warehouse", "warehouse", + "column_break_54", "actual_qty", "company_total_stock", + "references_section", "material_request", "material_request_item", "sales_order", @@ -73,8 +79,6 @@ "against_blanket_order", "blanket_order", "blanket_order_rate", - "item_group", - "brand", "section_break_56", "received_qty", "returned_qty", @@ -442,13 +446,13 @@ { "fieldname": "warehouse_and_reference", "fieldtype": "Section Break", - "label": "Warehouse and Reference" + "label": "Warehouse Settings" }, { "fieldname": "warehouse", "fieldtype": "Link", "in_list_view": 1, - "label": "Warehouse", + "label": "Target Warehouse", "oldfieldname": "warehouse", "oldfieldtype": "Link", "options": "Warehouse", @@ -760,7 +764,7 @@ "allow_on_submit": 1, "fieldname": "actual_qty", "fieldtype": "Float", - "label": "Available Qty at Warehouse", + "label": "Available Qty at Target Warehouse", "print_hide": 1, "read_only": 1 }, @@ -774,6 +778,7 @@ }, { "collapsible": 1, + "collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount", "fieldname": "discount_and_margin_section", "fieldtype": "Section Break", "label": "Discount and Margin" @@ -868,13 +873,36 @@ "fieldtype": "Float", "label": "Finished Good Item Qty", "mandatory_depends_on": "eval:parent.is_subcontracted && !parent.is_old_subcontracting_flow" + }, + { + "depends_on": "eval:parent.is_internal_supplier", + "fieldname": "from_warehouse", + "fieldtype": "Link", + "label": "From Warehouse", + "options": "Warehouse" + }, + { + "collapsible": 1, + "fieldname": "references_section", + "fieldtype": "Section Break", + "label": "References" + }, + { + "fieldname": "column_break_54", + "fieldtype": "Column Break" + }, + { + "default": "1", + "fieldname": "apply_tds", + "fieldtype": "Check", + "label": "Apply TDS" } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-06-17 05:29:40.602349", + "modified": "2022-11-29 16:47:41.364387", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order Item", 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 4e29ee53ea..2f0b7862a8 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js @@ -15,10 +15,20 @@ frappe.ui.form.on("Request for Quotation",{ frm.fields_dict["suppliers"].grid.get_field("contact").get_query = function(doc, cdt, cdn) { let d = locals[cdt][cdn]; return { - query: "erpnext.buying.doctype.request_for_quotation.request_for_quotation.get_supplier_contacts", - filters: {'supplier': d.supplier} - } + query: "frappe.contacts.doctype.contact.contact.contact_query", + filters: { + link_doctype: "Supplier", + link_name: d.supplier || "" + } + }; } + + frm.set_query('warehouse', 'items', () => ({ + filters: { + company: frm.doc.company, + is_group: 0 + } + })); }, onload: function(frm) { @@ -47,44 +57,95 @@ frappe.ui.form.on("Request for Quotation",{ }); }, __("Tools")); - frm.add_custom_button(__('Download PDF'), () => { - var suppliers = []; - const fields = [{ - fieldtype: 'Link', - label: __('Select a Supplier'), - fieldname: 'supplier', - options: 'Supplier', - reqd: 1, - get_query: () => { - return { - filters: [ - ["Supplier", "name", "in", frm.doc.suppliers.map((row) => {return row.supplier;})] - ] - } - } - }]; - - frappe.prompt(fields, data => { - var child = locals[cdt][cdn] - - var w = window.open( - frappe.urllib.get_full_url("/api/method/erpnext.buying.doctype.request_for_quotation.request_for_quotation.get_pdf?" - +"doctype="+encodeURIComponent(frm.doc.doctype) - +"&name="+encodeURIComponent(frm.doc.name) - +"&supplier="+encodeURIComponent(data.supplier) - +"&no_letterhead=0")); - if(!w) { - frappe.msgprint(__("Please enable pop-ups")); return; - } + frm.add_custom_button( + __("Download PDF"), + () => { + frappe.prompt( + [ + { + fieldtype: "Link", + label: "Select a Supplier", + fieldname: "supplier", + options: "Supplier", + reqd: 1, + default: frm.doc.suppliers?.length == 1 ? frm.doc.suppliers[0].supplier : "", + get_query: () => { + return { + filters: [ + [ + "Supplier", + "name", + "in", + frm.doc.suppliers.map((row) => { + return row.supplier; + }), + ], + ], + }; + }, + }, + { + fieldtype: "Section Break", + label: "Print Settings", + fieldname: "print_settings", + collapsible: 1, + }, + { + fieldtype: "Link", + label: "Print Format", + fieldname: "print_format", + options: "Print Format", + placeholder: "Standard", + get_query: () => { + return { + filters: { + doc_type: "Request for Quotation", + }, + }; + }, + }, + { + fieldtype: "Link", + label: "Language", + fieldname: "language", + options: "Language", + default: frappe.boot.lang, + }, + { + fieldtype: "Link", + label: "Letter Head", + fieldname: "letter_head", + options: "Letter Head", + default: frm.doc.letter_head, + }, + ], + (data) => { + var w = window.open( + frappe.urllib.get_full_url( + "/api/method/erpnext.buying.doctype.request_for_quotation.request_for_quotation.get_pdf?" + + new URLSearchParams({ + name: frm.doc.name, + supplier: data.supplier, + print_format: data.print_format || "Standard", + language: data.language || frappe.boot.lang, + letterhead: data.letter_head || frm.doc.letter_head || "", + }).toString() + ) + ); + if (!w) { + frappe.msgprint(__("Please enable pop-ups")); + return; + } + }, + "Download PDF for Supplier", + "Download" + ); }, - 'Download PDF for Supplier', - 'Download'); - }, - __("Tools")); + __("Tools") + ); - frm.page.set_inner_btn_group_as_primary(__('Create')); + frm.page.set_inner_btn_group_as_primary(__("Create")); } - }, make_supplier_quotation: function(frm) { 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 083cab78f7..bd65b0c805 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json @@ -28,6 +28,8 @@ "sec_break_email_2", "message_for_supplier", "terms_section_break", + "incoterm", + "named_place", "tc_name", "terms", "printing_settings", @@ -271,13 +273,25 @@ "fieldname": "schedule_date", "fieldtype": "Date", "label": "Required Date" + }, + { + "fieldname": "incoterm", + "fieldtype": "Link", + "label": "Incoterm", + "options": "Incoterm" + }, + { + "depends_on": "incoterm", + "fieldname": "named_place", + "fieldtype": "Data", + "label": "Named Place" } ], "icon": "fa fa-shopping-cart", "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2022-04-06 17:47:49.909000", + "modified": "2023-01-31 23:22:06.684694", "modified_by": "Administrator", "module": "Buying", "name": "Request for Quotation", @@ -345,5 +359,6 @@ "search_fields": "status, transaction_date", "show_name_in_global_search": 1, "sort_field": "modified", - "sort_order": "DESC" -} + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py index f319506ff9..7927beb823 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py @@ -3,6 +3,7 @@ import json +from typing import Optional import frappe from frappe import _ @@ -31,7 +32,7 @@ class RequestforQuotation(BuyingController): if self.docstatus < 1: # after amend and save, status still shows as cancelled, until submit - frappe.db.set(self, "status", "Draft") + self.db_set("status", "Draft") def validate_duplicate_supplier(self): supplier_list = [d.supplier for d in self.suppliers] @@ -73,14 +74,14 @@ class RequestforQuotation(BuyingController): ) def on_submit(self): - frappe.db.set(self, "status", "Submitted") + self.db_set("status", "Submitted") for supplier in self.suppliers: supplier.email_sent = 0 supplier.quote_status = "Pending" self.send_to_supplier() def on_cancel(self): - frappe.db.set(self, "status", "Cancelled") + self.db_set("status", "Cancelled") @frappe.whitelist() def get_supplier_email_preview(self, supplier): @@ -112,7 +113,7 @@ class RequestforQuotation(BuyingController): def get_link(self): # RFQ link for supplier portal - return get_url("/rfq/" + self.name) + return get_url("/app/request-for-quotation/" + self.name) def update_supplier_part_no(self, supplier): self.vendor = supplier @@ -216,6 +217,7 @@ class RequestforQuotation(BuyingController): recipients=data.email_id, sender=sender, attachments=attachments, + print_format=self.meta.default_print_format or "Standard", send_email=True, doctype=self.doctype, name=self.name, @@ -224,9 +226,7 @@ class RequestforQuotation(BuyingController): frappe.msgprint(_("Email Sent to Supplier {0}").format(data.supplier)) def get_attachments(self): - attachments = [d.name for d in get_attachments(self.doctype, self.name)] - attachments.append(frappe.attach_print(self.doctype, self.name, doc=self)) - return attachments + return [d.name for d in get_attachments(self.doctype, self.name)] def update_rfq_supplier_status(self, sup_name=None): for supplier in self.suppliers: @@ -286,18 +286,6 @@ def get_list_context(context=None): return list_context -@frappe.whitelist() -@frappe.validate_and_sanitize_search_inputs -def get_supplier_contacts(doctype, txt, searchfield, start, page_len, filters): - return frappe.db.sql( - """select `tabContact`.name from `tabContact`, `tabDynamic Link` - where `tabDynamic Link`.link_doctype = 'Supplier' and (`tabDynamic Link`.link_name=%(name)s - and `tabDynamic Link`.link_name like %(txt)s) and `tabContact`.name = `tabDynamic Link`.parent - limit %(page_len)s offset %(start)s""", - {"start": start, "page_len": page_len, "txt": "%%%s%%" % txt, "name": filters.get("supplier")}, - ) - - @frappe.whitelist() def make_supplier_quotation_from_rfq(source_name, target_doc=None, for_supplier=None): def postprocess(source, target_doc): @@ -401,17 +389,26 @@ def create_rfq_items(sq_doc, supplier, data): @frappe.whitelist() -def get_pdf(doctype, name, supplier): - doc = get_rfq_doc(doctype, name, supplier) - if doc: - download_pdf(doctype, name, doc=doc) - - -def get_rfq_doc(doctype, name, supplier): +def get_pdf( + name: str, + supplier: str, + print_format: Optional[str] = None, + language: Optional[str] = None, + letterhead: Optional[str] = None, +): + doc = frappe.get_doc("Request for Quotation", name) if supplier: - doc = frappe.get_doc(doctype, name) doc.update_supplier_part_no(supplier) - return doc + + # permissions get checked in `download_pdf` + download_pdf( + doc.doctype, + doc.name, + print_format, + doc=doc, + language=language, + letterhead=letterhead or None, + ) @frappe.whitelist() @@ -490,7 +487,7 @@ def get_rfq_containing_supplier(doctype, txt, searchfield, start, page_len, filt conditions += "and rfq.transaction_date = '{0}'".format(filters.get("transaction_date")) rfq_data = frappe.db.sql( - """ + f""" select distinct rfq.name, rfq.transaction_date, rfq.company @@ -498,15 +495,18 @@ def get_rfq_containing_supplier(doctype, txt, searchfield, start, page_len, filt `tabRequest for Quotation` rfq, `tabRequest for Quotation Supplier` rfq_supplier where rfq.name = rfq_supplier.parent - and rfq_supplier.supplier = '{0}' + and rfq_supplier.supplier = %(supplier)s and rfq.docstatus = 1 - and rfq.company = '{1}' - {2} + and rfq.company = %(company)s + {conditions} order by rfq.transaction_date ASC - limit %(page_len)s offset %(start)s """.format( - filters.get("supplier"), filters.get("company"), conditions - ), - {"page_len": page_len, "start": start}, + limit %(page_len)s offset %(start)s """, + { + "page_len": page_len, + "start": start, + "company": filters.get("company"), + "supplier": filters.get("supplier"), + }, as_dict=1, ) diff --git a/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py index 064b806e95..d250e6f18a 100644 --- a/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py @@ -8,6 +8,7 @@ from frappe.utils import nowdate from erpnext.buying.doctype.request_for_quotation.request_for_quotation import ( create_supplier_quotation, + get_pdf, make_supplier_quotation_from_rfq, ) from erpnext.crm.doctype.opportunity.opportunity import make_request_for_quotation as make_rfq @@ -124,6 +125,11 @@ class TestRequestforQuotation(FrappeTestCase): rfq.status = "Draft" rfq.submit() + def test_get_pdf(self): + rfq = make_request_for_quotation() + get_pdf(rfq.name, rfq.get("suppliers")[0].supplier) + self.assertEqual(frappe.local.response.type, "pdf") + def make_request_for_quotation(**args): """ diff --git a/erpnext/buying/doctype/supplier/supplier.json b/erpnext/buying/doctype/supplier/supplier.json index e0ee658c18..1bf7f589e2 100644 --- a/erpnext/buying/doctype/supplier/supplier.json +++ b/erpnext/buying/doctype/supplier/supplier.json @@ -10,34 +10,36 @@ "document_type": "Setup", "engine": "InnoDB", "field_order": [ - "basic_info", "naming_series", "supplier_name", "country", - "default_bank_account", - "tax_id", - "tax_category", - "tax_withholding_category", - "image", "column_break0", "supplier_group", "supplier_type", - "allow_purchase_invoice_creation_without_purchase_order", - "allow_purchase_invoice_creation_without_purchase_receipt", - "is_internal_supplier", - "represents_company", - "disabled", "is_transporter", - "warn_rfqs", - "warn_pos", - "prevent_rfqs", - "prevent_pos", - "allowed_to_transact_section", - "companies", - "section_break_7", + "image", + "defaults_section", "default_currency", + "default_bank_account", "column_break_10", "default_price_list", + "internal_supplier_section", + "is_internal_supplier", + "represents_company", + "column_break_16", + "companies", + "column_break2", + "supplier_details", + "column_break_30", + "website", + "language", + "dashboard_tab", + "tax_tab", + "tax_id", + "column_break_27", + "tax_category", + "tax_withholding_category", + "contact_and_address_tab", "address_contacts", "address_html", "column_break1", @@ -49,30 +51,26 @@ "column_break_44", "supplier_primary_address", "primary_address", - "default_payable_accounts", - "accounts", - "section_credit_limit", + "accounting_tab", "payment_terms", - "cb_21", + "accounts", + "settings_tab", + "allow_purchase_invoice_creation_without_purchase_order", + "allow_purchase_invoice_creation_without_purchase_receipt", + "column_break_54", + "is_frozen", + "disabled", + "warn_rfqs", + "warn_pos", + "prevent_rfqs", + "prevent_pos", + "block_supplier_section", "on_hold", "hold_type", - "release_date", - "default_tax_withholding_config", - "column_break2", - "website", - "supplier_details", - "column_break_30", - "language", - "is_frozen" + "column_break_59", + "release_date" ], "fields": [ - { - "fieldname": "basic_info", - "fieldtype": "Section Break", - "label": "Name and Type", - "oldfieldtype": "Section Break", - "options": "fa fa-user" - }, { "fieldname": "naming_series", "fieldtype": "Select", @@ -192,6 +190,7 @@ "default": "0", "fieldname": "warn_rfqs", "fieldtype": "Check", + "hidden": 1, "label": "Warn RFQs", "read_only": 1 }, @@ -199,6 +198,7 @@ "default": "0", "fieldname": "warn_pos", "fieldtype": "Check", + "hidden": 1, "label": "Warn POs", "read_only": 1 }, @@ -206,6 +206,7 @@ "default": "0", "fieldname": "prevent_rfqs", "fieldtype": "Check", + "hidden": 1, "label": "Prevent RFQs", "read_only": 1 }, @@ -213,15 +214,10 @@ "default": "0", "fieldname": "prevent_pos", "fieldtype": "Check", + "hidden": 1, "label": "Prevent POs", "read_only": 1 }, - { - "depends_on": "represents_company", - "fieldname": "allowed_to_transact_section", - "fieldtype": "Section Break", - "label": "Allowed To Transact With" - }, { "depends_on": "represents_company", "fieldname": "companies", @@ -229,12 +225,6 @@ "label": "Allowed To Transact With", "options": "Allowed To Transact With" }, - { - "collapsible": 1, - "fieldname": "section_break_7", - "fieldtype": "Section Break", - "label": "Currency and Price List" - }, { "fieldname": "default_currency", "fieldtype": "Link", @@ -254,22 +244,12 @@ "label": "Price List", "options": "Price List" }, - { - "collapsible": 1, - "fieldname": "section_credit_limit", - "fieldtype": "Section Break", - "label": "Payment Terms" - }, { "fieldname": "payment_terms", "fieldtype": "Link", "label": "Default Payment Terms Template", "options": "Payment Terms Template" }, - { - "fieldname": "cb_21", - "fieldtype": "Column Break" - }, { "default": "0", "fieldname": "on_hold", @@ -315,13 +295,6 @@ "label": "Contact HTML", "read_only": 1 }, - { - "collapsible": 1, - "collapsible_depends_on": "accounts", - "fieldname": "default_payable_accounts", - "fieldtype": "Section Break", - "label": "Default Payable Accounts" - }, { "description": "Mention if non-standard payable account", "fieldname": "accounts", @@ -329,12 +302,6 @@ "label": "Accounts", "options": "Party Account" }, - { - "collapsible": 1, - "fieldname": "default_tax_withholding_config", - "fieldtype": "Section Break", - "label": "Default Tax Withholding Config" - }, { "collapsible": 1, "collapsible_depends_on": "supplier_details", @@ -383,7 +350,7 @@ { "fieldname": "primary_address_and_contact_detail_section", "fieldtype": "Section Break", - "label": "Primary Address and Contact Detail" + "label": "Primary Address and Contact" }, { "description": "Reselect, if the chosen contact is edited after save", @@ -420,6 +387,64 @@ "fieldtype": "Link", "label": "Supplier Primary Address", "options": "Address" + }, + { + "fieldname": "dashboard_tab", + "fieldtype": "Tab Break", + "label": "Dashboard", + "show_dashboard": 1 + }, + { + "fieldname": "settings_tab", + "fieldtype": "Tab Break", + "label": "Settings" + }, + { + "fieldname": "contact_and_address_tab", + "fieldtype": "Tab Break", + "label": "Contact & Address" + }, + { + "fieldname": "accounting_tab", + "fieldtype": "Tab Break", + "label": "Accounting" + }, + { + "fieldname": "defaults_section", + "fieldtype": "Section Break", + "label": "Defaults" + }, + { + "fieldname": "tax_tab", + "fieldtype": "Tab Break", + "label": "Tax" + }, + { + "collapsible": 1, + "fieldname": "internal_supplier_section", + "fieldtype": "Section Break", + "label": "Internal Supplier" + }, + { + "fieldname": "column_break_16", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_27", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_54", + "fieldtype": "Column Break" + }, + { + "fieldname": "block_supplier_section", + "fieldtype": "Section Break", + "label": "Block Supplier" + }, + { + "fieldname": "column_break_59", + "fieldtype": "Column Break" } ], "icon": "fa fa-user", @@ -432,11 +457,10 @@ "link_fieldname": "party" } ], - "modified": "2022-04-16 18:02:27.838623", + "modified": "2023-02-18 11:05:50.592270", "modified_by": "Administrator", "module": "Buying", "name": "Supplier", - "name_case": "Title Case", "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ diff --git a/erpnext/buying/doctype/supplier/supplier.py b/erpnext/buying/doctype/supplier/supplier.py index 43152e89a8..120b2f8bbe 100644 --- a/erpnext/buying/doctype/supplier/supplier.py +++ b/erpnext/buying/doctype/supplier/supplier.py @@ -20,9 +20,6 @@ from erpnext.utilities.transaction_base import TransactionBase class Supplier(TransactionBase): - def get_feed(self): - return self.supplier_name - def onload(self): """Load address and contacts in `__onload`""" load_address_and_contact(self) @@ -145,7 +142,7 @@ class Supplier(TransactionBase): def after_rename(self, olddn, newdn, merge=False): if frappe.defaults.get_global_default("supp_master_name") == "Supplier Name": - frappe.db.set(self, "supplier_name", newdn) + self.db_set("supplier_name", newdn) @frappe.whitelist() diff --git a/erpnext/buying/doctype/supplier/test_supplier.py b/erpnext/buying/doctype/supplier/test_supplier.py index 55722686fe..b9fc344647 100644 --- a/erpnext/buying/doctype/supplier/test_supplier.py +++ b/erpnext/buying/doctype/supplier/test_supplier.py @@ -3,6 +3,7 @@ import frappe +from frappe.custom.doctype.property_setter.property_setter import make_property_setter from frappe.test_runner import make_test_records from erpnext.accounts.party import get_due_date @@ -152,6 +153,44 @@ class TestSupplier(FrappeTestCase): # Rollback address.delete() + def test_serach_fields_for_supplier(self): + from erpnext.controllers.queries import supplier_query + + frappe.db.set_value("Buying Settings", None, "supp_master_name", "Naming Series") + + supplier_name = create_supplier(supplier_name="Test Supplier 1").name + + make_property_setter( + "Supplier", None, "search_fields", "supplier_group", "Data", for_doctype="Doctype" + ) + + data = supplier_query( + "Supplier", supplier_name, "name", 0, 20, filters={"name": supplier_name}, as_dict=True + ) + + self.assertEqual(data[0].name, supplier_name) + self.assertEqual(data[0].supplier_group, "Services") + self.assertTrue("supplier_type" not in data[0]) + + make_property_setter( + "Supplier", + None, + "search_fields", + "supplier_group, supplier_type", + "Data", + for_doctype="Doctype", + ) + data = supplier_query( + "Supplier", supplier_name, "name", 0, 20, filters={"name": supplier_name}, as_dict=True + ) + + self.assertEqual(data[0].name, supplier_name) + self.assertEqual(data[0].supplier_group, "Services") + self.assertEqual(data[0].supplier_type, "Company") + self.assertTrue("supplier_type" in data[0]) + + frappe.db.set_value("Buying Settings", None, "supp_master_name", "Supplier Name") + def create_supplier(**args): args = frappe._dict(args) diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json index 8d1939a101..c5b369bedd 100644 --- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json +++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json @@ -13,19 +13,13 @@ "naming_series", "supplier", "supplier_name", - "column_break1", "company", + "column_break1", + "status", "transaction_date", "valid_till", "quotation_number", "amended_from", - "address_section", - "supplier_address", - "contact_person", - "address_display", - "contact_display", - "contact_mobile", - "contact_email", "currency_and_price_list", "currency", "conversion_rate", @@ -36,25 +30,25 @@ "ignore_pricing_rule", "items_section", "items", - "pricing_rule_details", - "pricing_rules", "section_break_22", "total_qty", + "total_net_weight", + "column_break_26", "base_total", "base_net_total", "column_break_24", "total", "net_total", - "total_net_weight", "taxes_section", "tax_category", - "column_break_36", - "shipping_rule", - "section_break_38", "taxes_and_charges", + "column_break_34", + "shipping_rule", + "column_break_36", + "incoterm", + "named_place", + "section_break_38", "taxes", - "tax_breakup", - "other_charges_calculation", "totals", "base_taxes_and_charges_added", "base_taxes_and_charges_deducted", @@ -80,24 +74,36 @@ "rounded_total", "in_words", "disable_rounded_total", - "terms_section_break", + "tax_breakup", + "other_charges_calculation", + "pricing_rule_details", + "pricing_rules", + "address_and_contact_tab", + "supplier_address", + "address_display", + "column_break_72", + "contact_person", + "contact_display", + "contact_mobile", + "contact_email", + "terms_tab", "tc_name", "terms", + "more_info_tab", "printing_settings", - "select_print_heading", - "group_same_items", - "column_break_72", "letter_head", + "group_same_items", + "column_break_85", + "select_print_heading", "language", "subscription_section", "auto_repeat", "update_auto_repeat_reference", "more_info", - "status", - "column_break_57", "is_subcontracted", - "reference", - "opportunity" + "column_break_57", + "opportunity", + "connections_tab" ], "fields": [ { @@ -146,7 +152,7 @@ "fieldname": "supplier_name", "fieldtype": "Data", "in_global_search": 1, - "label": "Name", + "label": "Supplier Name", "read_only": 1 }, { @@ -193,12 +199,6 @@ "reqd": 1, "search_index": 1 }, - { - "collapsible": 1, - "fieldname": "address_section", - "fieldtype": "Section Break", - "label": "Address and Contact" - }, { "fieldname": "supplier_address", "fieldtype": "Link", @@ -309,6 +309,8 @@ { "fieldname": "items_section", "fieldtype": "Section Break", + "hide_border": 1, + "label": "Items", "oldfieldtype": "Section Break", "options": "fa fa-shopping-cart" }, @@ -316,7 +318,6 @@ "allow_bulk_edit": 1, "fieldname": "items", "fieldtype": "Table", - "label": "Items", "oldfieldname": "po_details", "oldfieldtype": "Table", "options": "Supplier Quotation Item", @@ -394,6 +395,7 @@ { "fieldname": "taxes_section", "fieldtype": "Section Break", + "hide_border": 1, "label": "Taxes and Charges", "oldfieldtype": "Section Break", "options": "fa fa-money" @@ -417,7 +419,8 @@ }, { "fieldname": "section_break_38", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "hide_border": 1 }, { "fieldname": "taxes_and_charges", @@ -523,7 +526,6 @@ }, { "collapsible": 1, - "collapsible_depends_on": "discount_amount", "fieldname": "section_break_41", "fieldtype": "Section Break", "label": "Additional Discount" @@ -563,7 +565,8 @@ }, { "fieldname": "section_break_46", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "label": "Totals" }, { "fieldname": "base_grand_total", @@ -654,19 +657,10 @@ "fieldtype": "Check", "label": "Disable Rounded Total" }, - { - "collapsible": 1, - "collapsible_depends_on": "terms", - "fieldname": "terms_section_break", - "fieldtype": "Section Break", - "label": "Terms and Conditions", - "oldfieldtype": "Section Break", - "options": "fa fa-legal" - }, { "fieldname": "tc_name", "fieldtype": "Link", - "label": "Terms", + "label": "Terms Template", "oldfieldname": "tc_name", "oldfieldtype": "Link", "options": "Terms and Conditions", @@ -729,7 +723,7 @@ { "fieldname": "subscription_section", "fieldtype": "Section Break", - "label": "Auto Repeat Section" + "label": "Auto Repeat" }, { "fieldname": "auto_repeat", @@ -751,7 +745,7 @@ "collapsible": 1, "fieldname": "more_info", "fieldtype": "Section Break", - "label": "More Information", + "label": "Additional Info", "oldfieldtype": "Section Break", "options": "fa fa-file-text" }, @@ -779,11 +773,6 @@ "label": "Is Subcontracted", "print_hide": 1 }, - { - "fieldname": "reference", - "fieldtype": "Section Break", - "label": "Reference" - }, { "fieldname": "opportunity", "fieldtype": "Link", @@ -803,6 +792,51 @@ "fieldname": "quotation_number", "fieldtype": "Data", "label": "Quotation Number" + }, + { + "fieldname": "address_and_contact_tab", + "fieldtype": "Tab Break", + "label": "Address & Contact" + }, + { + "fieldname": "terms_tab", + "fieldtype": "Tab Break", + "label": "Terms" + }, + { + "fieldname": "more_info_tab", + "fieldtype": "Tab Break", + "label": "More Info" + }, + { + "fieldname": "connections_tab", + "fieldtype": "Tab Break", + "label": "Connections", + "show_dashboard": 1 + }, + { + "fieldname": "column_break_26", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_34", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_85", + "fieldtype": "Column Break" + }, + { + "fieldname": "incoterm", + "fieldtype": "Link", + "label": "Incoterm", + "options": "Incoterm" + }, + { + "depends_on": "incoterm", + "fieldname": "named_place", + "fieldtype": "Data", + "label": "Named Place" } ], "icon": "fa fa-shopping-cart", @@ -810,7 +844,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2022-03-14 16:13:20.284572", + "modified": "2022-12-12 18:35:39.740974", "modified_by": "Administrator", "module": "Buying", "name": "Supplier Quotation", diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py index c19c1df180..2dd748bc19 100644 --- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py +++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py @@ -30,11 +30,11 @@ class SupplierQuotation(BuyingController): self.validate_valid_till() def on_submit(self): - frappe.db.set(self, "status", "Submitted") + self.db_set("status", "Submitted") self.update_rfq_supplier_status(1) def on_cancel(self): - frappe.db.set(self, "status", "Cancelled") + self.db_set("status", "Cancelled") self.update_rfq_supplier_status(0) def on_trash(self): diff --git a/erpnext/buying/report/procurement_tracker/procurement_tracker.py b/erpnext/buying/report/procurement_tracker/procurement_tracker.py index d70ac46ce3..71019e8037 100644 --- a/erpnext/buying/report/procurement_tracker/procurement_tracker.py +++ b/erpnext/buying/report/procurement_tracker/procurement_tracker.py @@ -127,32 +127,27 @@ def get_columns(filters): return columns -def get_conditions(filters): - conditions = "" - +def apply_filters_on_query(filters, parent, child, query): if filters.get("company"): - conditions += " AND parent.company=%s" % frappe.db.escape(filters.get("company")) + query = query.where(parent.company == filters.get("company")) if filters.get("cost_center") or filters.get("project"): - conditions += """ - AND (child.`cost_center`=%s OR child.`project`=%s) - """ % ( - frappe.db.escape(filters.get("cost_center")), - frappe.db.escape(filters.get("project")), + query = query.where( + (child.cost_center == filters.get("cost_center")) | (child.project == filters.get("project")) ) if filters.get("from_date"): - conditions += " AND parent.transaction_date>='%s'" % filters.get("from_date") + query = query.where(parent.transaction_date >= filters.get("from_date")) if filters.get("to_date"): - conditions += " AND parent.transaction_date<='%s'" % filters.get("to_date") - return conditions + query = query.where(parent.transaction_date <= filters.get("to_date")) + + return query def get_data(filters): - conditions = get_conditions(filters) - purchase_order_entry = get_po_entries(conditions) - mr_records, procurement_record_against_mr = get_mapped_mr_details(conditions) + purchase_order_entry = get_po_entries(filters) + mr_records, procurement_record_against_mr = get_mapped_mr_details(filters) pr_records = get_mapped_pr_records() pi_records = get_mapped_pi_records() @@ -187,11 +182,15 @@ def get_data(filters): return procurement_record -def get_mapped_mr_details(conditions): +def get_mapped_mr_details(filters): mr_records = {} - mr_details = frappe.db.sql( - """ - SELECT + parent = frappe.qb.DocType("Material Request") + child = frappe.qb.DocType("Material Request Item") + + query = ( + frappe.qb.from_(parent) + .from_(child) + .select( parent.transaction_date, parent.per_ordered, parent.owner, @@ -203,18 +202,13 @@ def get_mapped_mr_details(conditions): child.uom, parent.status, child.project, - child.cost_center - FROM `tabMaterial Request` parent, `tabMaterial Request Item` child - WHERE - parent.per_ordered>=0 - AND parent.name=child.parent - AND parent.docstatus=1 - {conditions} - """.format( - conditions=conditions - ), - as_dict=1, - ) # nosec + child.cost_center, + ) + .where((parent.per_ordered >= 0) & (parent.name == child.parent) & (parent.docstatus == 1)) + ) + query = apply_filters_on_query(filters, parent, child, query) + + mr_details = query.run(as_dict=True) procurement_record_against_mr = [] for record in mr_details: @@ -241,46 +235,49 @@ def get_mapped_mr_details(conditions): def get_mapped_pi_records(): - return frappe._dict( - frappe.db.sql( - """ - SELECT - pi_item.po_detail, - pi_item.base_amount - FROM `tabPurchase Invoice Item` as pi_item - INNER JOIN `tabPurchase Order` as po - ON pi_item.`purchase_order` = po.`name` - WHERE - pi_item.docstatus = 1 - AND po.status not in ('Closed','Completed','Cancelled') - AND pi_item.po_detail IS NOT NULL - """ + po = frappe.qb.DocType("Purchase Order") + pi_item = frappe.qb.DocType("Purchase Invoice Item") + pi_records = ( + frappe.qb.from_(pi_item) + .inner_join(po) + .on(pi_item.purchase_order == po.name) + .select(pi_item.po_detail, pi_item.base_amount) + .where( + (pi_item.docstatus == 1) + & (po.status.notin(("Closed", "Completed", "Cancelled"))) + & (pi_item.po_detail.isnotnull()) ) - ) + ).run() + + return frappe._dict(pi_records) def get_mapped_pr_records(): - return frappe._dict( - frappe.db.sql( - """ - SELECT - pr_item.purchase_order_item, - pr.posting_date - FROM `tabPurchase Receipt` pr, `tabPurchase Receipt Item` pr_item - WHERE - pr.docstatus=1 - AND pr.name=pr_item.parent - AND pr_item.purchase_order_item IS NOT NULL - AND pr.status not in ('Closed','Completed','Cancelled') - """ + pr = frappe.qb.DocType("Purchase Receipt") + pr_item = frappe.qb.DocType("Purchase Receipt Item") + pr_records = ( + frappe.qb.from_(pr) + .from_(pr_item) + .select(pr_item.purchase_order_item, pr.posting_date) + .where( + (pr.docstatus == 1) + & (pr.name == pr_item.parent) + & (pr_item.purchase_order_item.isnotnull()) + & (pr.status.notin(("Closed", "Completed", "Cancelled"))) ) - ) + ).run() + + return frappe._dict(pr_records) -def get_po_entries(conditions): - return frappe.db.sql( - """ - SELECT +def get_po_entries(filters): + parent = frappe.qb.DocType("Purchase Order") + child = frappe.qb.DocType("Purchase Order Item") + + query = ( + frappe.qb.from_(parent) + .from_(child) + .select( child.name, child.parent, child.cost_center, @@ -297,17 +294,15 @@ def get_po_entries(conditions): parent.transaction_date, parent.supplier, parent.status, - parent.owner - FROM `tabPurchase Order` parent, `tabPurchase Order Item` child - WHERE - parent.docstatus = 1 - AND parent.name = child.parent - AND parent.status not in ('Closed','Completed','Cancelled') - {conditions} - GROUP BY - parent.name, child.item_code - """.format( - conditions=conditions - ), - as_dict=1, - ) # nosec + parent.owner, + ) + .where( + (parent.docstatus == 1) + & (parent.name == child.parent) + & (parent.status.notin(("Closed", "Completed", "Cancelled"))) + ) + .groupby(parent.name, child.item_code) + ) + query = apply_filters_on_query(filters, parent, child, query) + + return query.run(as_dict=True) diff --git a/erpnext/buying/report/procurement_tracker/test_procurement_tracker.py b/erpnext/buying/report/procurement_tracker/test_procurement_tracker.py index 47a66ad46f..9b53421319 100644 --- a/erpnext/buying/report/procurement_tracker/test_procurement_tracker.py +++ b/erpnext/buying/report/procurement_tracker/test_procurement_tracker.py @@ -15,60 +15,4 @@ from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse class TestProcurementTracker(FrappeTestCase): - def test_result_for_procurement_tracker(self): - filters = {"company": "_Test Procurement Company", "cost_center": "Main - _TPC"} - expected_data = self.generate_expected_data() - report = execute(filters) - - length = len(report[1]) - self.assertEqual(expected_data, report[1][length - 1]) - - def generate_expected_data(self): - if not frappe.db.exists("Company", "_Test Procurement Company"): - frappe.get_doc( - dict( - doctype="Company", - company_name="_Test Procurement Company", - abbr="_TPC", - default_currency="INR", - country="Pakistan", - ) - ).insert() - warehouse = create_warehouse("_Test Procurement Warehouse", company="_Test Procurement Company") - mr = make_material_request( - company="_Test Procurement Company", warehouse=warehouse, cost_center="Main - _TPC" - ) - po = make_purchase_order(mr.name) - po.supplier = "_Test Supplier" - po.get("items")[0].cost_center = "Main - _TPC" - po.submit() - pr = make_purchase_receipt(po.name) - pr.get("items")[0].cost_center = "Main - _TPC" - pr.submit() - date_obj = datetime.date(datetime.now()) - - po.load_from_db() - - expected_data = { - "material_request_date": date_obj, - "cost_center": "Main - _TPC", - "project": None, - "requesting_site": "_Test Procurement Warehouse - _TPC", - "requestor": "Administrator", - "material_request_no": mr.name, - "item_code": "_Test Item", - "quantity": 10.0, - "unit_of_measurement": "_Test UOM", - "status": "To Bill", - "purchase_order_date": date_obj, - "purchase_order": po.name, - "supplier": "_Test Supplier", - "estimated_cost": 0.0, - "actual_cost": 0.0, - "purchase_order_amt": po.net_total, - "purchase_order_amt_in_company_currency": po.base_net_total, - "expected_delivery_date": date_obj, - "actual_delivery_date": date_obj, - } - - return expected_data + pass diff --git a/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py index a5c464910d..e10c0e2fcc 100644 --- a/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py +++ b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py @@ -6,6 +6,7 @@ import copy import frappe from frappe import _ +from frappe.query_builder.functions import IfNull from frappe.utils import date_diff, flt, getdate @@ -16,9 +17,7 @@ def execute(filters=None): validate_filters(filters) columns = get_columns(filters) - conditions = get_conditions(filters) - - data = get_data(conditions, filters) + data = get_data(filters) if not data: return [], [], None, [] @@ -37,60 +36,61 @@ def validate_filters(filters): frappe.throw(_("To Date cannot be before From Date.")) -def get_conditions(filters): - conditions = "" - if filters.get("from_date") and filters.get("to_date"): - conditions += " and po.transaction_date between %(from_date)s and %(to_date)s" +def get_data(filters): + po = frappe.qb.DocType("Purchase Order") + po_item = frappe.qb.DocType("Purchase Order Item") + pi_item = frappe.qb.DocType("Purchase Invoice Item") - for field in ["company", "name"]: + query = ( + frappe.qb.from_(po) + .from_(po_item) + .left_join(pi_item) + .on(pi_item.po_detail == po_item.name) + .select( + po.transaction_date.as_("date"), + po_item.schedule_date.as_("required_date"), + po_item.project, + po.name.as_("purchase_order"), + po.status, + po.supplier, + po_item.item_code, + po_item.qty, + po_item.received_qty, + (po_item.qty - po_item.received_qty).as_("pending_qty"), + IfNull(pi_item.qty, 0).as_("billed_qty"), + po_item.base_amount.as_("amount"), + (po_item.received_qty * po_item.base_rate).as_("received_qty_amount"), + (po_item.billed_amt * IfNull(po.conversion_rate, 1)).as_("billed_amount"), + (po_item.base_amount - (po_item.billed_amt * IfNull(po.conversion_rate, 1))).as_( + "pending_amount" + ), + po.set_warehouse.as_("warehouse"), + po.company, + po_item.name, + ) + .where( + (po_item.parent == po.name) & (po.status.notin(("Stopped", "Closed"))) & (po.docstatus == 1) + ) + .groupby(po_item.name) + .orderby(po.transaction_date) + ) + + for field in ("company", "name"): if filters.get(field): - conditions += f" and po.{field} = %({field})s" + query = query.where(po[field] == filters.get(field)) + + if filters.get("from_date") and filters.get("to_date"): + query = query.where( + po.transaction_date.between(filters.get("from_date"), filters.get("to_date")) + ) if filters.get("status"): - conditions += " and po.status in %(status)s" + query = query.where(po.status.isin(filters.get("status"))) if filters.get("project"): - conditions += " and poi.project = %(project)s" + query = query.where(po_item.project == filters.get("project")) - return conditions - - -def get_data(conditions, filters): - data = frappe.db.sql( - """ - SELECT - po.transaction_date as date, - poi.schedule_date as required_date, - poi.project, - po.name as purchase_order, - po.status, po.supplier, poi.item_code, - poi.qty, poi.received_qty, - (poi.qty - poi.received_qty) AS pending_qty, - IFNULL(pii.qty, 0) as billed_qty, - poi.base_amount as amount, - (poi.received_qty * poi.base_rate) as received_qty_amount, - (poi.billed_amt * IFNULL(po.conversion_rate, 1)) as billed_amount, - (poi.base_amount - (poi.billed_amt * IFNULL(po.conversion_rate, 1))) as pending_amount, - po.set_warehouse as warehouse, - po.company, poi.name - FROM - `tabPurchase Order` po, - `tabPurchase Order Item` poi - LEFT JOIN `tabPurchase Invoice Item` pii - ON pii.po_detail = poi.name - WHERE - poi.parent = po.name - and po.status not in ('Stopped', 'Closed') - and po.docstatus = 1 - {0} - GROUP BY poi.name - ORDER BY po.transaction_date ASC - """.format( - conditions - ), - filters, - as_dict=1, - ) + data = query.run(as_dict=True) return data diff --git a/erpnext/buying/report/purchase_order_trends/purchase_order_trends.py b/erpnext/buying/report/purchase_order_trends/purchase_order_trends.py index dbdc62e9ec..d089473a16 100644 --- a/erpnext/buying/report/purchase_order_trends/purchase_order_trends.py +++ b/erpnext/buying/report/purchase_order_trends/purchase_order_trends.py @@ -53,4 +53,5 @@ def get_chart_data(data, conditions, filters): }, "type": "line", "lineOptions": {"regionFill": 1}, + "fieldtype": "Currency", } diff --git a/erpnext/buying/report/subcontracted_item_to_be_received/test_subcontracted_item_to_be_received.py b/erpnext/buying/report/subcontracted_item_to_be_received/test_subcontracted_item_to_be_received.py index c772c1a1b1..d13d9701f3 100644 --- a/erpnext/buying/report/subcontracted_item_to_be_received/test_subcontracted_item_to_be_received.py +++ b/erpnext/buying/report/subcontracted_item_to_be_received/test_subcontracted_item_to_be_received.py @@ -4,6 +4,8 @@ # Decompiled by https://python-decompiler.com +import copy + import frappe from frappe.tests.utils import FrappeTestCase @@ -11,10 +13,12 @@ from erpnext.buying.report.subcontracted_item_to_be_received.subcontracted_item_ execute, ) from erpnext.controllers.tests.test_subcontracting_controller import ( + get_rm_items, get_subcontracting_order, make_service_item, + make_stock_in_entry, + make_stock_transfer_entry, ) -from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import ( make_subcontracting_receipt, ) @@ -36,15 +40,18 @@ class TestSubcontractedItemToBeReceived(FrappeTestCase): sco = get_subcontracting_order( service_items=service_items, supplier_warehouse="_Test Warehouse 1 - _TC" ) - 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, + rm_items = get_rm_items(sco.supplied_items) + itemwise_details = make_stock_in_entry(rm_items=rm_items) + + for item in rm_items: + item["sco_rm_detail"] = sco.items[0].name + + make_stock_transfer_entry( + sco_no=sco.name, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), ) + make_subcontracting_receipt_against_sco(sco.name) sco.reload() col, data = execute( diff --git a/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.py b/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.py index 3013b6d160..a728290961 100644 --- a/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.py +++ b/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.py @@ -16,8 +16,7 @@ def execute(filters=None): return [], [] columns = get_columns(filters) - conditions = get_conditions(filters) - supplier_quotation_data = get_data(filters, conditions) + supplier_quotation_data = get_data(filters) data, chart_data = prepare_data(supplier_quotation_data, filters) message = get_message() @@ -25,50 +24,51 @@ def execute(filters=None): return columns, data, message, chart_data -def get_conditions(filters): - conditions = "" +def get_data(filters): + sq = frappe.qb.DocType("Supplier Quotation") + sq_item = frappe.qb.DocType("Supplier Quotation Item") + + query = ( + frappe.qb.from_(sq_item) + .from_(sq) + .select( + sq_item.parent, + sq_item.item_code, + sq_item.qty, + sq_item.stock_qty, + sq_item.amount, + sq_item.uom, + sq_item.stock_uom, + sq_item.request_for_quotation, + sq_item.lead_time_days, + sq.supplier.as_("supplier_name"), + sq.valid_till, + ) + .where( + (sq_item.parent == sq.name) + & (sq_item.docstatus < 2) + & (sq.company == filters.get("company")) + & (sq.transaction_date.between(filters.get("from_date"), filters.get("to_date"))) + ) + .orderby(sq.transaction_date, sq_item.item_code) + ) + if filters.get("item_code"): - conditions += " AND sqi.item_code = %(item_code)s" + query = query.where(sq_item.item_code == filters.get("item_code")) if filters.get("supplier_quotation"): - conditions += " AND sqi.parent in %(supplier_quotation)s" + query = query.where(sq_item.parent.isin(filters.get("supplier_quotation"))) if filters.get("request_for_quotation"): - conditions += " AND sqi.request_for_quotation = %(request_for_quotation)s" + query = query.where(sq_item.request_for_quotation == filters.get("request_for_quotation")) if filters.get("supplier"): - conditions += " AND sq.supplier in %(supplier)s" + query = query.where(sq.supplier.isin(filters.get("supplier"))) if not filters.get("include_expired"): - conditions += " AND sq.status != 'Expired'" + query = query.where(sq.status != "Expired") - return conditions - - -def get_data(filters, conditions): - supplier_quotation_data = frappe.db.sql( - """ - SELECT - sqi.parent, sqi.item_code, - sqi.qty, sqi.stock_qty, sqi.amount, - sqi.uom, sqi.stock_uom, - sqi.request_for_quotation, - sqi.lead_time_days, sq.supplier as supplier_name, sq.valid_till - FROM - `tabSupplier Quotation Item` sqi, - `tabSupplier Quotation` sq - WHERE - sqi.parent = sq.name - AND sqi.docstatus < 2 - AND sq.company = %(company)s - AND sq.transaction_date between %(from_date)s and %(to_date)s - {0} - order by sq.transaction_date, sqi.item_code""".format( - conditions - ), - filters, - as_dict=1, - ) + supplier_quotation_data = query.run(as_dict=True) return supplier_quotation_data diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 70d2bc68a1..3705fcf499 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -7,7 +7,7 @@ import json 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.query_builder.functions import Abs, Sum from frappe.utils import ( add_days, add_months, @@ -151,6 +151,7 @@ class AccountsController(TransactionBase): self.validate_inter_company_reference() self.disable_pricing_rule_on_internal_transfer() + self.disable_tax_included_prices_for_internal_transfer() self.set_incoming_rate() if self.meta.get_field("currency"): @@ -196,15 +197,25 @@ class AccountsController(TransactionBase): validate_einvoice_fields(self) - if self.doctype != "Material Request": + if self.doctype != "Material Request" and not self.ignore_pricing_rule: apply_pricing_rule_on_transaction(self) def before_cancel(self): validate_einvoice_fields(self) def on_trash(self): + # delete references in 'Repost Payment Ledger' + rpi = frappe.qb.DocType("Repost Payment Ledger Items") + frappe.qb.from_(rpi).delete().where( + (rpi.voucher_type == self.doctype) & (rpi.voucher_no == self.name) + ).run() + # delete sl and gl entries on deletion of transaction if frappe.db.get_single_value("Accounts Settings", "delete_linked_ledger_entries"): + ple = frappe.qb.DocType("Payment Ledger Entry") + frappe.qb.from_(ple).delete().where( + (ple.voucher_type == self.doctype) & (ple.voucher_no == self.name) + ).run() frappe.db.sql( "delete from `tabGL Entry` where voucher_type=%s and voucher_no=%s", (self.doctype, self.name) ) @@ -222,7 +233,7 @@ class AccountsController(TransactionBase): for item in self.get("items"): if item.get("enable_deferred_revenue") or item.get("enable_deferred_expense"): if not item.get(field_map.get(self.doctype)): - default_deferred_account = frappe.db.get_value( + default_deferred_account = frappe.get_cached_value( "Company", self.company, "default_" + field_map.get(self.doctype) ) if not default_deferred_account: @@ -234,6 +245,14 @@ class AccountsController(TransactionBase): else: item.set(field_map.get(self.doctype), default_deferred_account) + def validate_auto_repeat_subscription_dates(self): + if ( + self.get("from_date") + and self.get("to_date") + and getdate(self.from_date) > getdate(self.to_date) + ): + frappe.throw(_("To Date cannot be before From Date"), title=_("Invalid Auto Repeat Date")) + def validate_deferred_start_and_end_date(self): for d in self.items: if d.get("enable_deferred_revenue") or d.get("enable_deferred_expense"): @@ -373,7 +392,7 @@ class AccountsController(TransactionBase): ) def validate_inter_company_reference(self): - if self.doctype not in ("Purchase Invoice", "Purchase Receipt", "Purchase Order"): + if self.doctype not in ("Purchase Invoice", "Purchase Receipt"): return if self.is_internal_transfer(): @@ -381,7 +400,7 @@ class AccountsController(TransactionBase): self.get("inter_company_reference") or self.get("inter_company_invoice_reference") or self.get("inter_company_order_reference") - ): + ) and not self.get("is_return"): msg = _("Internal Sale or Delivery Reference missing.") msg += _("Please create purchase from internal sale or delivery document itself") frappe.throw(msg, title=_("Internal Sales Reference Missing")) @@ -394,6 +413,20 @@ class AccountsController(TransactionBase): alert=1, ) + def disable_tax_included_prices_for_internal_transfer(self): + if self.is_internal_transfer(): + tax_updated = False + for tax in self.get("taxes"): + if tax.get("included_in_print_rate"): + tax.included_in_print_rate = 0 + tax_updated = True + + if tax_updated: + frappe.msgprint( + _("Disabled tax included prices since this {} is an internal transfer").format(self.doctype), + alert=1, + ) + def validate_due_date(self): if self.get("is_pos"): return @@ -557,7 +590,12 @@ class AccountsController(TransactionBase): if bool(uom) != bool(stock_uom): # xor item.stock_uom = item.uom = uom or stock_uom - item.conversion_factor = get_uom_conv_factor(item.get("uom"), item.get("stock_uom")) + # UOM cannot be zero so substitute as 1 + item.conversion_factor = ( + get_uom_conv_factor(item.get("uom"), item.get("stock_uom")) + or item.get("conversion_factor") + or 1 + ) if self.doctype == "Purchase Invoice": self.set_expense_account(for_validate) @@ -567,6 +605,11 @@ class AccountsController(TransactionBase): # if user changed the discount percentage then set user's discount percentage ? if pricing_rule_args.get("price_or_product_discount") == "Price": item.set("pricing_rules", pricing_rule_args.get("pricing_rules")) + if pricing_rule_args.get("apply_rule_on_other_items"): + other_items = json.loads(pricing_rule_args.get("apply_rule_on_other_items")) + if other_items and item.item_code not in other_items: + return + item.set("discount_percentage", pricing_rule_args.get("discount_percentage")) item.set("discount_amount", pricing_rule_args.get("discount_amount")) if pricing_rule_args.get("pricing_rule_for") == "Rate": @@ -652,7 +695,7 @@ class AccountsController(TransactionBase): def validate_enabled_taxes_and_charges(self): taxes_and_charges_doctype = self.meta.get_options("taxes_and_charges") - if frappe.db.get_value(taxes_and_charges_doctype, self.taxes_and_charges, "disabled"): + if frappe.get_cached_value(taxes_and_charges_doctype, self.taxes_and_charges, "disabled"): frappe.throw( _("{0} '{1}' is disabled").format(taxes_and_charges_doctype, self.taxes_and_charges) ) @@ -660,7 +703,7 @@ class AccountsController(TransactionBase): def validate_tax_account_company(self): for d in self.get("taxes"): if d.account_head: - tax_account_company = frappe.db.get_value("Account", d.account_head, "company") + tax_account_company = frappe.get_cached_value("Account", d.account_head, "company") if tax_account_company != self.company: frappe.throw( _("Row #{0}: Account {1} does not belong to company {2}").format( @@ -795,15 +838,12 @@ class AccountsController(TransactionBase): self.set("advances", []) advance_allocated = 0 for d in res: - if d.against_order: - allocated_amount = flt(d.amount) + if self.get("party_account_currency") == self.company_currency: + amount = self.get("base_rounded_total") or self.base_grand_total else: - if self.get("party_account_currency") == self.company_currency: - amount = self.get("base_rounded_total") or self.base_grand_total - else: - amount = self.get("rounded_total") or self.grand_total + amount = self.get("rounded_total") or self.grand_total - allocated_amount = min(amount - advance_allocated, d.amount) + allocated_amount = min(amount - advance_allocated, d.amount) advance_allocated += flt(allocated_amount) advance_row = { @@ -908,7 +948,9 @@ class AccountsController(TransactionBase): party_account = self.credit_to if is_purchase_invoice else self.debit_to party_type = "Supplier" if is_purchase_invoice else "Customer" - gain_loss_account = frappe.db.get_value("Company", self.company, "exchange_gain_loss_account") + gain_loss_account = frappe.get_cached_value( + "Company", self.company, "exchange_gain_loss_account" + ) if not gain_loss_account: frappe.throw( _("Please set default Exchange Gain/Loss Account in Company {}").format(self.get("company")) @@ -1005,7 +1047,7 @@ class AccountsController(TransactionBase): else self.grand_total ), "outstanding_amount": self.outstanding_amount, - "difference_account": frappe.db.get_value( + "difference_account": frappe.get_cached_value( "Company", self.company, "exchange_gain_loss_account" ), "exchange_gain_loss": flt(d.get("exchange_gain_loss")), @@ -1109,17 +1151,17 @@ class AccountsController(TransactionBase): frappe.db.get_single_value("Selling Settings", "enable_discount_accounting") ) + if self.doctype == "Purchase Invoice": + dr_or_cr = "credit" + rev_dr_cr = "debit" + supplier_or_customer = self.supplier + + else: + dr_or_cr = "debit" + rev_dr_cr = "credit" + supplier_or_customer = self.customer + if enable_discount_accounting: - if self.doctype == "Purchase Invoice": - dr_or_cr = "credit" - rev_dr_cr = "debit" - supplier_or_customer = self.supplier - - else: - dr_or_cr = "debit" - rev_dr_cr = "credit" - supplier_or_customer = self.customer - for item in self.get("items"): if item.get("discount_amount") and item.get("discount_account"): discount_amount = item.discount_amount * item.qty @@ -1173,18 +1215,22 @@ class AccountsController(TransactionBase): ) ) - if self.get("discount_amount") and self.get("additional_discount_account"): - gl_entries.append( - self.get_gl_dict( - { - "account": self.additional_discount_account, - "against": supplier_or_customer, - dr_or_cr: self.discount_amount, - "cost_center": self.cost_center, - }, - item=self, - ) + if ( + (enable_discount_accounting or self.get("is_cash_or_non_trade_discount")) + and self.get("additional_discount_account") + and self.get("discount_amount") + ): + gl_entries.append( + self.get_gl_dict( + { + "account": self.additional_discount_account, + "against": supplier_or_customer, + dr_or_cr: self.discount_amount, + "cost_center": self.cost_center, + }, + item=self, ) + ) def validate_multiple_billing(self, ref_dt, item_ref_dn, based_on, parentfield): from erpnext.controllers.status_updater import get_allowance_for @@ -1321,30 +1367,20 @@ class AccountsController(TransactionBase): return stock_items def set_total_advance_paid(self): - if self.doctype == "Sales Order": - dr_or_cr = "credit_in_account_currency" - rev_dr_or_cr = "debit_in_account_currency" - party = self.customer - else: - dr_or_cr = "debit_in_account_currency" - rev_dr_or_cr = "credit_in_account_currency" - party = self.supplier - - advance = frappe.db.sql( - """ - select - account_currency, sum({dr_or_cr}) - sum({rev_dr_cr}) as amount - from - `tabGL Entry` - where - against_voucher_type = %s and against_voucher = %s and party=%s - and docstatus = 1 - """.format( - dr_or_cr=dr_or_cr, rev_dr_cr=rev_dr_or_cr - ), - (self.doctype, self.name, party), - as_dict=1, - ) # nosec + ple = frappe.qb.DocType("Payment Ledger Entry") + party = self.customer if self.doctype == "Sales Order" else self.supplier + advance = ( + frappe.qb.from_(ple) + .select(ple.account_currency, Abs(Sum(ple.amount_in_account_currency)).as_("amount")) + .where( + (ple.against_voucher_type == self.doctype) + & (ple.against_voucher_no == self.name) + & (ple.party == party) + & (ple.docstatus == 1) + & (ple.company == self.company) + ) + .run(as_dict=True) + ) if advance: advance = advance[0] @@ -1379,7 +1415,7 @@ class AccountsController(TransactionBase): @property def company_abbr(self): if not hasattr(self, "_abbr"): - self._abbr = frappe.db.get_value("Company", self.company, "abbr") + self._abbr = frappe.get_cached_value("Company", self.company, "abbr") return self._abbr @@ -1765,7 +1801,7 @@ class AccountsController(TransactionBase): """ if self.is_internal_transfer() and not self.unrealized_profit_loss_account: - unrealized_profit_loss_account = frappe.db.get_value( + unrealized_profit_loss_account = frappe.get_cached_value( "Company", self.company, "unrealized_profit_loss_account" ) @@ -1866,10 +1902,23 @@ class AccountsController(TransactionBase): ): throw(_("Conversion rate cannot be 0 or 1")) + def check_finance_books(self, item, asset): + if ( + len(asset.finance_books) > 1 + and not item.get("finance_book") + and not self.get("finance_book") + and asset.finance_books[0].finance_book + ): + frappe.throw( + _("Select finance book for the item {0} at row {1}").format(item.item_code, item.idx) + ) + @frappe.whitelist() def get_tax_rate(account_head): - return frappe.db.get_value("Account", account_head, ["tax_rate", "account_name"], as_dict=True) + return frappe.get_cached_value( + "Account", account_head, ["tax_rate", "account_name"], as_dict=True + ) @frappe.whitelist() @@ -1878,7 +1927,7 @@ def get_default_taxes_and_charges(master_doctype, tax_template=None, company=Non return {} if tax_template and company: - tax_template_company = frappe.db.get_value(master_doctype, tax_template, "company") + tax_template_company = frappe.get_cached_value(master_doctype, tax_template, "company") if tax_template_company == company: return @@ -2273,7 +2322,7 @@ def get_due_date(term, posting_date=None, bill_date=None): elif term.due_date_based_on == "Day(s) after the end of the invoice month": due_date = add_days(get_last_day(date), term.credit_days) elif term.due_date_based_on == "Month(s) after the end of the invoice month": - due_date = add_months(get_last_day(date), term.credit_months) + due_date = get_last_day(add_months(date, term.credit_months)) return due_date @@ -2285,7 +2334,7 @@ def get_discount_date(term, posting_date=None, bill_date=None): elif term.discount_validity_based_on == "Day(s) after the end of the invoice month": discount_validity = add_days(get_last_day(date), term.discount_validity) elif term.discount_validity_based_on == "Month(s) after the end of the invoice month": - discount_validity = add_months(get_last_day(date), term.discount_validity) + discount_validity = get_last_day(add_months(date, term.discount_validity)) return discount_validity diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 036733c0c3..4f7d9ad92e 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -6,6 +6,7 @@ import frappe from frappe import ValidationError, _, msgprint from frappe.contacts.doctype.address.address import get_address_display from frappe.utils import cint, cstr, flt, getdate +from frappe.utils.data import nowtime from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget from erpnext.accounts.party import get_party_details @@ -24,10 +25,6 @@ class BuyingController(SubcontractingController): def __setup__(self): self.flags.ignore_permlevel_for_fields = ["buying_price_list", "price_list_currency"] - def get_feed(self): - if self.get("supplier_name"): - return _("From {0} | {1} {2}").format(self.supplier_name, self.currency, self.grand_total) - def validate(self): super(BuyingController, self).validate() if getattr(self, "supplier", None) and not self.supplier_name: @@ -40,6 +37,7 @@ class BuyingController(SubcontractingController): self.validate_from_warehouse() self.set_supplier_address() self.validate_asset_return() + self.validate_auto_repeat_subscription_dates() if self.doctype == "Purchase Invoice": self.validate_purchase_receipt_if_update_stock() @@ -86,6 +84,7 @@ class BuyingController(SubcontractingController): company=self.company, party_address=self.get("supplier_address"), shipping_address=self.get("shipping_address"), + company_address=self.get("billing_address"), fetch_payment_terms_template=not self.get("ignore_default_payment_terms_template"), ignore_permissions=self.flags.ignore_permissions, ) @@ -192,16 +191,16 @@ class BuyingController(SubcontractingController): if self.meta.get_field("base_in_words"): if self.meta.get_field("base_rounded_total") and not self.is_rounded_total_disabled(): - amount = self.base_rounded_total + amount = abs(self.base_rounded_total) else: - amount = self.base_grand_total + amount = abs(self.base_grand_total) self.base_in_words = money_in_words(amount, self.company_currency) if self.meta.get_field("in_words"): if self.meta.get_field("rounded_total") and not self.is_rounded_total_disabled(): - amount = self.rounded_total + amount = abs(self.rounded_total) else: - amount = self.grand_total + amount = abs(self.grand_total) self.in_words = money_in_words(amount, self.currency) @@ -275,6 +274,9 @@ class BuyingController(SubcontractingController): if self.doctype not in ("Purchase Receipt", "Purchase Invoice", "Purchase Order"): return + if not self.is_internal_transfer(): + return + ref_doctype_map = { "Purchase Order": "Sales Order Item", "Purchase Receipt": "Delivery Note Item", @@ -288,12 +290,16 @@ class BuyingController(SubcontractingController): # Get outgoing rate based on original item cost based on valuation method if not d.get(frappe.scrub(ref_doctype)): + posting_time = self.get("posting_time") + if not posting_time and self.doctype == "Purchase Order": + posting_time = nowtime() + outgoing_rate = get_incoming_rate( { "item_code": d.item_code, "warehouse": d.get("from_warehouse"), "posting_date": self.get("posting_date") or self.get("transation_date"), - "posting_time": self.get("posting_time"), + "posting_time": posting_time, "qty": -1 * flt(d.get("stock_qty")), "serial_no": d.get("serial_no"), "batch_no": d.get("batch_no"), @@ -307,20 +313,26 @@ class BuyingController(SubcontractingController): rate = flt(outgoing_rate * (d.conversion_factor or 1), d.precision("rate")) else: - rate = frappe.db.get_value(ref_doctype, d.get(frappe.scrub(ref_doctype)), "rate") + field = "incoming_rate" if self.get("is_internal_supplier") else "rate" + rate = flt( + frappe.db.get_value(ref_doctype, d.get(frappe.scrub(ref_doctype)), field) + * (d.conversion_factor or 1), + d.precision("rate"), + ) if self.is_internal_transfer(): - if rate != d.rate: - d.rate = rate - frappe.msgprint( - _( - "Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer" - ).format(d.idx), - alert=1, - ) - d.discount_percentage = 0.0 - d.discount_amount = 0.0 - d.margin_rate_or_amount = 0.0 + if self.doctype == "Purchase Receipt" or self.get("update_stock"): + if rate != d.rate: + d.rate = rate + frappe.msgprint( + _( + "Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer" + ).format(d.idx), + alert=1, + ) + d.discount_percentage = 0.0 + d.discount_amount = 0.0 + d.margin_rate_or_amount = 0.0 def validate_for_subcontracting(self): if self.is_subcontracted and self.get("is_old_subcontracting_flow"): @@ -535,7 +547,9 @@ class BuyingController(SubcontractingController): self.process_fixed_asset() self.update_fixed_asset(field) - if self.doctype in ["Purchase Order", "Purchase Receipt"]: + if self.doctype in ["Purchase Order", "Purchase Receipt"] and not frappe.db.get_single_value( + "Buying Settings", "disable_last_purchase_rate" + ): update_last_purchase_rate(self, is_submit=1) def on_cancel(self): @@ -544,7 +558,9 @@ class BuyingController(SubcontractingController): if self.get("is_return"): return - if self.doctype in ["Purchase Order", "Purchase Receipt"]: + if self.doctype in ["Purchase Order", "Purchase Receipt"] and not frappe.db.get_single_value( + "Buying Settings", "disable_last_purchase_rate" + ): update_last_purchase_rate(self, is_submit=0) if self.doctype in ["Purchase Receipt", "Purchase Invoice"]: @@ -696,6 +712,8 @@ class BuyingController(SubcontractingController): asset.purchase_date = self.posting_date asset.supplier = self.supplier elif self.docstatus == 2: + if asset.docstatus == 2: + continue if asset.docstatus == 0: asset.set(field, None) asset.supplier = None diff --git a/erpnext/controllers/employee_boarding_controller.py b/erpnext/controllers/employee_boarding_controller.py deleted file mode 100644 index c06fb5930d..0000000000 --- a/erpnext/controllers/employee_boarding_controller.py +++ /dev/null @@ -1,193 +0,0 @@ -# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors -# License: GNU General Public License v3. See license.txt - -import frappe -from frappe import _ -from frappe.desk.form import assign_to -from frappe.model.document import Document -from frappe.utils import add_days, flt, unique - -from erpnext.setup.doctype.employee.employee import get_holiday_list_for_employee -from erpnext.setup.doctype.holiday_list.holiday_list import is_holiday - - -class EmployeeBoardingController(Document): - """ - Create the project and the task for the boarding process - Assign to the concerned person and roles as per the onboarding/separation template - """ - - def validate(self): - # remove the task if linked before submitting the form - if self.amended_from: - for activity in self.activities: - activity.task = "" - - def on_submit(self): - # create the project for the given employee onboarding - project_name = _(self.doctype) + " : " - if self.doctype == "Employee Onboarding": - project_name += self.job_applicant - else: - project_name += self.employee - - project = frappe.get_doc( - { - "doctype": "Project", - "project_name": project_name, - "expected_start_date": self.date_of_joining - if self.doctype == "Employee Onboarding" - else self.resignation_letter_date, - "department": self.department, - "company": self.company, - } - ).insert(ignore_permissions=True, ignore_mandatory=True) - - self.db_set("project", project.name) - self.db_set("boarding_status", "Pending") - self.reload() - self.create_task_and_notify_user() - - def create_task_and_notify_user(self): - # create the task for the given project and assign to the concerned person - holiday_list = self.get_holiday_list() - - for activity in self.activities: - if activity.task: - continue - - dates = self.get_task_dates(activity, holiday_list) - - task = frappe.get_doc( - { - "doctype": "Task", - "project": self.project, - "subject": activity.activity_name + " : " + self.employee_name, - "description": activity.description, - "department": self.department, - "company": self.company, - "task_weight": activity.task_weight, - "exp_start_date": dates[0], - "exp_end_date": dates[1], - } - ).insert(ignore_permissions=True) - activity.db_set("task", task.name) - - users = [activity.user] if activity.user else [] - if activity.role: - user_list = frappe.db.sql_list( - """ - SELECT - DISTINCT(has_role.parent) - FROM - `tabHas Role` has_role - LEFT JOIN `tabUser` user - ON has_role.parent = user.name - WHERE - has_role.parenttype = 'User' - AND user.enabled = 1 - AND has_role.role = %s - """, - activity.role, - ) - users = unique(users + user_list) - - if "Administrator" in users: - users.remove("Administrator") - - # assign the task the users - if users: - self.assign_task_to_users(task, users) - - def get_holiday_list(self): - if self.doctype == "Employee Separation": - return get_holiday_list_for_employee(self.employee) - else: - if self.employee: - return get_holiday_list_for_employee(self.employee) - else: - if not self.holiday_list: - frappe.throw(_("Please set the Holiday List."), frappe.MandatoryError) - else: - return self.holiday_list - - def get_task_dates(self, activity, holiday_list): - start_date = end_date = None - - if activity.begin_on is not None: - start_date = add_days(self.boarding_begins_on, activity.begin_on) - start_date = self.update_if_holiday(start_date, holiday_list) - - if activity.duration is not None: - end_date = add_days(self.boarding_begins_on, activity.begin_on + activity.duration) - end_date = self.update_if_holiday(end_date, holiday_list) - - return [start_date, end_date] - - def update_if_holiday(self, date, holiday_list): - while is_holiday(holiday_list, date): - date = add_days(date, 1) - return date - - def assign_task_to_users(self, task, users): - for user in users: - args = { - "assign_to": [user], - "doctype": task.doctype, - "name": task.name, - "description": task.description or task.subject, - "notify": self.notify_users_by_email, - } - assign_to.add(args) - - def on_cancel(self): - # delete task project - project = self.project - for task in frappe.get_all("Task", filters={"project": project}): - frappe.delete_doc("Task", task.name, force=1) - frappe.delete_doc("Project", project, force=1) - self.db_set("project", "") - for activity in self.activities: - activity.db_set("task", "") - - frappe.msgprint( - _("Linked Project {} and Tasks deleted.").format(project), alert=True, indicator="blue" - ) - - -@frappe.whitelist() -def get_onboarding_details(parent, parenttype): - return frappe.get_all( - "Employee Boarding Activity", - fields=[ - "activity_name", - "role", - "user", - "required_for_employee_creation", - "description", - "task_weight", - "begin_on", - "duration", - ], - filters={"parent": parent, "parenttype": parenttype}, - order_by="idx", - ) - - -def update_employee_boarding_status(project): - employee_onboarding = frappe.db.exists("Employee Onboarding", {"project": project.name}) - employee_separation = frappe.db.exists("Employee Separation", {"project": project.name}) - - if not (employee_onboarding or employee_separation): - return - - status = "Pending" - if flt(project.percent_complete) > 0.0 and flt(project.percent_complete) < 100.0: - status = "In Process" - elif flt(project.percent_complete) == 100.0: - status = "Completed" - - if employee_onboarding: - frappe.db.set_value("Employee Onboarding", employee_onboarding, "boarding_status", status) - elif employee_separation: - frappe.db.set_value("Employee Separation", employee_separation, "boarding_status", status) diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 243ebb66e2..b0cf724166 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -18,8 +18,9 @@ from erpnext.stock.get_item_details import _get_item_tax_template @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def employee_query(doctype, txt, searchfield, start, page_len, filters): + doctype = "Employee" conditions = [] - fields = get_fields("Employee", ["name", "employee_name"]) + fields = get_fields(doctype, ["name", "employee_name"]) return frappe.db.sql( """select {fields} from `tabEmployee` @@ -49,7 +50,8 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters): @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def lead_query(doctype, txt, searchfield, start, page_len, filters): - fields = get_fields("Lead", ["name", "lead_name", "company_name"]) + doctype = "Lead" + fields = get_fields(doctype, ["name", "lead_name", "company_name"]) return frappe.db.sql( """select {fields} from `tabLead` @@ -76,18 +78,17 @@ def lead_query(doctype, txt, searchfield, start, page_len, filters): @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs -def customer_query(doctype, txt, searchfield, start, page_len, filters): +def customer_query(doctype, txt, searchfield, start, page_len, filters, as_dict=False): + doctype = "Customer" conditions = [] cust_master_name = frappe.defaults.get_user_default("cust_master_name") - if cust_master_name == "Customer Name": - fields = ["name", "customer_group", "territory"] - else: - fields = ["name", "customer_name", "customer_group", "territory"] + fields = ["name"] + if cust_master_name != "Customer Name": + fields.append("customer_name") - fields = get_fields("Customer", fields) - - searchfields = frappe.get_meta("Customer").get_search_fields() + fields = get_fields(doctype, fields) + searchfields = frappe.get_meta(doctype).get_search_fields() searchfields = " or ".join(field + " like %(txt)s" for field in searchfields) return frappe.db.sql( @@ -109,21 +110,22 @@ def customer_query(doctype, txt, searchfield, start, page_len, filters): } ), {"txt": "%%%s%%" % txt, "_txt": txt.replace("%", ""), "start": start, "page_len": page_len}, + as_dict=as_dict, ) # searches for supplier @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs -def supplier_query(doctype, txt, searchfield, start, page_len, filters): +def supplier_query(doctype, txt, searchfield, start, page_len, filters, as_dict=False): + doctype = "Supplier" supp_master_name = frappe.defaults.get_user_default("supp_master_name") - if supp_master_name == "Supplier Name": - fields = ["name", "supplier_group"] - else: - fields = ["name", "supplier_name", "supplier_group"] + fields = ["name"] + if supp_master_name != "Supplier Name": + fields.append("supplier_name") - fields = get_fields("Supplier", fields) + fields = get_fields(doctype, fields) return frappe.db.sql( """select {field} from `tabSupplier` @@ -141,12 +143,14 @@ def supplier_query(doctype, txt, searchfield, start, page_len, filters): **{"field": ", ".join(fields), "key": searchfield, "mcond": get_match_cond(doctype)} ), {"txt": "%%%s%%" % txt, "_txt": txt.replace("%", ""), "start": start, "page_len": page_len}, + as_dict=as_dict, ) @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def tax_account_query(doctype, txt, searchfield, start, page_len, filters): + doctype = "Account" company_currency = erpnext.get_company_currency(filters.get("company")) def get_accounts(with_account_type_filter): @@ -197,30 +201,25 @@ def tax_account_query(doctype, txt, searchfield, start, page_len, filters): @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=False): + doctype = "Item" conditions = [] if isinstance(filters, str): filters = json.loads(filters) # Get searchfields from meta and use in Item Link field query - meta = frappe.get_meta("Item", cached=True) + meta = frappe.get_meta(doctype, cached=True) searchfields = meta.get_search_fields() - # these are handled separately - ignored_search_fields = ("item_name", "description") - for ignored_field in ignored_search_fields: - if ignored_field in searchfields: - searchfields.remove(ignored_field) - columns = "" - extra_searchfields = [ - field - for field in searchfields - if not field in ["name", "item_group", "description", "item_name"] - ] + extra_searchfields = [field for field in searchfields if not field in ["name", "description"]] if extra_searchfields: - columns = ", " + ", ".join(extra_searchfields) + columns += ", " + ", ".join(extra_searchfields) + + if "description" in searchfields: + columns += """, if(length(tabItem.description) > 40, \ + concat(substr(tabItem.description, 1, 40), "..."), description) as description""" searchfields = searchfields + [ field @@ -257,15 +256,13 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals filters.pop("supplier", None) description_cond = "" - if frappe.db.count("Item", cache=True) < 50000: + if frappe.db.count(doctype, cache=True) < 50000: # scan description only if items are less than 50000 description_cond = "or tabItem.description LIKE %(txt)s" + return frappe.db.sql( """select - tabItem.name, tabItem.item_name, tabItem.item_group, - if(length(tabItem.description) > 40, \ - concat(substr(tabItem.description, 1, 40), "..."), description) as description - {columns} + tabItem.name {columns} from tabItem where tabItem.docstatus < 2 and tabItem.disabled=0 @@ -300,8 +297,9 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def bom(doctype, txt, searchfield, start, page_len, filters): + doctype = "BOM" conditions = [] - fields = get_fields("BOM", ["name", "item"]) + fields = get_fields(doctype, ["name", "item"]) return frappe.db.sql( """select {fields} @@ -331,6 +329,7 @@ def bom(doctype, txt, searchfield, start, page_len, filters): @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_project_name(doctype, txt, searchfield, start, page_len, filters): + doctype = "Project" cond = "" if filters and filters.get("customer"): cond = """(`tabProject`.customer = %s or @@ -338,8 +337,8 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters): frappe.db.escape(filters.get("customer")) ) - fields = get_fields("Project", ["name", "project_name"]) - searchfields = frappe.get_meta("Project").get_search_fields() + fields = get_fields(doctype, ["name", "project_name"]) + searchfields = frappe.get_meta(doctype).get_search_fields() searchfields = " or ".join(["`tabProject`." + field + " like %(txt)s" for field in searchfields]) return frappe.db.sql( @@ -366,7 +365,8 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters): @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_delivery_notes_to_be_billed(doctype, txt, searchfield, start, page_len, filters, as_dict): - fields = get_fields("Delivery Note", ["name", "customer", "posting_date"]) + doctype = "Delivery Note" + fields = get_fields(doctype, ["name", "customer", "posting_date"]) return frappe.db.sql( """ @@ -402,6 +402,7 @@ def get_delivery_notes_to_be_billed(doctype, txt, searchfield, start, page_len, @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_batch_no(doctype, txt, searchfield, start, page_len, filters): + doctype = "Batch" cond = "" if filters.get("posting_date"): cond = "and (batch.expiry_date is null or batch.expiry_date >= %(posting_date)s)" @@ -420,7 +421,7 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters): if filters.get("is_return"): having_clause = "" - meta = frappe.get_meta("Batch", cached=True) + meta = frappe.get_meta(doctype, cached=True) searchfields = meta.get_search_fields() search_columns = "" @@ -496,6 +497,7 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters): @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_account_list(doctype, txt, searchfield, start, page_len, filters): + doctype = "Account" filter_list = [] if isinstance(filters, dict): @@ -514,7 +516,7 @@ def get_account_list(doctype, txt, searchfield, start, page_len, filters): filter_list.append([doctype, searchfield, "like", "%%%s%%" % txt]) return frappe.desk.reportview.execute( - "Account", + doctype, filters=filter_list, fields=["name", "parent_account"], limit_start=start, @@ -553,6 +555,7 @@ def get_income_account(doctype, txt, searchfield, start, page_len, filters): if not filters: filters = {} + doctype = "Account" condition = "" if filters.get("company"): condition += "and tabAccount.company = %(company)s" @@ -628,6 +631,7 @@ def get_expense_account(doctype, txt, searchfield, start, page_len, filters): if not filters: filters = {} + doctype = "Account" condition = "" if filters.get("company"): condition += "and tabAccount.company = %(company)s" @@ -650,6 +654,7 @@ def get_expense_account(doctype, txt, searchfield, start, page_len, filters): @frappe.validate_and_sanitize_search_inputs def warehouse_query(doctype, txt, searchfield, start, page_len, filters): # Should be used when item code is passed in filters. + doctype = "Warehouse" conditions, bin_conditions = [], [] filter_dict = get_doctype_wise_filters(filters) diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 04a0dfa3d4..9fcb769bc8 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -37,7 +37,7 @@ def validate_return_against(doc): if ( ref_doc.company == doc.company and ref_doc.get(party_type) == doc.get(party_type) - and ref_doc.docstatus == 1 + and ref_doc.docstatus.is_submitted() ): # validate posting date time return_posting_datetime = "%s %s" % (doc.posting_date, doc.get("posting_time") or "00:00:00") @@ -305,7 +305,7 @@ def get_returned_qty_map_for_row(return_against, party, row_name, doctype): fields += ["sum(abs(`tab{0}`.received_stock_qty)) as received_stock_qty".format(child_doctype)] # Used retrun against and supplier and is_retrun because there is an index added for it - data = frappe.db.get_list( + data = frappe.get_all( doctype, fields=fields, filters=[ @@ -326,7 +326,7 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos company = frappe.db.get_value("Delivery Note", source_name, "company") - default_warehouse_for_sales_return = frappe.db.get_value( + default_warehouse_for_sales_return = frappe.get_cached_value( "Company", company, "default_warehouse_for_sales_return" ) @@ -340,11 +340,11 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None): # look for Print Heading "Credit Note" if not doc.select_print_heading: - doc.select_print_heading = frappe.db.get_value("Print Heading", _("Credit Note")) + doc.select_print_heading = frappe.get_cached_value("Print Heading", _("Credit Note")) elif doctype == "Purchase Invoice": # look for Print Heading "Debit Note" - doc.select_print_heading = frappe.db.get_value("Print Heading", _("Debit Note")) + doc.select_print_heading = frappe.get_cached_value("Print Heading", _("Debit Note")) for tax in doc.get("taxes") or []: if tax.charge_type == "Actual": @@ -404,12 +404,17 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None): returned_qty_map = get_returned_qty_map_for_row( source_parent.name, source_parent.supplier, source_doc.name, doctype ) - target_doc.received_qty = -1 * flt( - source_doc.received_qty - (returned_qty_map.get("received_qty") or 0) - ) - target_doc.rejected_qty = -1 * flt( - source_doc.rejected_qty - (returned_qty_map.get("rejected_qty") or 0) - ) + + if doctype == "Subcontracting Receipt": + target_doc.received_qty = -1 * flt(source_doc.qty) + else: + target_doc.received_qty = -1 * flt( + source_doc.received_qty - (returned_qty_map.get("received_qty") or 0) + ) + target_doc.rejected_qty = -1 * flt( + source_doc.rejected_qty - (returned_qty_map.get("rejected_qty") or 0) + ) + target_doc.qty = -1 * flt(source_doc.qty - (returned_qty_map.get("qty") or 0)) if hasattr(target_doc, "stock_qty"): @@ -503,7 +508,7 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None): doctype + " Item": { "doctype": doctype + " Item", - "field_map": {"serial_no": "serial_no", "batch_no": "batch_no"}, + "field_map": {"serial_no": "serial_no", "batch_no": "batch_no", "bom": "bom"}, "postprocess": update_item, }, "Payment Schedule": {"doctype": "Payment Schedule", "postprocess": update_terms}, diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index a3d41ab29a..8b4d28bc7d 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -19,14 +19,11 @@ class SellingController(StockController): def __setup__(self): self.flags.ignore_permlevel_for_fields = ["selling_price_list", "price_list_currency"] - def get_feed(self): - return _("To {0} | {1} {2}").format(self.customer_name, self.currency, self.grand_total) - def onload(self): super(SellingController, self).onload() if self.doctype in ("Sales Order", "Delivery Note", "Sales Invoice"): - for item in self.get("items"): - item.update(get_bin_details(item.item_code, item.warehouse)) + for item in self.get("items") + (self.get("packed_items") or []): + item.update(get_bin_details(item.item_code, item.warehouse, include_child_warehouses=True)) def validate(self): super(SellingController, self).validate() @@ -40,6 +37,7 @@ class SellingController(StockController): self.set_customer_address() self.validate_for_duplicate_items() self.validate_target_warehouse() + self.validate_auto_repeat_subscription_dates() def set_missing_values(self, for_validate=False): @@ -311,6 +309,7 @@ class SellingController(StockController): "sales_invoice_item": d.get("sales_invoice_item"), "dn_detail": d.get("dn_detail"), "incoming_rate": p.get("incoming_rate"), + "item_row": p, } ) ) @@ -334,6 +333,7 @@ class SellingController(StockController): "sales_invoice_item": d.get("sales_invoice_item"), "dn_detail": d.get("dn_detail"), "incoming_rate": d.get("incoming_rate"), + "item_row": d, } ) ) @@ -439,24 +439,31 @@ class SellingController(StockController): # For internal transfers use incoming rate as the valuation rate if self.is_internal_transfer(): - if d.doctype == "Packed Item": - incoming_rate = flt(d.incoming_rate * d.conversion_factor, d.precision("incoming_rate")) - if d.incoming_rate != incoming_rate: - d.incoming_rate = incoming_rate - else: - rate = flt(d.incoming_rate * d.conversion_factor, d.precision("rate")) - if d.rate != rate: - d.rate = rate - frappe.msgprint( - _( - "Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer" - ).format(d.idx), - alert=1, + if self.doctype == "Delivery Note" or self.get("update_stock"): + if d.doctype == "Packed Item": + incoming_rate = flt( + flt(d.incoming_rate, d.precision("incoming_rate")) * d.conversion_factor, + d.precision("incoming_rate"), ) + if d.incoming_rate != incoming_rate: + d.incoming_rate = incoming_rate + else: + rate = flt( + flt(d.incoming_rate, d.precision("incoming_rate")) * d.conversion_factor, + d.precision("rate"), + ) + if d.rate != rate: + d.rate = rate + frappe.msgprint( + _( + "Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer" + ).format(d.idx), + alert=1, + ) - d.discount_percentage = 0.0 - d.discount_amount = 0.0 - d.margin_rate_or_amount = 0.0 + d.discount_percentage = 0.0 + d.discount_amount = 0.0 + d.margin_rate_or_amount = 0.0 elif self.get("return_against"): # Get incoming rate of return entry from reference document @@ -573,6 +580,7 @@ class SellingController(StockController): "customer_address": "address_display", "shipping_address_name": "shipping_address", "company_address": "company_address_display", + "dispatch_address_name": "dispatch_address", } for address_field, address_display_field in address_dict.items(): diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index 197d2ba2dc..dd2a67032f 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -58,7 +58,7 @@ status_map = { "eval:(self.per_delivered == 100 or self.skip_delivery_note) and self.per_billed == 100 and self.docstatus == 1", ], ["Cancelled", "eval:self.docstatus==2"], - ["Closed", "eval:self.status=='Closed'"], + ["Closed", "eval:self.status=='Closed' and self.docstatus != 2"], ["On Hold", "eval:self.status=='On Hold'"], ], "Purchase Order": [ @@ -79,7 +79,7 @@ status_map = { ["Delivered", "eval:self.status=='Delivered'"], ["Cancelled", "eval:self.docstatus==2"], ["On Hold", "eval:self.status=='On Hold'"], - ["Closed", "eval:self.status=='Closed'"], + ["Closed", "eval:self.status=='Closed' and self.docstatus != 2"], ], "Delivery Note": [ ["Draft", None], @@ -87,7 +87,7 @@ status_map = { ["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"], ["Completed", "eval:self.per_billed == 100 and self.docstatus == 1"], ["Cancelled", "eval:self.docstatus==2"], - ["Closed", "eval:self.status=='Closed'"], + ["Closed", "eval:self.status=='Closed' and self.docstatus != 2"], ], "Purchase Receipt": [ ["Draft", None], @@ -95,7 +95,7 @@ status_map = { ["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"], ["Completed", "eval:self.per_billed == 100 and self.docstatus == 1"], ["Cancelled", "eval:self.docstatus==2"], - ["Closed", "eval:self.status=='Closed'"], + ["Closed", "eval:self.status=='Closed' and self.docstatus != 2"], ], "Material Request": [ ["Draft", None], @@ -307,6 +307,20 @@ class StatusUpdater(Document): def limits_crossed_error(self, args, item, qty_or_amount): """Raise exception for limits crossed""" + if ( + self.doctype in ["Sales Invoice", "Delivery Note"] + and qty_or_amount == "amount" + and self.is_internal_customer + ): + return + + elif ( + self.doctype in ["Purchase Invoice", "Purchase Receipt"] + and qty_or_amount == "amount" + and self.is_internal_supplier + ): + return + if qty_or_amount == "qty": action_msg = _( 'To allow over receipt / delivery, update "Over Receipt/Delivery Allowance" in Stock Settings or the Item.' @@ -333,16 +347,21 @@ class StatusUpdater(Document): ) def warn_about_bypassing_with_role(self, item, qty_or_amount, role): - action = _("Over Receipt/Delivery") if qty_or_amount == "qty" else _("Overbilling") + if qty_or_amount == "qty": + msg = _("Over Receipt/Delivery of {0} {1} ignored for item {2} because you have {3} role.") + else: + msg = _("Overbilling of {0} {1} ignored for item {2} because you have {3} role.") - msg = _("{} of {} {} ignored for item {} because you have {} role.").format( - action, - _(item["target_ref_field"].title()), - frappe.bold(item["reduce_by"]), - frappe.bold(item.get("item_code")), - role, + frappe.msgprint( + msg.format( + _(item["target_ref_field"].title()), + frappe.bold(item["reduce_by"]), + frappe.bold(item.get("item_code")), + role, + ), + indicator="orange", + alert=True, ) - frappe.msgprint(msg, indicator="orange", alert=True) def update_qty(self, update_modified=True): """Updates qty or amount at row level diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index e27718a9b4..1e4fabe0d2 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -36,6 +36,10 @@ class QualityInspectionNotSubmittedError(frappe.ValidationError): pass +class BatchExpiredError(frappe.ValidationError): + pass + + class StockController(AccountsController): def validate(self): super(StockController, self).validate() @@ -53,7 +57,7 @@ class StockController(AccountsController): make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) provisional_accounting_for_non_stock_items = cint( - frappe.db.get_value( + frappe.get_cached_value( "Company", self.company, "enable_provisional_accounting_for_non_stock_items" ) ) @@ -77,6 +81,10 @@ class StockController(AccountsController): def validate_serialized_batch(self): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos + is_material_issue = False + if self.doctype == "Stock Entry" and self.purpose == "Material Issue": + is_material_issue = True + for d in self.get("items"): if hasattr(d, "serial_no") and hasattr(d, "batch_no") and d.serial_no and d.batch_no: serial_nos = frappe.get_all( @@ -93,6 +101,9 @@ class StockController(AccountsController): ) ) + if is_material_issue: + continue + if flt(d.qty) > 0.0 and d.get("batch_no") and self.get("posting_date") and self.docstatus < 2: expiry_date = frappe.get_cached_value("Batch", d.get("batch_no"), "expiry_date") @@ -100,7 +111,8 @@ class StockController(AccountsController): frappe.throw( _("Row #{0}: The batch {1} has already expired.").format( d.idx, get_link_to_form("Batch", d.get("batch_no")) - ) + ), + BatchExpiredError, ) def clean_serial_nos(self): @@ -130,13 +142,15 @@ class StockController(AccountsController): warehouse_with_no_account = [] precision = self.get_debit_field_precision() for item_row in voucher_details: - sle_list = sle_map.get(item_row.name) + sle_rounding_diff = 0.0 if sle_list: for sle in sle_list: if warehouse_account.get(sle.warehouse): # from warehouse account + sle_rounding_diff += flt(sle.stock_value_difference) + self.check_expense_account(item_row) # expense account/ target_warehouse / source_warehouse @@ -179,9 +193,49 @@ class StockController(AccountsController): elif sle.warehouse not in warehouse_with_no_account: warehouse_with_no_account.append(sle.warehouse) + if abs(sle_rounding_diff) > (1.0 / (10**precision)) and self.is_internal_transfer(): + warehouse_asset_account = "" + if self.get("is_internal_customer"): + warehouse_asset_account = warehouse_account[item_row.get("target_warehouse")]["account"] + elif self.get("is_internal_supplier"): + warehouse_asset_account = warehouse_account[item_row.get("warehouse")]["account"] + + expense_account = frappe.get_cached_value("Company", self.company, "default_expense_account") + + gl_list.append( + self.get_gl_dict( + { + "account": expense_account, + "against": warehouse_asset_account, + "cost_center": item_row.cost_center, + "project": item_row.project or self.get("project"), + "remarks": _("Rounding gain/loss Entry for Stock Transfer"), + "debit": sle_rounding_diff, + "is_opening": item_row.get("is_opening") or self.get("is_opening") or "No", + }, + warehouse_account[sle.warehouse]["account_currency"], + item=item_row, + ) + ) + + gl_list.append( + self.get_gl_dict( + { + "account": warehouse_asset_account, + "against": expense_account, + "cost_center": item_row.cost_center, + "remarks": _("Rounding gain/loss Entry for Stock Transfer"), + "credit": sle_rounding_diff, + "project": item_row.get("project") or self.get("project"), + "is_opening": item_row.get("is_opening") or self.get("is_opening") or "No", + }, + item=item_row, + ) + ) + if warehouse_with_no_account: for wh in warehouse_with_no_account: - if frappe.db.get_value("Warehouse", wh, "company"): + if frappe.get_cached_value("Warehouse", wh, "company"): frappe.throw( _( "Warehouse {0} is not linked to any account, please mention the account in the warehouse record or set default inventory account in company {1}." @@ -310,7 +364,13 @@ class StockController(AccountsController): ) if ( self.doctype - not in ("Purchase Receipt", "Purchase Invoice", "Stock Reconciliation", "Stock Entry") + not in ( + "Purchase Receipt", + "Purchase Invoice", + "Stock Reconciliation", + "Stock Entry", + "Subcontracting Receipt", + ) and not is_expense_account ): frappe.throw( @@ -372,11 +432,38 @@ class StockController(AccountsController): return sl_dict def update_inventory_dimensions(self, row, sl_dict) -> None: + # To handle delivery note and sales invoice + if row.get("item_row"): + row = row.get("item_row") + dimensions = get_evaluated_inventory_dimension(row, sl_dict, parent_doc=self) for dimension in dimensions: - if dimension and row.get(dimension.source_fieldname): + if not dimension: + continue + + if row.get(dimension.source_fieldname): sl_dict[dimension.target_fieldname] = row.get(dimension.source_fieldname) + if not sl_dict.get(dimension.target_fieldname) and dimension.fetch_from_parent: + sl_dict[dimension.target_fieldname] = self.get(dimension.fetch_from_parent) + + # Get value based on doctype name + if not sl_dict.get(dimension.target_fieldname): + fieldname = next( + ( + field.fieldname + for field in frappe.get_meta(self.doctype).fields + if field.options == dimension.fetch_from_parent + ), + None, + ) + + if fieldname and self.get(fieldname): + sl_dict[dimension.target_fieldname] = self.get(fieldname) + + if sl_dict[dimension.target_fieldname] and self.docstatus == 1: + row.db_set(dimension.source_fieldname, sl_dict[dimension.target_fieldname]) + def make_sl_entries(self, sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False): from erpnext.stock.stock_ledger import make_sl_entries @@ -655,6 +742,47 @@ class StockController(AccountsController): else: create_repost_item_valuation_entry(args) + def add_gl_entry( + self, + gl_entries, + account, + cost_center, + debit, + credit, + remarks, + against_account, + debit_in_account_currency=None, + credit_in_account_currency=None, + account_currency=None, + project=None, + voucher_detail_no=None, + item=None, + posting_date=None, + ): + + gl_entry = { + "account": account, + "cost_center": cost_center, + "debit": debit, + "credit": credit, + "against": against_account, + "remarks": remarks, + } + + if voucher_detail_no: + gl_entry.update({"voucher_detail_no": voucher_detail_no}) + + if debit_in_account_currency: + gl_entry.update({"debit_in_account_currency": debit_in_account_currency}) + + if credit_in_account_currency: + gl_entry.update({"credit_in_account_currency": credit_in_account_currency}) + + if posting_date: + gl_entry.update({"posting_date": posting_date}) + + gl_entries.append(self.get_gl_dict(gl_entry, item=item)) + def repost_required_for_queue(doc: StockController) -> bool: """check if stock document contains repeated item-warehouse with queue based valuation. diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py index 2a2f8f562e..cc80f6ca98 100644 --- a/erpnext/controllers/subcontracting_controller.py +++ b/erpnext/controllers/subcontracting_controller.py @@ -7,6 +7,7 @@ from collections import defaultdict import frappe from frappe import _ +from frappe.model.mapper import get_mapped_doc from frappe.utils import cint, cstr, flt, get_link_to_form from erpnext.controllers.stock_controller import StockController @@ -68,17 +69,30 @@ class SubcontractingController(StockController): def validate_items(self): for item in self.items: - if not frappe.get_value("Item", item.item_code, "is_sub_contracted_item"): - msg = f"Item {item.item_name} must be a subcontracted item." - frappe.throw(_(msg)) + is_stock_item, is_sub_contracted_item = frappe.get_value( + "Item", item.item_code, ["is_stock_item", "is_sub_contracted_item"] + ) + + if not is_stock_item: + frappe.throw(_("Row {0}: Item {1} must be a stock item.").format(item.idx, item.item_name)) + + if not is_sub_contracted_item: + frappe.throw( + _("Row {0}: Item {1} must be a subcontracted item.").format(item.idx, item.item_name) + ) + if item.bom: bom = frappe.get_doc("BOM", item.bom) if not bom.is_active: - msg = f"Please select an active BOM for Item {item.item_name}." - frappe.throw(_(msg)) + frappe.throw( + _("Row {0}: Please select an active BOM for Item {1}.").format(item.idx, item.item_name) + ) if bom.item != item.item_code: - msg = f"Please select an valid BOM for Item {item.item_name}." - frappe.throw(_(msg)) + frappe.throw( + _("Row {0}: Please select an valid BOM for Item {1}.").format(item.idx, item.item_name) + ) + else: + frappe.throw(_("Row {0}: Please select a BOM for Item {1}.").format(item.idx, item.item_name)) def __get_data_before_save(self): item_dict = {} @@ -395,7 +409,14 @@ class SubcontractingController(StockController): if self.available_materials.get(key) and self.available_materials[key]["batch_no"]: new_rm_obj = None for batch_no, batch_qty in self.available_materials[key]["batch_no"].items(): - if batch_qty >= qty: + if batch_qty >= qty or ( + rm_obj.consumed_qty == 0 + and self.backflush_based_on == "BOM" + and len(self.available_materials[key]["batch_no"]) == 1 + ): + if rm_obj.consumed_qty == 0: + self.__set_consumed_qty(rm_obj, qty) + self.__set_batch_no_as_per_qty(item_row, rm_obj, batch_no, qty) self.available_materials[key]["batch_no"][batch_no] -= qty return @@ -490,7 +511,7 @@ class SubcontractingController(StockController): row.item_code, row.get(self.subcontract_data.order_field), ) and transfer_item.qty > 0: - qty = self.__get_qty_based_on_material_transfer(row, transfer_item) or 0 + qty = flt(self.__get_qty_based_on_material_transfer(row, transfer_item)) transfer_item.qty -= qty self.__add_supplied_item(row, transfer_item.get("item_details"), qty) @@ -720,6 +741,25 @@ class SubcontractingController(StockController): sco_doc = frappe.get_doc("Subcontracting Order", sco) sco_doc.update_status() + def set_missing_values_in_additional_costs(self): + self.total_additional_costs = sum(flt(item.amount) for item in self.get("additional_costs")) + + if self.total_additional_costs: + if self.distribute_additional_costs_based_on == "Amount": + total_amt = sum(flt(item.amount) for item in self.get("items")) + for item in self.items: + item.additional_cost_per_qty = ( + (item.amount * self.total_additional_costs) / total_amt + ) / item.qty + else: + total_qty = sum(flt(item.qty) for item in self.get("items")) + additional_cost_per_qty = self.total_additional_costs / total_qty + for item in self.items: + item.additional_cost_per_qty = additional_cost_per_qty + else: + for item in self.items: + item.additional_cost_per_qty = 0 + @frappe.whitelist() def get_current_stock(self): if self.doctype in ["Purchase Receipt", "Subcontracting Receipt"]: @@ -730,7 +770,7 @@ class SubcontractingController(StockController): {"item_code": item.rm_item_code, "warehouse": self.supplier_warehouse}, "actual_qty", ) - item.current_stock = flt(actual_qty) or 0 + item.current_stock = flt(actual_qty) @property def sub_contracted_items(self): @@ -750,7 +790,7 @@ def get_item_details(items): item = frappe.qb.DocType("Item") item_list = ( frappe.qb.from_(item) - .select(item.item_code, item.description, item.allow_alternative_item) + .select(item.item_code, item.item_name, item.description, item.allow_alternative_item) .where(item.name.isin(items)) .run(as_dict=True) ) @@ -763,68 +803,96 @@ def get_item_details(items): @frappe.whitelist() -def make_rm_stock_entry(subcontract_order, rm_items, order_doctype="Subcontracting Order"): - rm_items_list = rm_items - - if isinstance(rm_items, str): - rm_items_list = json.loads(rm_items) - elif not rm_items: - frappe.throw(_("No Items available for transfer")) - - if rm_items_list: - fg_items = list(set(item["item_code"] for item in rm_items_list)) - else: - frappe.throw(_("No Items selected for transfer")) - +def make_rm_stock_entry( + subcontract_order, rm_items=None, order_doctype="Subcontracting Order", target_doc=None +): if subcontract_order: subcontract_order = frappe.get_doc(order_doctype, subcontract_order) - if fg_items: - items = tuple(set(item["rm_item_code"] for item in rm_items_list)) - item_wh = get_item_details(items) + if not rm_items: + if not subcontract_order.supplied_items: + frappe.throw(_("No item available for transfer.")) - stock_entry = frappe.new_doc("Stock Entry") - stock_entry.purpose = "Send to Subcontractor" - if order_doctype == "Purchase Order": - stock_entry.purchase_order = subcontract_order.name - else: - stock_entry.subcontracting_order = subcontract_order.name - stock_entry.supplier = subcontract_order.supplier - stock_entry.supplier_name = subcontract_order.supplier_name - stock_entry.supplier_address = subcontract_order.supplier_address - stock_entry.address_display = subcontract_order.address_display - stock_entry.company = subcontract_order.company - stock_entry.to_warehouse = subcontract_order.supplier_warehouse - stock_entry.set_stock_entry_type() + rm_items = subcontract_order.supplied_items - if order_doctype == "Purchase Order": - rm_detail_field = "po_detail" - else: - rm_detail_field = "sco_rm_detail" + fg_item_code_list = list( + set(item.get("main_item_code") or item.get("item_code") for item in rm_items) + ) - for item_code in fg_items: - for rm_item_data in rm_items_list: - if rm_item_data["item_code"] == item_code: - rm_item_code = rm_item_data["rm_item_code"] - items_dict = { - rm_item_code: { - rm_detail_field: rm_item_data.get("name"), - "item_name": rm_item_data["item_name"], - "description": item_wh.get(rm_item_code, {}).get("description", ""), - "qty": rm_item_data["qty"], - "from_warehouse": rm_item_data["warehouse"], - "stock_uom": rm_item_data["stock_uom"], - "serial_no": rm_item_data.get("serial_no"), - "batch_no": rm_item_data.get("batch_no"), - "main_item_code": rm_item_data["item_code"], - "allow_alternative_item": item_wh.get(rm_item_code, {}).get("allow_alternative_item"), + if fg_item_code_list: + rm_item_code_list = tuple(set(item.get("rm_item_code") for item in rm_items)) + item_wh = get_item_details(rm_item_code_list) + + field_no_map, rm_detail_field = "purchase_order", "sco_rm_detail" + if order_doctype == "Purchase Order": + field_no_map, rm_detail_field = "subcontracting_order", "po_detail" + + if target_doc and target_doc.get("items"): + target_doc.items = [] + + stock_entry = get_mapped_doc( + order_doctype, + subcontract_order.name, + { + order_doctype: { + "doctype": "Stock Entry", + "field_map": { + "supplier": "supplier", + "supplier_name": "supplier_name", + "supplier_address": "supplier_address", + "to_warehouse": "supplier_warehouse", + }, + "field_no_map": [field_no_map], + "validation": { + "docstatus": ["=", 1], + }, + }, + }, + target_doc, + ignore_child_tables=True, + ) + + stock_entry.purpose = "Send to Subcontractor" + + if order_doctype == "Purchase Order": + stock_entry.purchase_order = subcontract_order.name + else: + stock_entry.subcontracting_order = subcontract_order.name + + stock_entry.set_stock_entry_type() + + for fg_item_code in fg_item_code_list: + for rm_item in rm_items: + + if rm_item.get("main_item_code") == fg_item_code or rm_item.get("item_code") == fg_item_code: + rm_item_code = rm_item.get("rm_item_code") + + items_dict = { + rm_item_code: { + rm_detail_field: rm_item.get("name"), + "item_name": rm_item.get("item_name") + or item_wh.get(rm_item_code, {}).get("item_name", ""), + "description": item_wh.get(rm_item_code, {}).get("description", ""), + "qty": rm_item.get("qty") + or max(rm_item.get("required_qty") - rm_item.get("total_supplied_qty"), 0), + "from_warehouse": rm_item.get("warehouse") or rm_item.get("reserve_warehouse"), + "to_warehouse": subcontract_order.supplier_warehouse, + "stock_uom": rm_item.get("stock_uom"), + "serial_no": rm_item.get("serial_no"), + "batch_no": rm_item.get("batch_no"), + "main_item_code": fg_item_code, + "allow_alternative_item": item_wh.get(rm_item_code, {}).get("allow_alternative_item"), + } } - } - stock_entry.add_to_stock_entry_detail(items_dict) - return stock_entry.as_dict() - else: - frappe.throw(_("No Items selected for transfer")) - return subcontract_order.name + + stock_entry.add_to_stock_entry_detail(items_dict) + + if target_doc: + return stock_entry + else: + return stock_entry.as_dict() + else: + frappe.throw(_("No Items selected for transfer.")) def add_items_in_ste( @@ -851,7 +919,18 @@ def add_items_in_ste( def make_return_stock_entry_for_subcontract( available_materials, order_doc, rm_details, order_doctype="Subcontracting Order" ): - ste_doc = frappe.new_doc("Stock Entry") + ste_doc = get_mapped_doc( + order_doctype, + order_doc.name, + { + order_doctype: { + "doctype": "Stock Entry", + "field_no_map": ["purchase_order", "subcontracting_order"], + }, + }, + ignore_child_tables=True, + ) + ste_doc.purpose = "Material Transfer" if order_doctype == "Purchase Order": diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 0d8cffe03f..8c403aa9bf 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -6,6 +6,7 @@ import json import frappe from frappe import _, scrub +from frappe.model.document import Document from frappe.utils import cint, flt, round_based_on_smallest_currency_fraction import erpnext @@ -20,7 +21,7 @@ from erpnext.stock.get_item_details import _get_item_tax_template class calculate_taxes_and_totals(object): - def __init__(self, doc): + def __init__(self, doc: Document): self.doc = doc frappe.flags.round_off_applicable_accounts = [] get_round_off_applicable_accounts(self.doc.company, frappe.flags.round_off_applicable_accounts) @@ -37,6 +38,12 @@ class calculate_taxes_and_totals(object): self.set_discount_amount() self.apply_discount_amount() + # Update grand total as per cash and non trade discount + if self.doc.apply_discount_on == "Grand Total" and self.doc.get("is_cash_or_non_trade_discount"): + self.doc.grand_total -= self.doc.discount_amount + self.doc.base_grand_total -= self.doc.base_discount_amount + self.set_rounded_total() + self.calculate_shipping_charges() if self.doc.doctype in ["Sales Invoice", "Purchase Invoice"]: @@ -52,12 +59,25 @@ class calculate_taxes_and_totals(object): self.initialize_taxes() self.determine_exclusive_rate() self.calculate_net_total() + self.calculate_tax_withholding_net_total() self.calculate_taxes() self.manipulate_grand_total_for_inclusive_tax() self.calculate_totals() self._cleanup() self.calculate_total_net_weight() + def calculate_tax_withholding_net_total(self): + if hasattr(self.doc, "tax_withholding_net_total"): + sum_net_amount = 0 + sum_base_net_amount = 0 + for item in self.doc.get("items"): + if hasattr(item, "apply_tds") and item.apply_tds: + sum_net_amount += item.net_amount + sum_base_net_amount += item.base_net_amount + + self.doc.tax_withholding_net_total = sum_net_amount + self.doc.base_tax_withholding_net_total = sum_base_net_amount + def validate_item_tax_template(self): for item in self.doc.get("items"): if item.item_code and item.get("item_tax_template"): @@ -500,9 +520,6 @@ class calculate_taxes_and_totals(object): else: self.doc.grand_total = flt(self.doc.net_total) - if self.doc.apply_discount_on == "Grand Total" and self.doc.get("is_cash_or_non_trade_discount"): - self.doc.grand_total -= self.doc.discount_amount - if self.doc.get("taxes"): self.doc.total_taxes_and_charges = flt( self.doc.grand_total - self.doc.net_total - flt(self.doc.rounding_adjustment), @@ -597,16 +614,16 @@ class calculate_taxes_and_totals(object): if not self.doc.apply_discount_on: frappe.throw(_("Please select Apply Discount On")) + self.doc.base_discount_amount = flt( + self.doc.discount_amount * self.doc.conversion_rate, self.doc.precision("base_discount_amount") + ) + if self.doc.apply_discount_on == "Grand Total" and self.doc.get( "is_cash_or_non_trade_discount" ): self.discount_amount_applied = True return - self.doc.base_discount_amount = flt( - self.doc.discount_amount * self.doc.conversion_rate, self.doc.precision("base_discount_amount") - ) - total_for_discount_amount = self.get_total_for_discount_amount() taxes = self.doc.get("taxes") net_total = 0 @@ -661,7 +678,7 @@ class calculate_taxes_and_totals(object): ) def calculate_total_advance(self): - if self.doc.docstatus < 2: + if not self.doc.docstatus.is_cancelled(): total_allocated_amount = sum( flt(adv.allocated_amount, adv.precision("allocated_amount")) for adv in self.doc.get("advances") @@ -692,7 +709,7 @@ class calculate_taxes_and_totals(object): ) ) - if self.doc.docstatus == 0: + if self.doc.docstatus.is_draft(): if self.doc.get("write_off_outstanding_amount_automatically"): self.doc.write_off_amount = 0 @@ -767,6 +784,18 @@ class calculate_taxes_and_totals(object): self.doc.precision("outstanding_amount"), ) + if ( + self.doc.doctype == "Sales Invoice" + and self.doc.get("is_pos") + and self.doc.get("pos_profile") + and self.doc.get("is_consolidated") + ): + write_off_limit = flt( + frappe.db.get_value("POS Profile", self.doc.pos_profile, "write_off_limit") + ) + if write_off_limit and abs(self.doc.outstanding_amount) <= write_off_limit: + self.doc.write_off_outstanding_amount_automatically = 1 + if ( self.doc.doctype == "Sales Invoice" and self.doc.get("is_pos") @@ -874,24 +903,33 @@ class calculate_taxes_and_totals(object): self.doc.other_charges_calculation = get_itemised_tax_breakup_html(self.doc) def set_total_amount_to_default_mop(self, total_amount_to_pay): - default_mode_of_payment = frappe.db.get_value( - "POS Payment Method", - {"parent": self.doc.pos_profile, "default": 1}, - ["mode_of_payment"], - as_dict=1, - ) - - if default_mode_of_payment: - self.doc.payments = [] - self.doc.append( - "payments", - { - "mode_of_payment": default_mode_of_payment.mode_of_payment, - "amount": total_amount_to_pay, - "default": 1, - }, + total_paid_amount = 0 + for payment in self.doc.get("payments"): + total_paid_amount += ( + payment.amount if self.doc.party_account_currency == self.doc.currency else payment.base_amount ) + pending_amount = total_amount_to_pay - total_paid_amount + + if pending_amount > 0: + default_mode_of_payment = frappe.db.get_value( + "POS Payment Method", + {"parent": self.doc.pos_profile, "default": 1}, + ["mode_of_payment"], + as_dict=1, + ) + + if default_mode_of_payment: + self.doc.payments = [] + self.doc.append( + "payments", + { + "mode_of_payment": default_mode_of_payment.mode_of_payment, + "amount": pending_amount, + "default": 1, + }, + ) + def get_itemised_tax_breakup_html(doc): if not doc.taxes: @@ -1019,7 +1057,7 @@ class init_landed_taxes_and_totals(object): company_currency = erpnext.get_company_currency(self.doc.company) for d in self.doc.get(self.tax_field): if not d.account_currency: - account_currency = frappe.db.get_value("Account", d.expense_account, "account_currency") + account_currency = frappe.get_cached_value("Account", d.expense_account, "account_currency") d.account_currency = account_currency or company_currency def set_exchange_rate(self): diff --git a/erpnext/controllers/tests/test_subcontracting_controller.py b/erpnext/controllers/tests/test_subcontracting_controller.py index 4fab8058b8..0e6fe95d45 100644 --- a/erpnext/controllers/tests/test_subcontracting_controller.py +++ b/erpnext/controllers/tests/test_subcontracting_controller.py @@ -36,6 +36,36 @@ class TestSubcontractingController(FrappeTestCase): sco.remove_empty_rows() self.assertEqual((len_before - 1), len(sco.service_items)) + def test_set_missing_values_in_additional_costs(self): + sco = get_subcontracting_order(do_not_submit=1) + + rate_without_additional_cost = sco.items[0].rate + amount_without_additional_cost = sco.items[0].amount + + additional_amount = 120 + sco.append( + "additional_costs", + { + "expense_account": "Cost of Goods Sold - _TC", + "description": "Test", + "amount": additional_amount, + }, + ) + sco.save() + + additional_cost_per_qty = additional_amount / sco.items[0].qty + + self.assertEqual(sco.items[0].additional_cost_per_qty, additional_cost_per_qty) + self.assertEqual(rate_without_additional_cost + additional_cost_per_qty, sco.items[0].rate) + self.assertEqual(amount_without_additional_cost + additional_amount, sco.items[0].amount) + + sco.additional_costs = [] + sco.save() + + self.assertEqual(sco.items[0].additional_cost_per_qty, 0) + self.assertEqual(rate_without_additional_cost, sco.items[0].rate) + self.assertEqual(amount_without_additional_cost, sco.items[0].amount) + def test_create_raw_materials_supplied(self): sco = get_subcontracting_order() sco.supplied_items = None @@ -785,6 +815,7 @@ def add_second_row_in_scr(scr): "item_name", "qty", "uom", + "bom", "warehouse", "stock_uom", "subcontracting_order", @@ -867,7 +898,7 @@ def make_stock_transfer_entry(**args): "item_name": row.item_code, "rate": row.rate or 100, "stock_uom": row.stock_uom or "Nos", - "warehouse": row.warehuose or "_Test Warehouse - _TC", + "warehouse": row.warehouse or "_Test Warehouse - _TC", } item_details = args.itemwise_details.get(row.item_code) @@ -1001,9 +1032,9 @@ def get_subcontracting_order(**args): if not args.service_items: service_items = [ { - "warehouse": "_Test Warehouse - _TC", + "warehouse": args.warehouse or "_Test Warehouse - _TC", "item_code": "Subcontracted Service Item 7", - "qty": 5, + "qty": 10, "rate": 100, "fg_item": "Subcontracted Item SA7", "fg_item_qty": 10, @@ -1016,6 +1047,7 @@ def get_subcontracting_order(**args): rm_items=service_items, is_subcontracted=1, supplier_warehouse=args.supplier_warehouse or "_Test Warehouse 1 - _TC", + company=args.company, ) return create_subcontracting_order(po_name=po.name, **args) diff --git a/erpnext/controllers/trends.py b/erpnext/controllers/trends.py index 1d6c5dc0be..1fb722e112 100644 --- a/erpnext/controllers/trends.py +++ b/erpnext/controllers/trends.py @@ -80,7 +80,7 @@ def get_data(filters, conditions): if conditions.get("trans") == "Quotation" and filters.get("group_by") == "Customer": cond += " and t1.quotation_to = 'Customer'" - year_start_date, year_end_date = frappe.db.get_value( + year_start_date, year_end_date = frappe.get_cached_value( "Fiscal Year", filters.get("fiscal_year"), ["year_start_date", "year_end_date"] ) @@ -275,7 +275,7 @@ def get_period_date_ranges(period, fiscal_year=None, year_start_date=None): from dateutil.relativedelta import relativedelta if not year_start_date: - year_start_date, year_end_date = frappe.db.get_value( + year_start_date, year_end_date = frappe.get_cached_value( "Fiscal Year", fiscal_year, ["year_start_date", "year_end_date"] ) diff --git a/erpnext/crm/doctype/appointment/appointment.json b/erpnext/crm/doctype/appointment/appointment.json index fe7b4e17f0..c26b064c4c 100644 --- a/erpnext/crm/doctype/appointment/appointment.json +++ b/erpnext/crm/doctype/appointment/appointment.json @@ -102,7 +102,7 @@ } ], "links": [], - "modified": "2021-06-30 13:09:14.228756", + "modified": "2022-12-15 11:11:02.131986", "modified_by": "Administrator", "module": "CRM", "name": "Appointment", @@ -121,16 +121,6 @@ "share": 1, "write": 1 }, - { - "create": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Guest", - "share": 1 - }, { "create": 1, "delete": 1, @@ -170,5 +160,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py index 5f5923dc89..bd49bdc925 100644 --- a/erpnext/crm/doctype/appointment/appointment.py +++ b/erpnext/crm/doctype/appointment/appointment.py @@ -6,8 +6,10 @@ from collections import Counter import frappe from frappe import _ +from frappe.desk.form.assign_to import add as add_assignment from frappe.model.document import Document -from frappe.utils import get_url, getdate +from frappe.share import add_docshare +from frappe.utils import get_url, getdate, now from frappe.utils.verified_command import get_signed_params @@ -104,35 +106,44 @@ class Appointment(Document): # Return if already linked if self.party: return + lead = frappe.get_doc( { "doctype": "Lead", "lead_name": self.customer_name, "email_id": self.customer_email, - "notes": self.customer_details, "phone": self.customer_phone_number, } ) + + if self.customer_details: + lead.append( + "notes", + { + "note": self.customer_details, + "added_by": frappe.session.user, + "added_on": now(), + }, + ) + lead.insert(ignore_permissions=True) + # Link lead self.party = lead.name def auto_assign(self): - from frappe.desk.form.assign_to import add as add_assignemnt - existing_assignee = self.get_assignee_from_latest_opportunity() if existing_assignee: # If the latest opportunity is assigned to someone # Assign the appointment to the same - add_assignemnt({"doctype": self.doctype, "name": self.name, "assign_to": [existing_assignee]}) + self.assign_agent(existing_assignee) return if self._assign: return available_agents = _get_agents_sorted_by_asc_workload(getdate(self.scheduled_time)) for agent in available_agents: if _check_agent_availability(agent, self.scheduled_time): - agent = agent[0] - add_assignemnt({"doctype": self.doctype, "name": self.name, "assign_to": [agent]}) + self.assign_agent(agent[0]) break def get_assignee_from_latest_opportunity(self): @@ -187,9 +198,15 @@ class Appointment(Document): params = {"email": self.customer_email, "appointment": self.name} return get_url(verify_route + "?" + get_signed_params(params)) + def assign_agent(self, agent): + if not frappe.has_permission(doc=self, user=agent): + add_docshare(self.doctype, self.name, agent, flags={"ignore_share_permission": True}) + + add_assignment({"doctype": self.doctype, "name": self.name, "assign_to": [agent]}) + def _get_agents_sorted_by_asc_workload(date): - appointments = frappe.db.get_list("Appointment", fields="*") + appointments = frappe.get_all("Appointment", fields="*") agent_list = _get_agent_list_as_strings() if not appointments: return agent_list @@ -214,7 +231,7 @@ def _get_agent_list_as_strings(): def _check_agent_availability(agent_email, scheduled_time): - appointemnts_at_scheduled_time = frappe.get_list( + appointemnts_at_scheduled_time = frappe.get_all( "Appointment", filters={"scheduled_time": scheduled_time} ) for appointment in appointemnts_at_scheduled_time: diff --git a/erpnext/crm/doctype/appointment/test_appointment.py b/erpnext/crm/doctype/appointment/test_appointment.py index 776e604333..178b9d2de5 100644 --- a/erpnext/crm/doctype/appointment/test_appointment.py +++ b/erpnext/crm/doctype/appointment/test_appointment.py @@ -6,29 +6,20 @@ import unittest import frappe - -def create_test_lead(): - test_lead = frappe.db.get_value("Lead", {"email_id": "test@example.com"}) - if test_lead: - return frappe.get_doc("Lead", test_lead) - test_lead = frappe.get_doc( - {"doctype": "Lead", "lead_name": "Test Lead", "email_id": "test@example.com"} - ) - test_lead.insert(ignore_permissions=True) - return test_lead +LEAD_EMAIL = "test_appointment_lead@example.com" -def create_test_appointments(): +def create_test_appointment(): test_appointment = frappe.get_doc( { "doctype": "Appointment", - "email": "test@example.com", "status": "Open", "customer_name": "Test Lead", "customer_phone_number": "666", "customer_skype": "test", - "customer_email": "test@example.com", + "customer_email": LEAD_EMAIL, "scheduled_time": datetime.datetime.now(), + "customer_details": "Hello, Friend!", } ) test_appointment.insert() @@ -36,16 +27,16 @@ def create_test_appointments(): class TestAppointment(unittest.TestCase): - test_appointment = test_lead = None + def setUpClass(): + frappe.db.delete("Lead", {"email_id": LEAD_EMAIL}) def setUp(self): - self.test_lead = create_test_lead() - self.test_appointment = create_test_appointments() + self.test_appointment = create_test_appointment() + self.test_appointment.set_verified(self.test_appointment.customer_email) def test_calendar_event_created(self): cal_event = frappe.get_doc("Event", self.test_appointment.calendar_event) self.assertEqual(cal_event.starts_on, self.test_appointment.scheduled_time) def test_lead_linked(self): - lead = frappe.get_doc("Lead", self.test_lead.name) - self.assertIsNotNone(lead) + self.assertTrue(self.test_appointment.party) diff --git a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json index 4b26e4901b..436eb10c88 100644 --- a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json +++ b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json @@ -1,4 +1,5 @@ { + "actions": [], "creation": "2019-08-27 10:56:48.309824", "doctype": "DocType", "editable_grid": 1, @@ -101,7 +102,8 @@ } ], "issingle": 1, - "modified": "2019-11-26 12:14:17.669366", + "links": [], + "modified": "2022-12-15 11:10:13.517742", "modified_by": "Administrator", "module": "CRM", "name": "Appointment Booking Settings", @@ -117,13 +119,6 @@ "share": 1, "write": 1 }, - { - "email": 1, - "print": 1, - "read": 1, - "role": "Guest", - "share": 1 - }, { "create": 1, "email": 1, @@ -147,5 +142,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/crm/doctype/lead/lead.json b/erpnext/crm/doctype/lead/lead.json index c946ae4999..077e7fa4af 100644 --- a/erpnext/crm/doctype/lead/lead.json +++ b/erpnext/crm/doctype/lead/lead.json @@ -312,7 +312,8 @@ "fieldtype": "Data", "hidden": 1, "label": "Title", - "print_hide": 1 + "print_hide": 1, + "read_only": 1 }, { "fieldname": "language", @@ -340,8 +341,8 @@ "fieldname": "no_of_employees", "fieldtype": "Select", "label": "No of Employees", - "options": "1-10\n11-20\n21-30\n31-100\n11-50\n51-200\n201-500\n101-500\n500-1000\n501-1000\n>1000\n1000+" - }, + "options": "1-10\n11-50\n51-200\n201-500\n501-1000\n1000+" + }, { "fieldname": "column_break_22", "fieldtype": "Column Break" @@ -375,7 +376,7 @@ "depends_on": "eval:!doc.__islocal", "fieldname": "notes_tab", "fieldtype": "Tab Break", - "label": "Notes" + "label": "Comments" }, { "collapsible": 1, @@ -506,7 +507,7 @@ { "fieldname": "dashboard_tab", "fieldtype": "Tab Break", - "label": "Dashboard", + "label": "Connections", "show_dashboard": 1 } ], @@ -514,11 +515,10 @@ "idx": 5, "image_field": "image", "links": [], - "modified": "2022-07-22 15:55:03.176094", + "modified": "2023-01-24 18:20:05.044791", "modified_by": "Administrator", "module": "CRM", "name": "Lead", - "name_case": "Title Case", "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py index 0d12499771..2a588d8d13 100644 --- a/erpnext/crm/doctype/lead/lead.py +++ b/erpnext/crm/doctype/lead/lead.py @@ -14,9 +14,6 @@ from erpnext.crm.utils import CRMNote, copy_comments, link_communications, link_ class Lead(SellingController, CRMNote): - def get_feed(self): - return "{0}: {1}".format(_(self.status), self.lead_name) - def onload(self): customer = frappe.db.get_value("Customer", {"lead_name": self.name}) self.get("__onload").is_customer = customer @@ -285,6 +282,7 @@ def _make_customer(source_name, target_doc=None, ignore_permissions=False): "contact_no": "phone_1", "fax": "fax_1", }, + "field_no_map": ["disabled"], } }, target_doc, @@ -393,7 +391,7 @@ def get_lead_details(lead, posting_date=None, company=None): { "territory": lead.territory, "customer_name": lead.company_name or lead.lead_name, - "contact_display": " ".join(filter(None, [lead.salutation, lead.lead_name])), + "contact_display": " ".join(filter(None, [lead.lead_name])), "contact_email": lead.email_id, "contact_mobile": lead.mobile_no, "contact_phone": lead.phone, @@ -453,6 +451,7 @@ def get_lead_with_phone_number(number): "Lead", or_filters={ "phone": ["like", "%{}".format(number)], + "whatsapp_no": ["like", "%{}".format(number)], "mobile_no": ["like", "%{}".format(number)], }, limit=1, diff --git a/erpnext/crm/doctype/lead_source/lead_source.json b/erpnext/crm/doctype/lead_source/lead_source.json index 723c6d993d..c3cedcc7a6 100644 --- a/erpnext/crm/doctype/lead_source/lead_source.json +++ b/erpnext/crm/doctype/lead_source/lead_source.json @@ -26,10 +26,11 @@ } ], "links": [], - "modified": "2021-02-08 12:51:48.971517", + "modified": "2023-02-10 00:51:44.973957", "modified_by": "Administrator", "module": "CRM", "name": "Lead Source", + "naming_rule": "By fieldname", "owner": "Administrator", "permissions": [ { @@ -58,5 +59,7 @@ ], "quick_entry": 1, "sort_field": "modified", - "sort_order": "DESC" + "sort_order": "DESC", + "states": [], + "translated_doctype": 1 } \ No newline at end of file diff --git a/erpnext/crm/doctype/opportunity/opportunity.json b/erpnext/crm/doctype/opportunity/opportunity.json index 68a2156981..07641d20c3 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.json +++ b/erpnext/crm/doctype/opportunity/opportunity.json @@ -463,7 +463,7 @@ "fieldname": "no_of_employees", "fieldtype": "Select", "label": "No of Employees", - "options": "1-10\n11-20\n21-30\n31-100\n11-50\n51-200\n201-500\n101-500\n500-1000\n501-1000\n>1000\n1000+" + "options": "1-10\n11-50\n51-200\n201-500\n501-1000\n1000+" }, { "fieldname": "annual_revenue", @@ -544,14 +544,14 @@ "depends_on": "eval:!doc.__islocal", "fieldname": "dashboard_tab", "fieldtype": "Tab Break", - "label": "Dashboard", + "label": "Connections", "show_dashboard": 1 }, { "depends_on": "eval:!doc.__islocal", "fieldname": "notes_tab", "fieldtype": "Tab Break", - "label": "Notes" + "label": "Comments" }, { "fieldname": "notes_html", @@ -622,7 +622,7 @@ "icon": "fa fa-info-sign", "idx": 195, "links": [], - "modified": "2022-07-22 18:46:32.858696", + "modified": "2022-10-13 12:42:21.545636", "modified_by": "Administrator", "module": "CRM", "name": "Opportunity", diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py index 08eb472bb9..f4b6e910ed 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.py +++ b/erpnext/crm/doctype/opportunity/opportunity.py @@ -60,7 +60,7 @@ class Opportunity(TransactionBase, CRMNote): if not self.get(field) and frappe.db.field_exists(self.opportunity_from, field): try: value = frappe.db.get_value(self.opportunity_from, self.party_name, field) - frappe.db.set(self, field, value) + self.db_set(field, value) except Exception: continue diff --git a/erpnext/crm/doctype/prospect/prospect.json b/erpnext/crm/doctype/prospect/prospect.json index 7f33c08c13..d32311bc4e 100644 --- a/erpnext/crm/doctype/prospect/prospect.json +++ b/erpnext/crm/doctype/prospect/prospect.json @@ -82,7 +82,7 @@ "fieldname": "no_of_employees", "fieldtype": "Select", "label": "No. of Employees", - "options": "1-10\n11-20\n21-30\n31-100\n11-50\n51-200\n201-500\n101-500\n500-1000\n501-1000\n>1000\n1000+" + "options": "1-10\n11-50\n51-200\n201-500\n501-1000\n1000+" }, { "fieldname": "annual_revenue", @@ -128,7 +128,7 @@ "depends_on": "eval:!doc.__islocal", "fieldname": "notes_section", "fieldtype": "Tab Break", - "label": "Notes" + "label": "Comments" }, { "depends_on": "eval: !doc.__islocal", @@ -218,7 +218,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2022-06-22 15:10:26.887502", + "modified": "2022-10-13 12:29:33.674561", "modified_by": "Administrator", "module": "CRM", "name": "Prospect", diff --git a/erpnext/crm/doctype/sales_stage/sales_stage.json b/erpnext/crm/doctype/sales_stage/sales_stage.json index 77aa559b77..caf8ff5b36 100644 --- a/erpnext/crm/doctype/sales_stage/sales_stage.json +++ b/erpnext/crm/doctype/sales_stage/sales_stage.json @@ -18,10 +18,11 @@ } ], "links": [], - "modified": "2020-05-20 12:22:01.866472", + "modified": "2023-02-10 01:40:23.713390", "modified_by": "Administrator", "module": "CRM", "name": "Sales Stage", + "naming_rule": "By fieldname", "owner": "Administrator", "permissions": [ { @@ -40,5 +41,7 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", - "track_changes": 1 + "states": [], + "track_changes": 1, + "translated_doctype": 1 } \ No newline at end of file diff --git a/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.js b/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.js index 116db2f5a2..7cd1710a7f 100644 --- a/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.js +++ b/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.js @@ -44,7 +44,7 @@ frappe.query_reports["Opportunity Summary by Sales Stage"] = { }, { fieldname: "opportunity_source", - label: __("Oppoturnity Source"), + label: __("Opportunity Source"), fieldtype: "Link", options: "Lead Source", }, @@ -62,4 +62,4 @@ frappe.query_reports["Opportunity Summary by Sales Stage"] = { default: frappe.defaults.get_user_default("Company") } ] -}; \ No newline at end of file +}; diff --git a/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py b/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py index d23a22ac46..dea3f2dd36 100644 --- a/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py +++ b/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py @@ -217,7 +217,7 @@ class SalesPipelineAnalytics(object): def check_for_assigned_to(self, period, value, count_or_amount, assigned_to, info): if self.filters.get("assigned_to"): - for data in json.loads(info.get("opportunity_owner")): + for data in json.loads(info.get("opportunity_owner") or "[]"): if data == self.filters.get("assigned_to"): self.set_formatted_data(period, data, count_or_amount, assigned_to) else: diff --git a/erpnext/crm/utils.py b/erpnext/crm/utils.py index 433d974a44..737452021c 100644 --- a/erpnext/crm/utils.py +++ b/erpnext/crm/utils.py @@ -120,7 +120,7 @@ def link_open_tasks(ref_doctype, ref_docname, doc): todo_doc = frappe.get_doc("ToDo", todo.name) todo_doc.reference_type = doc.doctype todo_doc.reference_name = doc.name - todo_doc.db_update() + todo_doc.save() def link_open_events(ref_doctype, ref_docname, doc): diff --git a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.js b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.js index 69b9cfaa68..c37fa2f6ea 100644 --- a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.js +++ b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.js @@ -48,5 +48,11 @@ frappe.ui.form.on("E Commerce Settings", { frm.set_value('default_customer_group', ''); frm.set_value('quotation_series', ''); } + }, + + enable_checkout: function(frm) { + if (frm.doc.enable_checkout) { + erpnext.utils.check_payments_app(); + } } }); diff --git a/erpnext/e_commerce/doctype/website_item/test_website_item.py b/erpnext/e_commerce/doctype/website_item/test_website_item.py index 828c655e79..bbe04d5514 100644 --- a/erpnext/e_commerce/doctype/website_item/test_website_item.py +++ b/erpnext/e_commerce/doctype/website_item/test_website_item.py @@ -174,7 +174,10 @@ class TestWebsiteItem(unittest.TestCase): # Website Item Portal Tests Begin def test_website_item_breadcrumbs(self): - "Check if breadcrumbs include homepage, product listing navigation page, parent item group(s) and item group." + """ + Check if breadcrumbs include homepage, product listing navigation page, + parent item group(s) and item group + """ from erpnext.setup.doctype.item_group.item_group import get_parent_item_groups item_code = "Test Breadcrumb Item" @@ -197,7 +200,7 @@ class TestWebsiteItem(unittest.TestCase): breadcrumbs = get_parent_item_groups(item.item_group) self.assertEqual(breadcrumbs[0]["name"], "Home") - self.assertEqual(breadcrumbs[1]["name"], "Shop by Category") + self.assertEqual(breadcrumbs[1]["name"], "All Products") self.assertEqual(breadcrumbs[2]["name"], "_Test Item Group B") # parent item group self.assertEqual(breadcrumbs[3]["name"], "_Test Item Group B - 1") diff --git a/erpnext/e_commerce/doctype/website_item/website_item.json b/erpnext/e_commerce/doctype/website_item/website_item.json index a416aac3a1..6556eabf4a 100644 --- a/erpnext/e_commerce/doctype/website_item/website_item.json +++ b/erpnext/e_commerce/doctype/website_item/website_item.json @@ -188,7 +188,8 @@ "in_list_view": 1, "label": "Item Group", "options": "Item Group", - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "default": "1", @@ -234,7 +235,8 @@ "fieldname": "brand", "fieldtype": "Link", "label": "Brand", - "options": "Brand" + "options": "Brand", + "search_index": 1 }, { "collapsible": 1, @@ -345,7 +347,8 @@ "image_field": "website_image", "index_web_pages_for_search": 1, "links": [], - "modified": "2022-06-28 17:10:30.613251", + "make_attachments_public": 1, + "modified": "2022-09-30 04:01:52.090732", "modified_by": "Administrator", "module": "E-commerce", "name": "Website Item", diff --git a/erpnext/e_commerce/doctype/website_item/website_item.py b/erpnext/e_commerce/doctype/website_item/website_item.py index c0f8c79283..3e5d5f768f 100644 --- a/erpnext/e_commerce/doctype/website_item/website_item.py +++ b/erpnext/e_commerce/doctype/website_item/website_item.py @@ -403,9 +403,6 @@ def on_doctype_update(): # since route is a Text column, it needs a length for indexing frappe.db.add_index("Website Item", ["route(500)"]) - frappe.db.add_index("Website Item", ["item_group"]) - frappe.db.add_index("Website Item", ["brand"]) - def check_if_user_is_customer(user=None): from frappe.contacts.doctype.contact.contact import get_contact_name diff --git a/erpnext/e_commerce/product_ui/search.js b/erpnext/e_commerce/product_ui/search.js index 61922459e5..1688cc1fb6 100644 --- a/erpnext/e_commerce/product_ui/search.js +++ b/erpnext/e_commerce/product_ui/search.js @@ -200,7 +200,7 @@ erpnext.ProductSearch = class { let thumbnail = res.thumbnail || '/assets/erpnext/images/ui-states/cart-empty-state.png'; html += `
{{ _("Date") }}
{%= __("Date") %}{%= __("Ref") %}{%= __("Party") %}{%= __("Reference") %}{%= __("Remarks") %} {%= __("Debit") %} {%= __("Credit") %} {%= __("Balance (Dr - Cr)") %} - {%= format_currency(data[i].debit, filters.presentation_currency) %} - {%= format_currency(data[i].credit, filters.presentation_currency) %} {%= frappe.format(data[i].account, {fieldtype: "Link"}) || " " %} - {%= data[i].account && format_currency(data[i].debit, filters.presentation_currency) %} + {%= data[i].account && format_currency(data[i].debit, filters.presentation_currency || data[i].account_currency) %} - {%= data[i].account && format_currency(data[i].credit, filters.presentation_currency) %} + {%= data[i].account && format_currency(data[i].credit, filters.presentation_currency || data[i].account_currency) %} - {%= format_currency(data[i].balance, filters.presentation_currency) %} + {%= format_currency(data[i].balance, filters.presentation_currency || data[i].account_currency) %}
\n\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\n\t\n\t\t{%- for item in doc.items -%}\n\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t{%- endfor -%}\n\t\n
{{ _(\"Item\") }}{{ _(\"Qty\") }}{{ _(\"Amount\") }}
\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
{{ item.item_name }}\n\t\t\t\t{%- endif -%}\n\t\t\t\t{%- if item.serial_no -%}\n\t\t\t\t\t
{{ _(\"SR.No\") }}:
\n\t\t\t\t\t{{ item.serial_no | replace(\"\\n\", \", \") }}\n\t\t\t\t{%- endif -%}\n\t\t\t
{{ item.qty }}{{ item.get_formatted(\"net_amount\") }}
\n\n\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t{%- for row in doc.taxes -%}\n\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t{%- endfor -%}\n\n\t\t{%- if doc.discount_amount -%}\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t{%- endif -%}\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t{%- if doc.rounded_total -%}\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t{%- endif -%}\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t{%- if doc.change_amount -%}\n\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t{%- endif -%}\n\t\n
\n\t\t\t\t{{ _(\"Total Excl. Tax\") }}\n\t\t\t\n\t\t\t\t{{ doc.get_formatted(\"net_total\", doc) }}\n\t\t\t
\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\n\t\t\t\t\t{{ row.get_formatted(\"tax_amount\", doc) }}\n\t\t\t\t
\n\t\t\t\t{{ _(\"Discount\") }}\n\t\t\t\n\t\t\t\t{{ doc.get_formatted(\"discount_amount\") }}\n\t\t\t
\n\t\t\t\t{{ _(\"Grand Total\") }}\n\t\t\t\n\t\t\t\t{{ doc.get_formatted(\"grand_total\") }}\n\t\t\t
\n\t\t\t\t{{ _(\"Rounded Total\") }}\n\t\t\t\n\t\t\t\t{{ doc.get_formatted(\"rounded_total\") }}\n\t\t\t
\n\t\t\t\t{{ _(\"Paid Amount\") }}\n\t\t\t\n\t\t\t\t{{ doc.get_formatted(\"paid_amount\") }}\n\t\t\t
\n\t\t\t\t\t{{ _(\"Change Amount\") }}\n\t\t\t\t\n\t\t\t\t\t{{ doc.get_formatted(\"change_amount\") }}\n\t\t\t\t
\n
\n

{{ doc.terms or \"\" }}

\n

{{ _(\"Thank you, please visit again.\") }}

", - "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 deleted file mode 100644 index 6b64d47453..0000000000 --- a/erpnext/regional/print_format/ksa_vat_invoice/ksa_vat_invoice.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "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 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": 1, - "doc_type": "Sales Invoice", - "docstatus": 0, - "doctype": "Print Format", - "font_size": 14, - "html": "
\n
\n
\n

TAX INVOICE

\n

\u0641\u0627\u062a\u0648\u0631\u0629 \u0636\u0631\u064a\u0628\u064a\u0629

\n
\n \n \n
\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 \n \n \n \n \n \n \n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\t\t{% if (company.tax_id) %}\n \n \n \n \n \n \n \n \n {% endif %}\n \n \n \n \n \n \n {% if(supplier_address_doc) %}\n \n \n \n \n \n \n \n \n \n \n \n \n {% endif %}\n \n \n \n \n \n \n\t\t{% set customer_tax_id = frappe.db.get_value('Customer', doc.customer, 'tax_id') %}\n\t\t{% if customer_tax_id %}\n \n \n \n \n \n \n \n \n {% endif %}\n \n \n \n \n \n {% if(customer_address) %}\n \n \n \n \n {% endif %}\n \n {% if(customer_shipping_address) %}\n \n \n \n \n \n \n \n \n \n {% endif %}\n \n\t\t{% if(doc.po_no) %}\n \n \n \n \n \n \n \n \n \n {% endif %}\n \n \n \n \n \n \n
{{ company.name }}{{ company.company_name_in_arabic }}
Invoice#: {{doc.name}}\u0631\u0642\u0645 \u0627\u0644\u0641\u0627\u062a\u0648\u0631\u0629: {{doc.name}}
Invoice Date: {{doc.posting_date}}\u062a\u0627\u0631\u064a\u062e \u0627\u0644\u0641\u0627\u062a\u0648\u0631\u0629: {{doc.posting_date}}
Date of Supply:{{doc.posting_date}}\u062a\u0627\u0631\u064a\u062e \u0627\u0644\u062a\u0648\u0631\u064a\u062f: {{doc.posting_date}}
Supplier:\u0627\u0644\u0645\u0648\u0631\u062f:
Supplier Tax Identification Number:\u0631\u0642\u0645 \u0627\u0644\u062a\u0639\u0631\u064a\u0641 \u0627\u0644\u0636\u0631\u064a\u0628\u064a \u0644\u0644\u0645\u0648\u0631\u062f:
{{ company.tax_id }}{{ company.tax_id }}
{{ company.name }}{{ company.company_name_in_arabic }}
{{ supplier_address_doc.address_line1}} {{ supplier_address_doc.address_in_arabic}}
Phone: {{ supplier_address_doc.phone }}\u0647\u0627\u062a\u0641: {{ supplier_address_doc.phone }}
Email: {{ supplier_address_doc.email_id }}\u0628\u0631\u064a\u062f \u0627\u0644\u0643\u062a\u0631\u0648\u0646\u064a: {{ supplier_address_doc.email_id }}
CUSTOMER:\u0639\u0645\u064a\u0644:
Customer Tax Identification Number:\u0631\u0642\u0645 \u0627\u0644\u062a\u0639\u0631\u064a\u0641 \u0627\u0644\u0636\u0631\u064a\u0628\u064a \u0644\u0644\u0639\u0645\u064a\u0644:
{{ customer_tax_id }}{{ customer_tax_id }}
{{ doc.customer }} {{ doc.customer_name_in_arabic }}
{{ customer_address.address_line1}} {{ customer_address.address_in_arabic}}
SHIPPING ADDRESS:\u0639\u0646\u0648\u0627\u0646 \u0627\u0644\u0634\u062d\u0646:
{{ customer_shipping_address.address_line1}} {{ customer_shipping_address.address_in_arabic}}
OTHER INFORMATION\u0645\u0639\u0644\u0648\u0645\u0627\u062a \u0623\u062e\u0631\u0649
Purchase Order Number: {{ doc.po_no }}\u0631\u0642\u0645 \u0623\u0645\u0631 \u0627\u0644\u0634\u0631\u0627\u0621: {{ doc.po_no }}
Payment Due Date: {{ doc.due_date}} \u062a\u0627\u0631\u064a\u062e \u0627\u0633\u062a\u062d\u0642\u0627\u0642 \u0627\u0644\u062f\u0641\u0639: {{ doc.due_date}}
\n\n \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 \n {% set total = namespace(amount = 0) %}\n \n \n \n \n \n \n \n \n {% for row in doc.taxes %}\n \n {% endfor %}\n \n \n \n \n \n {%- for item in doc.items -%}\n {% set total.amount = item.amount %}\n \n \n \n \n \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 \n {% endfor %}\n \n \n {%- endfor -%}\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
Nature of goods or services
\u0637\u0628\u064a\u0639\u0629 \u0627\u0644\u0633\u0644\u0639 \u0623\u0648 \u0627\u0644\u062e\u062f\u0645\u0627\u062a
\n Unit price
\n \u0633\u0639\u0631 \u0627\u0644\u0648\u062d\u062f\u0629\n
\n Quantity
\n \u0627\u0644\u0643\u0645\u064a\u0629\n
\n Taxable Amount
\n \u0627\u0644\u0645\u0628\u0644\u063a \u0627\u0644\u062e\u0627\u0636\u0639 \u0644\u0644\u0636\u0631\u064a\u0628\u0629\n
{{row.description}}\n Total
\n \u0627\u0644\u0645\u062c\u0645\u0648\u0639\n
{{ item.item_code or item.item_name }}{{ item.get_formatted(\"rate\") }}{{ item.qty }}{{ item.get_formatted(\"amount\") }}\n
\n {%- if(data_object[key][0])-%}\n {{ frappe.format(data_object[key][0], {'fieldtype': 'Percent'}) }}\n {%- endif -%}\n \n {%- if(data_object[key][1])-%}\n {{ frappe.format_value(tax_amount, currency=doc.currency) }}\n {% set total.amount = total.amount + tax_amount %}\n {%- endif -%}\n
\n
{{ frappe.format_value(frappe.utils.flt(total.amount, doc.precision('total_taxes_and_charges')), currency=doc.currency) }}
\n {{ doc.get_formatted(\"total\") }}
\n {{ doc.get_formatted(\"total_taxes_and_charges\") }}\n
\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
\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
\n Total (Excluding VAT)\n
\n Total VAT\n
\n {{ doc.get_formatted(\"total\") }}
\n {{ doc.get_formatted(\"total_taxes_and_charges\") }}\n
{{ doc.get_formatted(\"grand_total\") }}\n \u0625\u062c\u0645\u0627\u0644\u064a \u0627\u0644\u0645\u0628\u0644\u063a \u0627\u0644\u0645\u0633\u062a\u062d\u0642Total Amount Due{{ doc.get_formatted(\"grand_total\") }}
\n\n\t{%- if doc.terms -%}\n

\n {{doc.terms}}\n

\n\t{%- endif -%}\n
\n", - "idx": 0, - "line_breaks": 0, - "margin_bottom": 15.0, - "margin_left": 15.0, - "margin_right": 15.0, - "margin_top": 15.0, - "modified": "2021-12-07 13:43:38.018593", - "modified_by": "Administrator", - "module": "Regional", - "name": "KSA VAT 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/report/ksa_vat/ksa_vat.js b/erpnext/regional/report/ksa_vat/ksa_vat.js deleted file mode 100644 index 59e72c3e63..0000000000 --- a/erpnext/regional/report/ksa_vat/ksa_vat.js +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright (c) 2016, Havenir Solutions and contributors -// For license information, please see license.txt -/* eslint-disable */ - -frappe.query_reports["KSA VAT"] = { - onload() { - frappe.breadcrumbs.add('Accounts'); - }, - "filters": [ - { - "fieldname": "company", - "label": __("Company"), - "fieldtype": "Link", - "options": "Company", - "reqd": 1, - "default": frappe.defaults.get_user_default("Company") - }, - { - "fieldname": "from_date", - "label": __("From Date"), - "fieldtype": "Date", - "reqd": 1, - "default": frappe.datetime.add_months(frappe.datetime.get_today(), -1), - }, - { - "fieldname": "to_date", - "label": __("To Date"), - "fieldtype": "Date", - "reqd": 1, - "default": frappe.datetime.get_today() - } - ], - "formatter": function(value, row, column, data, default_formatter) { - if (data - && (data.title=='VAT on Sales' || data.title=='VAT on Purchases') - && data.title==value) { - value = $(`${value}`); - var $value = $(value).css("font-weight", "bold"); - value = $value.wrap("

").parent().html(); - return value - }else if (data.title=='Grand Total'){ - if (data.title==value) { - value = $(`${value}`); - var $value = $(value).css("font-weight", "bold"); - value = $value.wrap("

").parent().html(); - return value - }else{ - value = default_formatter(value, row, column, data); - value = $(`${value}`); - var $value = $(value).css("font-weight", "bold"); - value = $value.wrap("

").parent().html(); - return value - } - }else{ - value = default_formatter(value, row, column, data); - return value; - } - }, -}; diff --git a/erpnext/regional/report/ksa_vat/ksa_vat.py b/erpnext/regional/report/ksa_vat/ksa_vat.py deleted file mode 100644 index 15996d2d1f..0000000000 --- a/erpnext/regional/report/ksa_vat/ksa_vat.py +++ /dev/null @@ -1,231 +0,0 @@ -# Copyright (c) 2013, Havenir Solutions and contributors -# For license information, please see license.txt - - -import json - -import frappe -from frappe import _ -from frappe.utils import get_url_to_list - - -def execute(filters=None): - columns = columns = get_columns() - data = get_data(filters) - return columns, data - - -def get_columns(): - return [ - { - "fieldname": "title", - "label": _("Title"), - "fieldtype": "Data", - "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, - }, - ] - - -def get_data(filters): - data = [] - - # 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 KSA VAT Setting for this company').format(url)) - return data - - ksa_vat_setting = frappe.get_doc("KSA VAT Setting", company) - - # Sales Heading - append_data(data, "VAT on Sales", "", "", "", company_currency) - - grand_total_taxable_amount = 0 - grand_total_taxable_adjustment_amount = 0 - grand_total_tax = 0 - - for vat_setting in ksa_vat_setting.ksa_vat_sales_accounts: - ( - total_taxable_amount, - total_taxable_adjustment_amount, - total_tax, - ) = get_tax_data_for_each_vat_setting(vat_setting, filters, "Sales Invoice") - - # Adding results to data - append_data( - data, - vat_setting.title, - total_taxable_amount, - total_taxable_adjustment_amount, - total_tax, - company_currency, - ) - - grand_total_taxable_amount += total_taxable_amount - grand_total_taxable_adjustment_amount += total_taxable_adjustment_amount - grand_total_tax += total_tax - - # Sales Grand Total - append_data( - data, - "Grand Total", - grand_total_taxable_amount, - grand_total_taxable_adjustment_amount, - grand_total_tax, - company_currency, - ) - - # Blank Line - append_data(data, "", "", "", "", company_currency) - - # Purchase Heading - append_data(data, "VAT on Purchases", "", "", "", company_currency) - - grand_total_taxable_amount = 0 - grand_total_taxable_adjustment_amount = 0 - grand_total_tax = 0 - - for vat_setting in ksa_vat_setting.ksa_vat_purchase_accounts: - ( - total_taxable_amount, - total_taxable_adjustment_amount, - total_tax, - ) = get_tax_data_for_each_vat_setting(vat_setting, filters, "Purchase Invoice") - - # Adding results to data - append_data( - data, - vat_setting.title, - total_taxable_amount, - total_taxable_adjustment_amount, - total_tax, - company_currency, - ) - - grand_total_taxable_amount += total_taxable_amount - grand_total_taxable_adjustment_amount += total_taxable_adjustment_amount - grand_total_tax += total_tax - - # Purchase Grand Total - append_data( - data, - "Grand Total", - grand_total_taxable_amount, - grand_total_taxable_adjustment_amount, - grand_total_tax, - company_currency, - ) - - return data - - -def get_tax_data_for_each_vat_setting(vat_setting, filters, doctype): - """ - (KSA, {filters}, 'Sales Invoice') => 500, 153, 10 \n - calculates and returns \n - total_taxable_amount, total_taxable_adjustment_amount, total_tax""" - from_date = filters.get("from_date") - to_date = filters.get("to_date") - - # Initiate variables - total_taxable_amount = 0 - total_taxable_adjustment_amount = 0 - total_tax = 0 - # Fetch All Invoices - invoices = frappe.get_all( - doctype, - filters={"docstatus": 1, "posting_date": ["between", [from_date, to_date]]}, - fields=["name", "is_return"], - ) - - for invoice in invoices: - invoice_items = frappe.get_all( - f"{doctype} Item", - filters={ - "docstatus": 1, - "parent": invoice.name, - "item_tax_template": vat_setting.item_tax_template, - }, - fields=["item_code", "net_amount"], - ) - - for item in invoice_items: - # Summing up total taxable amount - if invoice.is_return == 0: - total_taxable_amount += item.net_amount - - if invoice.is_return == 1: - total_taxable_adjustment_amount += item.net_amount - - # Summing up total tax - total_tax += get_tax_amount(item.item_code, vat_setting.account, doctype, invoice.name) - - return total_taxable_amount, total_taxable_adjustment_amount, total_tax - - -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, - "currency": company_currency, - } - ) - - -def get_tax_amount(item_code, account_head, doctype, parent): - if doctype == "Sales Invoice": - tax_doctype = "Sales Taxes and Charges" - - elif doctype == "Purchase Invoice": - tax_doctype = "Purchase Taxes and Charges" - - item_wise_tax_detail = frappe.get_value( - tax_doctype, - {"docstatus": 1, "parent": parent, "account_head": account_head}, - "item_wise_tax_detail", - ) - - tax_amount = 0 - if item_wise_tax_detail and len(item_wise_tax_detail) > 0: - item_wise_tax_detail = json.loads(item_wise_tax_detail) - for key, value in item_wise_tax_detail.items(): - if key == item_code: - tax_amount = value[1] - break - - return tax_amount diff --git a/erpnext/regional/saudi_arabia/setup.py b/erpnext/regional/saudi_arabia/setup.py deleted file mode 100644 index 7f41c462cc..0000000000 --- a/erpnext/regional/saudi_arabia/setup.py +++ /dev/null @@ -1,173 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# License: GNU General Public License v3. See license.txt - -import frappe -from frappe.permissions import add_permission, update_permission_property -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 - - -def setup(company=None, patch=True): - add_print_formats() - 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) - for role in ("Accounts Manager", "Accounts User", "System Manager"): - add_permission("KSA VAT Setting", role, 0) - update_permission_property("KSA VAT Setting", role, 0, "write", 1) - update_permission_property("KSA VAT Setting", role, 0, "create", 1) - - """Enable KSA VAT Report""" - frappe.db.set_value("Report", "KSA VAT", "disabled", 0) - - -def make_custom_fields(): - """Create Custom fields - - QR code Image file - - Company Name in Arabic - - Address in Arabic - """ - is_zero_rated = dict( - fieldname="is_zero_rated", - label="Is Zero Rated", - fieldtype="Check", - fetch_from="item_code.is_zero_rated", - insert_after="description", - print_hide=1, - ) - - is_exempt = dict( - fieldname="is_exempt", - label="Is Exempt", - fieldtype="Check", - fetch_from="item_code.is_exempt", - insert_after="is_zero_rated", - print_hide=1, - ) - - purchase_invoice_fields = [ - dict( - fieldname="company_trn", - label="Company TRN", - fieldtype="Read Only", - insert_after="shipping_address", - fetch_from="company.tax_id", - print_hide=1, - ), - dict( - fieldname="supplier_name_in_arabic", - label="Supplier Name in Arabic", - fieldtype="Read Only", - insert_after="supplier_name", - fetch_from="supplier.supplier_name_in_arabic", - print_hide=1, - ), - ] - - sales_invoice_fields = [ - dict( - fieldname="company_trn", - label="Company TRN", - fieldtype="Read Only", - insert_after="company_address", - fetch_from="company.tax_id", - print_hide=1, - ), - dict( - fieldname="customer_name_in_arabic", - label="Customer Name in Arabic", - fieldtype="Read Only", - insert_after="customer_name", - fetch_from="customer.customer_name_in_arabic", - print_hide=1, - ), - dict( - fieldname="ksa_einv_qr", - label="KSA E-Invoicing QR", - fieldtype="Attach Image", - read_only=1, - no_copy=1, - hidden=1, - ), - ] - - custom_fields = { - "Item": [is_zero_rated, is_exempt], - "Customer": [ - dict( - fieldname="customer_name_in_arabic", - label="Customer Name in Arabic", - fieldtype="Data", - insert_after="customer_name", - ), - ], - "Supplier": [ - dict( - fieldname="supplier_name_in_arabic", - label="Supplier Name in Arabic", - fieldtype="Data", - insert_after="supplier_name", - ), - ], - "Purchase Invoice": purchase_invoice_fields, - "Purchase Order": purchase_invoice_fields, - "Purchase Receipt": purchase_invoice_fields, - "Sales Invoice": sales_invoice_fields, - "POS Invoice": sales_invoice_fields, - "Sales Order": sales_invoice_fields, - "Delivery Note": sales_invoice_fields, - "Sales Invoice Item": [is_zero_rated, is_exempt], - "POS Invoice Item": [is_zero_rated, is_exempt], - "Purchase Invoice Item": [is_zero_rated, is_exempt], - "Sales Order Item": [is_zero_rated, is_exempt], - "Delivery Note Item": [is_zero_rated, is_exempt], - "Quotation Item": [is_zero_rated, is_exempt], - "Purchase Order Item": [is_zero_rated, is_exempt], - "Purchase Receipt Item": [is_zero_rated, is_exempt], - "Supplier Quotation Item": [is_zero_rated, is_exempt], - "Address": [ - dict( - fieldname="address_in_arabic", - label="Address in Arabic", - fieldtype="Data", - insert_after="address_line2", - ) - ], - "Company": [ - dict( - fieldname="company_name_in_arabic", - label="Company Name In Arabic", - fieldtype="Data", - insert_after="company_name", - ) - ], - } - - create_custom_fields(custom_fields, ignore_validate=True, update=True) - - -def update_regional_tax_settings(country, company): - create_ksa_vat_setting(company) diff --git a/erpnext/regional/saudi_arabia/utils.py b/erpnext/regional/saudi_arabia/utils.py deleted file mode 100644 index b47adc95f7..0000000000 --- a/erpnext/regional/saudi_arabia/utils.py +++ /dev/null @@ -1,169 +0,0 @@ -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=None): - region = get_region(doc.company) - if region not in ["Saudi Arabia"]: - 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("ksa_einv_qr") - if qr_code and frappe.db.exists({"doctype": "File", "file_url": qr_code}): - return - - meta = frappe.get_meta(doc.doctype) - - 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 - - seller_name = frappe.db.get_value("Company", doc.company, "company_name_in_arabic") - - if not seller_name: - frappe.throw(_("Arabic name missing for {} in the company document").format(doc.company)) - - 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])) - - # 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)) - - tag = bytes([2]).hex() - length = bytes([len(tax_id)]).hex() - value = tax_id.encode("utf-8").hex() - tlv_array.append("".join([tag, length, value])) - - # 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(get_vat_amount(doc)) - - 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 get_vat_amount(doc): - vat_settings = frappe.db.get_value("KSA VAT Setting", {"company": doc.company}) - vat_accounts = [] - vat_amount = 0 - - if vat_settings: - vat_settings_doc = frappe.get_cached_doc("KSA VAT Setting", vat_settings) - - for row in vat_settings_doc.get("ksa_vat_sales_accounts"): - vat_accounts.append(row.account) - - for tax in doc.get("taxes"): - if tax.account_head in vat_accounts: - vat_amount += tax.tax_amount - - return vat_amount - - -def delete_qr_code_file(doc, method=None): - region = get_region(doc.company) - if region not in ["Saudi Arabia"]: - return - - if hasattr(doc, "ksa_einv_qr"): - if doc.get("ksa_einv_qr"): - file_doc = frappe.get_list("File", {"file_url": doc.get("ksa_einv_qr")}) - if len(file_doc): - 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/regional/saudi_arabia/wizard/data/ksa_vat_settings.json b/erpnext/regional/saudi_arabia/wizard/data/ksa_vat_settings.json deleted file mode 100644 index 60951a9cec..0000000000 --- a/erpnext/regional/saudi_arabia/wizard/data/ksa_vat_settings.json +++ /dev/null @@ -1,47 +0,0 @@ -[ - { - "type": "Sales Account", - "accounts": [ - { - "title": "Standard rated Sales", - "item_tax_template": "KSA VAT 5%", - "account": "VAT 5%" - }, - { - "title": "Zero rated domestic sales", - "item_tax_template": "KSA VAT Zero", - "account": "VAT Zero" - }, - { - "title": "Exempted sales", - "item_tax_template": "KSA VAT Exempted", - "account": "VAT Exempted" - } - ] - }, - { - "type": "Purchase Account", - "accounts": [ - { - "title": "Standard rated domestic purchases", - "item_tax_template": "KSA VAT 5%", - "account": "VAT 5%" - }, - { - "title": "Imports subject to VAT paid at customs", - "item_tax_template": "KSA Excise 50%", - "account": "Excise 50%" - }, - { - "title": "Zero rated purchases", - "item_tax_template": "KSA VAT Zero", - "account": "VAT Zero" - }, - { - "title": "Exempted purchases", - "item_tax_template": "KSA VAT Exempted", - "account": "VAT Exempted" - } - ] - } -] \ No newline at end of file diff --git a/erpnext/regional/saudi_arabia/wizard/operations/__init__.py b/erpnext/regional/saudi_arabia/wizard/operations/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/regional/saudi_arabia/wizard/operations/setup_ksa_vat_setting.py b/erpnext/regional/saudi_arabia/wizard/operations/setup_ksa_vat_setting.py deleted file mode 100644 index 66d9df224e..0000000000 --- a/erpnext/regional/saudi_arabia/wizard/operations/setup_ksa_vat_setting.py +++ /dev/null @@ -1,46 +0,0 @@ -import json -import os - -import frappe - - -def create_ksa_vat_setting(company): - """On creation of first company. Creates KSA VAT Setting""" - - company = frappe.get_doc("Company", company) - - file_path = os.path.join(os.path.dirname(__file__), "..", "data", "ksa_vat_settings.json") - with open(file_path, "r") as json_file: - account_data = json.load(json_file) - - # Creating KSA VAT Setting - ksa_vat_setting = frappe.get_doc({"doctype": "KSA VAT Setting", "company": company.name}) - - for data in account_data: - if data["type"] == "Sales Account": - for row in data["accounts"]: - item_tax_template = row["item_tax_template"] - account = row["account"] - ksa_vat_setting.append( - "ksa_vat_sales_accounts", - { - "title": row["title"], - "item_tax_template": f"{item_tax_template} - {company.abbr}", - "account": f"{account} - {company.abbr}", - }, - ) - - elif data["type"] == "Purchase Account": - for row in data["accounts"]: - item_tax_template = row["item_tax_template"] - account = row["account"] - ksa_vat_setting.append( - "ksa_vat_purchase_accounts", - { - "title": row["title"], - "item_tax_template": f"{item_tax_template} - {company.abbr}", - "account": f"{account} - {company.abbr}", - }, - ) - - ksa_vat_setting.save() diff --git a/erpnext/regional/united_arab_emirates/setup.py b/erpnext/regional/united_arab_emirates/setup.py index be621bcdd1..36a079546e 100644 --- a/erpnext/regional/united_arab_emirates/setup.py +++ b/erpnext/regional/united_arab_emirates/setup.py @@ -37,7 +37,7 @@ def make_custom_fields(): fieldname="vat_section", label="VAT Details", fieldtype="Section Break", - insert_after="group_same_items", + insert_after="language", print_hide=1, collapsible=1, ), diff --git a/erpnext/regional/united_states/setup.py b/erpnext/regional/united_states/setup.py index b8f85723f5..808bbb1a69 100644 --- a/erpnext/regional/united_states/setup.py +++ b/erpnext/regional/united_states/setup.py @@ -2,9 +2,6 @@ # License: GNU General Public License v3. See license.txt import frappe -import os -import json -from frappe.permissions import add_permission, update_permission_property from frappe.custom.doctype.custom_field.custom_field import create_custom_fields @@ -49,7 +46,7 @@ def make_custom_fields(update=True): dict( fieldname="exempt_from_sales_tax", fieldtype="Check", - insert_after="represents_company", + insert_after="dn_required", label="Is customer exempted from sales tax?", ) ], diff --git a/erpnext/selling/doctype/customer/customer.json b/erpnext/selling/doctype/customer/customer.json index 7cba141eec..c133cd3152 100644 --- a/erpnext/selling/doctype/customer/customer.json +++ b/erpnext/selling/doctype/customer/customer.json @@ -14,30 +14,35 @@ "naming_series", "salutation", "customer_name", + "customer_type", + "customer_group", + "column_break0", + "territory", "gender", - "default_bank_account", - "tax_id", - "tax_category", - "tax_withholding_category", "lead_name", "opportunity_name", - "image", - "column_break0", - "customer_group", - "customer_type", - "territory", "account_manager", - "so_required", - "dn_required", - "is_internal_customer", - "represents_company", - "disabled", - "allowed_to_transact_section", - "companies", - "currency_and_price_list", + "image", + "defaults_tab", "default_currency", + "default_bank_account", "column_break_14", "default_price_list", + "internal_customer_section", + "is_internal_customer", + "represents_company", + "column_break_70", + "companies", + "more_info", + "market_segment", + "industry", + "customer_pos_id", + "website", + "language", + "column_break_45", + "customer_details", + "dashboard_tab", + "contact_and_address_tab", "address_contacts", "address_html", "column_break1", @@ -49,34 +54,39 @@ "column_break_26", "customer_primary_address", "primary_address", - "default_receivable_accounts", - "accounts", + "tax_tab", + "taxation_section", + "tax_id", + "column_break_21", + "tax_category", + "tax_withholding_category", + "accounting_tab", "credit_limit_section", "payment_terms", "credit_limits", - "more_info", - "customer_details", - "column_break_45", - "market_segment", - "industry", - "website", - "language", - "is_frozen", - "column_break_38", + "default_receivable_accounts", + "accounts", + "loyalty_points_tab", "loyalty_program", + "column_break_54", "loyalty_program_tier", - "sales_team_section_break", - "default_sales_partner", - "default_commission_rate", - "sales_team_section", + "sales_team_tab", "sales_team", - "customer_pos_id" + "sales_team_section", + "default_sales_partner", + "column_break_66", + "default_commission_rate", + "settings_tab", + "so_required", + "dn_required", + "column_break_53", + "is_frozen", + "disabled" ], "fields": [ { "fieldname": "basic_info", "fieldtype": "Section Break", - "label": "Name and Type", "oldfieldtype": "Section Break", "options": "fa fa-user" }, @@ -215,12 +225,6 @@ "options": "Company", "unique": 1 }, - { - "depends_on": "represents_company", - "fieldname": "allowed_to_transact_section", - "fieldtype": "Section Break", - "label": "Allowed To Transact With" - }, { "depends_on": "represents_company", "fieldname": "companies", @@ -228,12 +232,6 @@ "label": "Allowed To Transact With", "options": "Allowed To Transact With" }, - { - "collapsible": 1, - "fieldname": "currency_and_price_list", - "fieldtype": "Section Break", - "label": "Currency and Price List" - }, { "fieldname": "default_currency", "fieldtype": "Link", @@ -295,7 +293,7 @@ "description": "Select, to make the customer searchable with these fields", "fieldname": "primary_address_and_contact_detail", "fieldtype": "Section Break", - "label": "Primary Address and Contact Detail" + "label": "Primary Address and Contact" }, { "description": "Reselect, if the chosen contact is edited after save", @@ -334,20 +332,18 @@ "read_only": 1 }, { - "collapsible": 1, "fieldname": "default_receivable_accounts", "fieldtype": "Section Break", "label": "Default Receivable Accounts" }, { - "description": "Mention if non-standard receivable account", + "description": "Mention if a non-standard receivable account", "fieldname": "accounts", "fieldtype": "Table", - "label": "Accounts", + "label": "Receivable Accounts", "options": "Party Account" }, { - "collapsible": 1, "fieldname": "credit_limit_section", "fieldtype": "Section Break", "label": "Credit Limit and Payment Terms" @@ -397,12 +393,6 @@ "fieldtype": "Check", "label": "Is Frozen" }, - { - "collapsible": 1, - "fieldname": "column_break_38", - "fieldtype": "Section Break", - "label": "Loyalty Points" - }, { "fieldname": "loyalty_program", "fieldtype": "Link", @@ -417,15 +407,6 @@ "no_copy": 1, "read_only": 1 }, - { - "collapsible": 1, - "collapsible_depends_on": "default_sales_partner", - "fieldname": "sales_team_section_break", - "fieldtype": "Section Break", - "label": "Sales Partner and Commission", - "oldfieldtype": "Section Break", - "options": "fa fa-group" - }, { "fieldname": "default_sales_partner", "fieldtype": "Link", @@ -446,13 +427,12 @@ "collapsible": 1, "collapsible_depends_on": "sales_team", "fieldname": "sales_team_section", - "fieldtype": "Section Break", - "label": "Sales Team" + "fieldtype": "Section Break" }, { "fieldname": "sales_team", "fieldtype": "Table", - "label": "Sales Team Details", + "label": "Sales Team", "oldfieldname": "sales_team", "oldfieldtype": "Table", "options": "Sales Team" @@ -498,6 +478,83 @@ "no_copy": 1, "options": "Opportunity", "print_hide": 1 + }, + { + "fieldname": "contact_and_address_tab", + "fieldtype": "Tab Break", + "label": "Contact & Address" + }, + { + "fieldname": "defaults_tab", + "fieldtype": "Section Break", + "label": "Defaults" + }, + { + "fieldname": "settings_tab", + "fieldtype": "Tab Break", + "label": "Settings" + }, + { + "collapsible": 1, + "collapsible_depends_on": "default_sales_partner", + "fieldname": "sales_team_tab", + "fieldtype": "Tab Break", + "label": "Sales Team", + "oldfieldtype": "Section Break", + "options": "fa fa-group" + }, + { + "fieldname": "column_break_66", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_21", + "fieldtype": "Column Break" + }, + { + "fieldname": "dashboard_tab", + "fieldtype": "Tab Break", + "label": "Dashboard", + "show_dashboard": 1 + }, + { + "fieldname": "column_break_53", + "fieldtype": "Column Break" + }, + { + "collapsible": 1, + "fieldname": "loyalty_points_tab", + "fieldtype": "Section Break", + "label": "Loyalty Points" + }, + { + "fieldname": "taxation_section", + "fieldtype": "Section Break" + }, + { + "fieldname": "accounting_tab", + "fieldtype": "Tab Break", + "label": "Accounting" + }, + { + "fieldname": "tax_tab", + "fieldtype": "Tab Break", + "label": "Tax" + }, + { + "collapsible": 1, + "collapsible_depends_on": "is_internal_customer", + "fieldname": "internal_customer_section", + "fieldtype": "Section Break", + "label": "Internal Customer" + }, + { + "fieldname": "column_break_70", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_54", + "fieldtype": "Column Break" } ], "icon": "fa fa-user", @@ -511,11 +568,10 @@ "link_fieldname": "party" } ], - "modified": "2022-04-16 20:32:34.000304", + "modified": "2023-02-18 11:04:46.343527", "modified_by": "Administrator", "module": "Selling", "name": "Customer", - "name_case": "Title Case", "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 6605685472..d9dab33501 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -6,7 +6,7 @@ import json import frappe import frappe.defaults -from frappe import _, msgprint +from frappe import _, msgprint, qb from frappe.contacts.address_and_contact import ( delete_contact_and_address, load_address_and_contact, @@ -27,9 +27,6 @@ from erpnext.utilities.transaction_base import TransactionBase class Customer(TransactionBase): - def get_feed(self): - return self.customer_name - def onload(self): """Load address and contacts in `__onload`""" load_address_and_contact(self) @@ -294,7 +291,7 @@ class Customer(TransactionBase): def after_rename(self, olddn, newdn, merge=False): if frappe.defaults.get_global_default("cust_master_name") == "Customer Name": - frappe.db.set(self, "customer_name", newdn) + self.db_set("customer_name", newdn) def set_loyalty_program(self): if self.loyalty_program: @@ -732,12 +729,15 @@ def make_address(args, is_primary_address=1): @frappe.validate_and_sanitize_search_inputs def get_customer_primary_contact(doctype, txt, searchfield, start, page_len, filters): customer = filters.get("customer") - return frappe.db.sql( - """ - select `tabContact`.name from `tabContact`, `tabDynamic Link` - where `tabContact`.name = `tabDynamic Link`.parent and `tabDynamic Link`.link_name = %(customer)s - and `tabDynamic Link`.link_doctype = 'Customer' - and `tabContact`.name like %(txt)s - """, - {"customer": customer, "txt": "%%%s%%" % txt}, + + con = qb.DocType("Contact") + dlink = qb.DocType("Dynamic Link") + + return ( + qb.from_(con) + .join(dlink) + .on(con.name == dlink.parent) + .select(con.name, con.email_id) + .where((dlink.link_name == customer) & (con.name.like(f"%{txt}%"))) + .run() ) diff --git a/erpnext/selling/doctype/customer/test_customer.py b/erpnext/selling/doctype/customer/test_customer.py index 7dc3fab623..a621c737ed 100644 --- a/erpnext/selling/doctype/customer/test_customer.py +++ b/erpnext/selling/doctype/customer/test_customer.py @@ -3,6 +3,7 @@ import frappe +from frappe.custom.doctype.property_setter.property_setter import make_property_setter from frappe.test_runner import make_test_records from frappe.tests.utils import FrappeTestCase from frappe.utils import flt @@ -341,6 +342,37 @@ class TestCustomer(FrappeTestCase): due_date = get_due_date("2017-01-22", "Customer", "_Test Customer") self.assertEqual(due_date, "2017-01-22") + def test_serach_fields_for_customer(self): + from erpnext.controllers.queries import customer_query + + frappe.db.set_value("Selling Settings", None, "cust_master_name", "Naming Series") + + make_property_setter( + "Customer", None, "search_fields", "customer_group", "Data", for_doctype="Doctype" + ) + + data = customer_query( + "Customer", "_Test Customer", "", 0, 20, filters={"name": "_Test Customer"}, as_dict=True + ) + + self.assertEqual(data[0].name, "_Test Customer") + self.assertEqual(data[0].customer_group, "_Test Customer Group") + self.assertTrue("territory" not in data[0]) + + make_property_setter( + "Customer", None, "search_fields", "customer_group, territory", "Data", for_doctype="Doctype" + ) + data = customer_query( + "Customer", "_Test Customer", "", 0, 20, filters={"name": "_Test Customer"}, as_dict=True + ) + + self.assertEqual(data[0].name, "_Test Customer") + self.assertEqual(data[0].customer_group, "_Test Customer Group") + self.assertEqual(data[0].territory, "_Test Territory") + self.assertTrue("territory" in data[0]) + + frappe.db.set_value("Selling Settings", None, "cust_master_name", "Customer Name") + def get_customer_dict(customer_name): return { diff --git a/erpnext/selling/doctype/customer_credit_limit/customer_credit_limit.json b/erpnext/selling/doctype/customer_credit_limit/customer_credit_limit.json index 800cb57f6c..2fd0c67405 100644 --- a/erpnext/selling/doctype/customer_credit_limit/customer_credit_limit.json +++ b/erpnext/selling/doctype/customer_credit_limit/customer_credit_limit.json @@ -12,7 +12,7 @@ ], "fields": [ { - "columns": 4, + "columns": 3, "fieldname": "credit_limit", "fieldtype": "Currency", "in_list_view": 1, @@ -31,6 +31,7 @@ "options": "Company" }, { + "columns": 3, "default": "0", "fieldname": "bypass_credit_limit_check", "fieldtype": "Check", @@ -40,7 +41,7 @@ ], "istable": 1, "links": [], - "modified": "2019-12-31 15:43:05.822328", + "modified": "2022-11-08 15:19:13.927194", "modified_by": "Administrator", "module": "Selling", "name": "Customer Credit Limit", @@ -48,5 +49,6 @@ "permissions": [], "quick_entry": 1, "sort_field": "modified", - "sort_order": "DESC" + "sort_order": "DESC", + "states": [] } \ No newline at end of file diff --git a/erpnext/selling/doctype/industry_type/industry_type.json b/erpnext/selling/doctype/industry_type/industry_type.json index 6c49f0f6dd..3c8ab8e47a 100644 --- a/erpnext/selling/doctype/industry_type/industry_type.json +++ b/erpnext/selling/doctype/industry_type/industry_type.json @@ -1,123 +1,68 @@ { - "allow_copy": 0, - "allow_import": 1, - "allow_rename": 1, - "autoname": "field:industry", - "beta": 0, - "creation": "2012-03-27 14:36:09", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Setup", - "editable_grid": 0, + "actions": [], + "allow_import": 1, + "allow_rename": 1, + "autoname": "field:industry", + "creation": "2012-03-27 14:36:09", + "doctype": "DocType", + "document_type": "Setup", + "engine": "InnoDB", + "field_order": [ + "industry" + ], "fields": [ { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "industry", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Industry", - "length": 0, - "no_copy": 0, - "oldfieldname": "industry", - "oldfieldtype": "Data", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "fieldname": "industry", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Industry", + "oldfieldname": "industry", + "oldfieldtype": "Data", + "reqd": 1, + "unique": 1 } - ], - "hide_heading": 0, - "hide_toolbar": 0, - "icon": "fa fa-flag", - "idx": 1, - "image_view": 0, - "in_create": 0, - - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2020-09-18 17:26:09.703215", - "modified_by": "Administrator", - "module": "Selling", - "name": "Industry Type", - "owner": "Administrator", + ], + "icon": "fa fa-flag", + "idx": 1, + "links": [], + "modified": "2023-02-10 03:14:40.735763", + "modified_by": "Administrator", + "module": "Selling", + "name": "Industry Type", + "naming_rule": "By fieldname", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 0, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Sales Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Sales Manager", + "share": 1, "write": 1 - }, + }, { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Sales User", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 - }, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Sales User" + }, { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 0, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Sales Master Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Sales Master Manager", + "share": 1, "write": 1 } - ], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "track_seen": 0 + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "translated_doctype": 1 } \ No newline at end of file diff --git a/erpnext/selling/doctype/installation_note/installation_note.py b/erpnext/selling/doctype/installation_note/installation_note.py index dd0b1e8751..0ef4754bc6 100644 --- a/erpnext/selling/doctype/installation_note/installation_note.py +++ b/erpnext/selling/doctype/installation_note/installation_note.py @@ -87,13 +87,13 @@ class InstallationNote(TransactionBase): frappe.throw(_("Please pull items from Delivery Note")) def on_update(self): - frappe.db.set(self, "status", "Draft") + self.db_set("status", "Draft") def on_submit(self): self.validate_serial_no() self.update_prevdoc_status() - frappe.db.set(self, "status", "Submitted") + self.db_set("status", "Submitted") def on_cancel(self): self.update_prevdoc_status() - frappe.db.set(self, "status", "Cancelled") + self.db_set("status", "Cancelled") diff --git a/erpnext/selling/doctype/party_specific_item/party_specific_item.json b/erpnext/selling/doctype/party_specific_item/party_specific_item.json index 32b5d478bb..a1f9902aae 100644 --- a/erpnext/selling/doctype/party_specific_item/party_specific_item.json +++ b/erpnext/selling/doctype/party_specific_item/party_specific_item.json @@ -1,5 +1,6 @@ { "actions": [], + "allow_import": 1, "creation": "2021-08-27 19:28:07.559978", "doctype": "DocType", "editable_grid": 1, @@ -51,7 +52,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2021-09-14 13:27:58.612334", + "modified": "2023-02-15 13:00:50.379713", "modified_by": "Administrator", "module": "Selling", "name": "Party Specific Item", @@ -72,6 +73,7 @@ ], "sort_field": "modified", "sort_order": "DESC", + "states": [], "title_field": "party", "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/selling/doctype/quotation/quotation.js b/erpnext/selling/doctype/quotation/quotation.js index 70ae085051..b348bd3575 100644 --- a/erpnext/selling/doctype/quotation/quotation.js +++ b/erpnext/selling/doctype/quotation/quotation.js @@ -84,11 +84,16 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext. } } - if(doc.docstatus == 1 && !(['Lost', 'Ordered']).includes(doc.status)) { - if(!doc.valid_till || frappe.datetime.get_diff(doc.valid_till, frappe.datetime.get_today()) >= 0) { - cur_frm.add_custom_button(__('Sales Order'), - cur_frm.cscript['Make Sales Order'], __('Create')); - } + if (doc.docstatus == 1 && !["Lost", "Ordered"].includes(doc.status)) { + if (frappe.boot.sysdefaults.allow_sales_order_creation_for_expired_quotation + || (!doc.valid_till) + || frappe.datetime.get_diff(doc.valid_till, frappe.datetime.get_today()) >= 0) { + this.frm.add_custom_button( + __("Sales Order"), + this.frm.cscript["Make Sales Order"], + __("Create") + ); + } if(doc.status!=="Ordered") { this.frm.add_custom_button(__('Set as Lost'), () => { diff --git a/erpnext/selling/doctype/quotation/quotation.json b/erpnext/selling/doctype/quotation/quotation.json index bb2f95dd17..eb2c0a48ac 100644 --- a/erpnext/selling/doctype/quotation/quotation.json +++ b/erpnext/selling/doctype/quotation/quotation.json @@ -15,26 +15,13 @@ "quotation_to", "party_name", "customer_name", - "column_break1", - "amended_from", - "company", + "column_break_7", "transaction_date", "valid_till", + "column_break1", "order_type", - "contact_section", - "customer_address", - "address_display", - "contact_person", - "contact_display", - "contact_mobile", - "contact_email", - "col_break98", - "shipping_address_name", - "shipping_address", - "company_address", - "company_address_display", - "customer_group", - "territory", + "company", + "amended_from", "currency_and_price_list", "currency", "conversion_rate", @@ -43,79 +30,107 @@ "price_list_currency", "plc_conversion_rate", "ignore_pricing_rule", - "section_break_33", - "scan_barcode", "items_section", + "scan_barcode", "items", - "bundle_items_section", - "packed_items", - "pricing_rule_details", - "pricing_rules", "sec_break23", "total_qty", + "total_net_weight", + "column_break_28", "base_total", "base_net_total", - "column_break_28", + "column_break_31", "total", "net_total", - "total_net_weight", "taxes_section", "tax_category", - "column_break_34", - "shipping_rule", - "section_break_36", "taxes_and_charges", + "column_break_36", + "shipping_rule", + "column_break_34", + "incoterm", + "named_place", + "section_break_36", "taxes", - "sec_tax_breakup", - "other_charges_calculation", "section_break_39", "base_total_taxes_and_charges", "column_break_42", "total_taxes_and_charges", - "section_break_44", - "coupon_code", - "referral_sales_partner", - "apply_discount_on", - "base_discount_amount", - "column_break_46", - "additional_discount_percentage", - "discount_amount", "totals", "base_grand_total", "base_rounding_adjustment", - "base_in_words", "base_rounded_total", + "base_in_words", "column_break3", "grand_total", "rounding_adjustment", "rounded_total", "in_words", + "section_break_44", + "apply_discount_on", + "base_discount_amount", + "coupon_code", + "column_break_46", + "additional_discount_percentage", + "discount_amount", + "referral_sales_partner", + "sec_tax_breakup", + "other_charges_calculation", + "bundle_items_section", + "packed_items", + "pricing_rule_details", + "pricing_rules", + "address_and_contact_tab", + "billing_address_section", + "customer_address", + "address_display", + "col_break98", + "contact_person", + "contact_display", + "contact_mobile", + "contact_email", + "shipping_address_section", + "shipping_address_name", + "column_break_81", + "shipping_address", + "company_address_section", + "company_address", + "column_break_87", + "company_address_display", + "terms_tab", "payment_schedule_section", "payment_terms_template", "payment_schedule", "terms_section_break", "tc_name", "terms", + "more_info_tab", + "subscription_section", + "auto_repeat", + "update_auto_repeat_reference", "print_settings", "letter_head", "group_same_items", "column_break_73", "select_print_heading", "language", - "subscription_section", - "auto_repeat", - "update_auto_repeat_reference", - "more_info", + "lost_reasons_section", + "lost_reasons", + "competitors", + "column_break_117", + "order_lost_reason", + "additional_info_section", + "status", + "customer_group", + "territory", + "column_break_108", "campaign", "source", - "order_lost_reason", "column_break4", - "status", - "enq_det", - "supplier_quotation", "opportunity", - "lost_reasons", - "competitors" + "supplier_quotation", + "enq_det", + "connections_tab" ], "fields": [ { @@ -241,14 +256,6 @@ "print_hide": 1, "reqd": 1 }, - { - "collapsible": 1, - "depends_on": "party_name", - "fieldname": "contact_section", - "fieldtype": "Section Break", - "label": "Address and Contact", - "options": "fa fa-bullhorn" - }, { "fieldname": "customer_address", "fieldtype": "Link", @@ -296,7 +303,7 @@ "read_only": 1 }, { - "depends_on": "eval:doc.quotation_to=='Customer' && doc.party_name", + "depends_on": "eval:(doc.quotation_to=='Customer' && doc.party_name)", "fieldname": "col_break98", "fieldtype": "Column Break", "width": "50%" @@ -316,7 +323,7 @@ "read_only": 1 }, { - "depends_on": "eval:doc.quotation_to=='Customer' && doc.party_name", + "depends_on": "eval:(doc.quotation_to=='Customer' && doc.party_name)", "fieldname": "customer_group", "fieldtype": "Link", "hidden": 1, @@ -402,13 +409,14 @@ "fieldname": "ignore_pricing_rule", "fieldtype": "Check", "label": "Ignore Pricing Rule", - "no_copy": 1, "permlevel": 1, "print_hide": 1 }, { "fieldname": "items_section", "fieldtype": "Section Break", + "hide_border": 1, + "label": "Items", "oldfieldtype": "Section Break", "options": "fa fa-shopping-cart" }, @@ -416,7 +424,6 @@ "allow_bulk_edit": 1, "fieldname": "items", "fieldtype": "Table", - "label": "Items", "oldfieldname": "quotation_details", "oldfieldtype": "Table", "options": "Quotation Item", @@ -424,6 +431,7 @@ "width": "40px" }, { + "collapsible": 1, "fieldname": "pricing_rule_details", "fieldtype": "Section Break", "label": "Pricing Rules" @@ -484,6 +492,7 @@ "read_only": 1 }, { + "depends_on": "total_net_weight", "fieldname": "total_net_weight", "fieldtype": "Float", "label": "Total Net Weight", @@ -493,6 +502,7 @@ { "fieldname": "taxes_section", "fieldtype": "Section Break", + "hide_border": 1, "label": "Taxes and Charges", "oldfieldtype": "Section Break", "options": "fa fa-money" @@ -518,7 +528,8 @@ }, { "fieldname": "section_break_36", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "hide_border": 1 }, { "fieldname": "taxes_and_charges", @@ -580,10 +591,9 @@ }, { "collapsible": 1, - "collapsible_depends_on": "discount_amount", "fieldname": "section_break_44", "fieldtype": "Section Break", - "label": "Additional Discount and Coupon Code" + "label": "Additional Discount" }, { "fieldname": "coupon_code", @@ -633,6 +643,7 @@ { "fieldname": "totals", "fieldtype": "Section Break", + "label": "Totals", "oldfieldtype": "Section Break", "options": "fa fa-money", "print_hide": 1 @@ -658,7 +669,6 @@ "read_only": 1 }, { - "description": "In Words will be visible once you save the Quotation.", "fieldname": "base_in_words", "fieldtype": "Data", "label": "In Words (Company Currency)", @@ -666,8 +676,7 @@ "oldfieldname": "in_words", "oldfieldtype": "Data", "print_hide": 1, - "read_only": 1, - "width": "200px" + "read_only": 1 }, { "fieldname": "base_rounded_total", @@ -750,7 +759,6 @@ "print_hide": 1 }, { - "collapsible": 1, "collapsible_depends_on": "terms", "fieldname": "terms_section_break", "fieldtype": "Section Break", @@ -825,7 +833,7 @@ { "fieldname": "subscription_section", "fieldtype": "Section Break", - "label": "Auto Repeat Section" + "label": "Auto Repeat" }, { "fieldname": "auto_repeat", @@ -838,20 +846,11 @@ }, { "allow_on_submit": 1, - "depends_on": "eval: doc.auto_repeat", + "depends_on": "eval:doc.auto_repeat", "fieldname": "update_auto_repeat_reference", "fieldtype": "Button", "label": "Update Auto Repeat Reference" }, - { - "collapsible": 1, - "fieldname": "more_info", - "fieldtype": "Section Break", - "label": "More Information", - "oldfieldtype": "Section Break", - "options": "fa fa-file-text", - "print_hide": 1 - }, { "fieldname": "campaign", "fieldtype": "Link", @@ -872,7 +871,7 @@ }, { "allow_on_submit": 1, - "depends_on": "eval:doc.status===\"Lost\"", + "depends_on": "eval:doc.status=='Lost'", "fieldname": "order_lost_reason", "fieldtype": "Small Text", "label": "Detailed Reason", @@ -935,7 +934,6 @@ "read_only": 1 }, { - "depends_on": "packed_items", "fieldname": "packed_items", "fieldtype": "Table", "label": "Bundle Items", @@ -944,7 +942,6 @@ }, { "collapsible": 1, - "collapsible_depends_on": "packed_items", "depends_on": "packed_items", "fieldname": "bundle_items_section", "fieldtype": "Section Break", @@ -971,22 +968,111 @@ "label": "Company Address", "read_only": 1 }, - { - "fieldname": "section_break_33", - "fieldtype": "Section Break" - }, { "fieldname": "scan_barcode", "fieldtype": "Data", "label": "Scan Barcode", "options": "Barcode" + }, + { + "fieldname": "address_and_contact_tab", + "fieldtype": "Tab Break", + "label": "Address & Contact" + }, + { + "fieldname": "terms_tab", + "fieldtype": "Tab Break", + "label": "Terms" + }, + { + "fieldname": "more_info_tab", + "fieldtype": "Tab Break", + "label": "More Info" + }, + { + "fieldname": "connections_tab", + "fieldtype": "Tab Break", + "label": "Connections", + "show_dashboard": 1 + }, + { + "fieldname": "column_break_7", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_31", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_36", + "fieldtype": "Column Break" + }, + { + "fieldname": "billing_address_section", + "fieldtype": "Section Break", + "label": "Billing Address", + "options": "fa fa-bullhorn" + }, + { + "fieldname": "shipping_address_section", + "fieldtype": "Section Break", + "label": "Shipping Address" + }, + { + "fieldname": "column_break_81", + "fieldtype": "Column Break" + }, + { + "fieldname": "company_address_section", + "fieldtype": "Section Break", + "label": "Company Address" + }, + { + "fieldname": "column_break_87", + "fieldtype": "Column Break" + }, + { + "collapsible": 1, + "depends_on": "eval:(doc.lost_reasons || doc.order_lost_reason)", + "fieldname": "lost_reasons_section", + "fieldtype": "Section Break", + "label": "Lost Reasons" + }, + { + "fieldname": "column_break_117", + "fieldtype": "Column Break" + }, + { + "collapsible": 1, + "fieldname": "additional_info_section", + "fieldtype": "Section Break", + "label": "Additional Info", + "oldfieldtype": "Section Break", + "options": "fa fa-file-text", + "print_hide": 1 + }, + { + "fieldname": "column_break_108", + "fieldtype": "Column Break" + }, + { + "fieldname": "incoterm", + "fieldtype": "Link", + "label": "Incoterm", + "options": "Incoterm" + }, + { + "depends_on": "incoterm", + "fieldname": "named_place", + "fieldtype": "Data", + "label": "Named Place" } ], "icon": "fa fa-shopping-cart", "idx": 82, "is_submittable": 1, "links": [], - "modified": "2022-06-11 20:35:32.635804", + "modified": "2022-12-12 18:32:28.671332", "modified_by": "Administrator", "module": "Selling", "name": "Quotation", @@ -1084,4 +1170,4 @@ "states": [], "timeline_field": "party_name", "title_field": "title" -} +} \ No newline at end of file diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index 863fbc4059..063813b2dc 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -119,10 +119,10 @@ class Quotation(SellingController): if not (self.is_fully_ordered() or self.is_partially_ordered()): get_lost_reasons = frappe.get_list("Quotation Lost Reason", fields=["name"]) lost_reasons_lst = [reason.get("name") for reason in get_lost_reasons] - frappe.db.set(self, "status", "Lost") + self.db_set("status", "Lost") if detailed_reason: - frappe.db.set(self, "order_lost_reason", detailed_reason) + self.db_set("order_lost_reason", detailed_reason) for reason in lost_reasons_list: if reason.get("lost_reason") in lost_reasons_lst: @@ -194,14 +194,18 @@ def get_list_context(context=None): @frappe.whitelist() -def make_sales_order(source_name, target_doc=None): - quotation = frappe.db.get_value( - "Quotation", source_name, ["transaction_date", "valid_till"], as_dict=1 - ) - if quotation.valid_till and ( - quotation.valid_till < quotation.transaction_date or quotation.valid_till < getdate(nowdate()) +def make_sales_order(source_name: str, target_doc=None): + if not frappe.db.get_singles_value( + "Selling Settings", "allow_sales_order_creation_for_expired_quotation" ): - frappe.throw(_("Validity period of this quotation has ended.")) + quotation = frappe.db.get_value( + "Quotation", source_name, ["transaction_date", "valid_till"], as_dict=1 + ) + if quotation.valid_till and ( + quotation.valid_till < quotation.transaction_date or quotation.valid_till < getdate(nowdate()) + ): + frappe.throw(_("Validity period of this quotation has ended.")) + return _make_sales_order(source_name, target_doc) @@ -247,7 +251,7 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): "Quotation": {"doctype": "Sales Order", "validation": {"docstatus": ["=", 1]}}, "Quotation Item": { "doctype": "Sales Order Item", - "field_map": {"parent": "prevdoc_docname"}, + "field_map": {"parent": "prevdoc_docname", "name": "quotation_item"}, "postprocess": update_item, "condition": lambda doc: doc.qty > 0, }, @@ -268,7 +272,7 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): def set_expired_status(): # filter out submitted non expired quotations whose validity has been ended - cond = "`tabQuotation`.docstatus = 1 and `tabQuotation`.status != 'Expired' and `tabQuotation`.valid_till < %s" + cond = "`tabQuotation`.docstatus = 1 and `tabQuotation`.status NOT IN ('Expired', 'Lost') and `tabQuotation`.valid_till < %s" # check if those QUO have SO against it so_against_quo = """ SELECT diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index 6f0b381fc1..cdf5f5d00c 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -30,6 +30,24 @@ class TestQuotation(FrappeTestCase): self.assertTrue(sales_order.get("payment_schedule")) + def test_maintain_rate_in_sales_cycle_is_enforced(self): + from erpnext.selling.doctype.quotation.quotation import make_sales_order + + maintain_rate = frappe.db.get_single_value("Selling Settings", "maintain_same_sales_rate") + frappe.db.set_single_value("Selling Settings", "maintain_same_sales_rate", 1) + + quotation = frappe.copy_doc(test_records[0]) + quotation.transaction_date = nowdate() + quotation.valid_till = add_months(quotation.transaction_date, 1) + quotation.insert() + quotation.submit() + + sales_order = make_sales_order(quotation.name) + sales_order.items[0].rate = 1 + self.assertRaises(frappe.ValidationError, sales_order.save) + + frappe.db.set_single_value("Selling Settings", "maintain_same_sales_rate", maintain_rate) + def test_make_sales_order_with_different_currency(self): from erpnext.selling.doctype.quotation.quotation import make_sales_order @@ -118,18 +136,31 @@ class TestQuotation(FrappeTestCase): sales_order.payment_schedule[1].due_date, getdate(add_days(quotation.transaction_date, 30)) ) - def test_valid_till(self): - from erpnext.selling.doctype.quotation.quotation import make_sales_order - + def test_valid_till_before_transaction_date(self): quotation = frappe.copy_doc(test_records[0]) quotation.valid_till = add_days(quotation.transaction_date, -1) self.assertRaises(frappe.ValidationError, quotation.validate) + def test_so_from_expired_quotation(self): + from erpnext.selling.doctype.quotation.quotation import make_sales_order + + frappe.db.set_single_value( + "Selling Settings", "allow_sales_order_creation_for_expired_quotation", 0 + ) + + quotation = frappe.copy_doc(test_records[0]) quotation.valid_till = add_days(nowdate(), -1) quotation.insert() quotation.submit() + self.assertRaises(frappe.ValidationError, make_sales_order, quotation.name) + frappe.db.set_single_value( + "Selling Settings", "allow_sales_order_creation_for_expired_quotation", 1 + ) + + make_sales_order(quotation.name) + def test_shopping_cart_without_website_item(self): if frappe.db.exists("Website Item", {"item_code": "_Test Item Home Desktop 100"}): frappe.get_last_doc("Website Item", {"item_code": "_Test Item Home Desktop 100"}).delete() diff --git a/erpnext/selling/doctype/quotation_item/quotation_item.json b/erpnext/selling/doctype/quotation_item/quotation_item.json index 31a95896bc..ca7dfd2337 100644 --- a/erpnext/selling/doctype/quotation_item/quotation_item.json +++ b/erpnext/selling/doctype/quotation_item/quotation_item.json @@ -90,7 +90,6 @@ "oldfieldtype": "Link", "options": "Item", "print_width": "150px", - "reqd": 1, "search_index": 1, "width": "150px" }, @@ -649,7 +648,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2021-07-15 12:40:51.074820", + "modified": "2022-12-25 02:49:53.926625", "modified_by": "Administrator", "module": "Selling", "name": "Quotation Item", diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 6b6ea89b63..fb64772479 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -59,7 +59,36 @@ frappe.ui.form.on("Sales Order", { }) }); } + + if (frm.doc.docstatus === 0 && frm.doc.is_internal_customer) { + frm.events.get_items_from_internal_purchase_order(frm); + } }, + + get_items_from_internal_purchase_order(frm) { + frm.add_custom_button(__('Purchase Order'), () => { + erpnext.utils.map_current_doc({ + method: 'erpnext.buying.doctype.purchase_order.purchase_order.make_inter_company_sales_order', + source_doctype: 'Purchase Order', + target: frm, + setters: [ + { + label: 'Supplier', + fieldname: 'supplier', + fieldtype: 'Link', + options: 'Supplier' + } + ], + get_query_filters: { + company: frm.doc.company, + is_internal_supplier: 1, + docstatus: 1, + status: ['!=', 'Completed'] + } + }); + }, __('Get Items From')); + }, + onload: function(frm) { if (!frm.doc.transaction_date){ frm.set_value('transaction_date', frappe.datetime.get_today()) @@ -95,6 +124,11 @@ frappe.ui.form.on("Sales Order", { return query; }); + // On cancel and amending a sales order with advance payment, reset advance paid amount + if (frm.is_new()) { + frm.set_value("advance_paid", 0) + } + frm.ignore_doctypes_on_cancel_all = ['Purchase Order']; }, diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json index 74c5c07e47..ccea8407ab 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.json +++ b/erpnext/selling/doctype/sales_order/sales_order.json @@ -15,37 +15,25 @@ "naming_series", "customer", "customer_name", + "tax_id", "order_type", - "skip_delivery_note", - "column_break1", - "amended_from", - "company", + "column_break_7", "transaction_date", "delivery_date", + "column_break1", "po_no", "po_date", - "tax_id", + "company", + "skip_delivery_note", + "amended_from", "accounting_dimensions_section", "cost_center", "dimension_col_break", "project", - "contact_info", - "customer_address", - "address_display", - "contact_person", - "contact_display", - "contact_phone", - "contact_mobile", - "contact_email", - "company_address", - "company_address_display", - "col_break46", - "shipping_address_name", - "shipping_address", - "dispatch_address_name", - "dispatch_address", - "customer_group", - "territory", + "column_break_77", + "source", + "campaign", + "custom_dimensions_section", "currency_and_price_list", "currency", "conversion_rate", @@ -55,46 +43,34 @@ "plc_conversion_rate", "ignore_pricing_rule", "sec_warehouse", + "scan_barcode", + "column_break_28", "set_warehouse", "items_section", - "scan_barcode", "items", - "packing_list", - "packed_items", - "pricing_rule_details", - "pricing_rules", "section_break_31", - "column_break_33a", "total_qty", + "total_net_weight", + "column_break_33", "base_total", "base_net_total", - "column_break_33", - "total_net_weight", + "column_break_33a", "total", "net_total", "taxes_section", "tax_category", + "taxes_and_charges", "column_break_38", "shipping_rule", + "column_break_49", + "incoterm", + "named_place", "section_break_40", - "taxes_and_charges", "taxes", - "sec_tax_breakup", - "other_charges_calculation", "section_break_43", "base_total_taxes_and_charges", "column_break_46", "total_taxes_and_charges", - "loyalty_points_redemption", - "loyalty_points", - "loyalty_amount", - "section_break_48", - "coupon_code", - "apply_discount_on", - "base_discount_amount", - "column_break_50", - "additional_discount_percentage", - "discount_amount", "totals", "base_grand_total", "base_rounding_adjustment", @@ -107,26 +83,49 @@ "in_words", "advance_paid", "disable_rounded_total", + "section_break_48", + "apply_discount_on", + "base_discount_amount", + "coupon_code", + "column_break_50", + "additional_discount_percentage", + "discount_amount", + "sec_tax_breakup", + "other_charges_calculation", + "packing_list", + "packed_items", + "pricing_rule_details", + "pricing_rules", + "contact_info", + "billing_address_column", + "customer_address", + "address_display", + "customer_group", + "territory", + "column_break_84", + "contact_person", + "contact_display", + "contact_phone", + "contact_mobile", + "contact_email", + "shipping_address_column", + "shipping_address_name", + "shipping_address", + "column_break_93", + "dispatch_address_name", + "dispatch_address", + "col_break46", + "company_address", + "column_break_92", + "company_address_display", "payment_schedule_section", + "payment_terms_section", "payment_terms_template", "payment_schedule", "terms_section_break", "tc_name", "terms", "more_info", - "is_internal_customer", - "represents_company", - "inter_company_order_reference", - "party_account_currency", - "column_break_77", - "source", - "campaign", - "printing_details", - "language", - "letter_head", - "column_break4", - "select_print_heading", - "group_same_items", "section_break_78", "status", "delivery_status", @@ -143,12 +142,29 @@ "total_commission", "section_break1", "sales_team", + "loyalty_points_redemption", + "loyalty_points", + "column_break_116", + "loyalty_amount", "subscription_section", "from_date", "to_date", "column_break_108", "auto_repeat", - "update_auto_repeat_reference" + "update_auto_repeat_reference", + "printing_details", + "letter_head", + "group_same_items", + "column_break4", + "select_print_heading", + "language", + "additional_info_section", + "is_internal_customer", + "represents_company", + "column_break_152", + "inter_company_order_reference", + "party_account_currency", + "connections_tab" ], "fields": [ { @@ -334,10 +350,10 @@ "collapsible": 1, "depends_on": "customer", "fieldname": "contact_info", - "fieldtype": "Section Break", + "fieldtype": "Tab Break", "hide_days": 1, "hide_seconds": 1, - "label": "Address and Contact", + "label": "Address & Contact", "options": "fa fa-bullhorn" }, { @@ -414,9 +430,10 @@ }, { "fieldname": "col_break46", - "fieldtype": "Column Break", + "fieldtype": "Section Break", "hide_days": 1, "hide_seconds": 1, + "label": "Company Address", "width": "50%" }, { @@ -544,15 +561,16 @@ "hide_days": 1, "hide_seconds": 1, "label": "Ignore Pricing Rule", - "no_copy": 1, "permlevel": 1, "print_hide": 1 }, { "fieldname": "sec_warehouse", "fieldtype": "Section Break", + "hide_border": 1, "hide_days": 1, - "hide_seconds": 1 + "hide_seconds": 1, + "label": "Items" }, { "fieldname": "set_warehouse", @@ -566,6 +584,7 @@ { "fieldname": "items_section", "fieldtype": "Section Break", + "hide_border": 1, "hide_days": 1, "hide_seconds": 1, "oldfieldtype": "Section Break", @@ -676,6 +695,7 @@ "read_only": 1 }, { + "depends_on": "total_net_weight", "fieldname": "total_net_weight", "fieldtype": "Float", "hide_days": 1, @@ -689,7 +709,7 @@ "fieldtype": "Section Break", "hide_days": 1, "hide_seconds": 1, - "label": "Taxes and Charges", + "label": "Taxes", "oldfieldtype": "Section Break", "options": "fa fa-money" }, @@ -721,6 +741,7 @@ { "fieldname": "section_break_40", "fieldtype": "Section Break", + "hide_border": 1, "hide_days": 1, "hide_seconds": 1 }, @@ -805,7 +826,7 @@ "hidden": 1, "hide_days": 1, "hide_seconds": 1, - "label": "Loyalty Points Redemption", + "label": "Loyalty Points", "print_hide": 1 }, { @@ -829,12 +850,11 @@ }, { "collapsible": 1, - "collapsible_depends_on": "discount_amount", "fieldname": "section_break_48", "fieldtype": "Section Break", "hide_days": 1, "hide_seconds": 1, - "label": "Additional Discount and Coupon Code" + "label": "Additional Discount" }, { "fieldname": "coupon_code", @@ -892,6 +912,7 @@ "fieldtype": "Section Break", "hide_days": 1, "hide_seconds": 1, + "label": "Totals", "oldfieldtype": "Section Break", "options": "fa fa-money", "print_hide": 1 @@ -1046,10 +1067,10 @@ }, { "fieldname": "payment_schedule_section", - "fieldtype": "Section Break", + "fieldtype": "Tab Break", "hide_days": 1, "hide_seconds": 1, - "label": "Payment Terms" + "label": "Terms" }, { "fieldname": "payment_terms_template", @@ -1071,13 +1092,12 @@ "print_hide": 1 }, { - "collapsible": 1, "collapsible_depends_on": "terms", "fieldname": "terms_section_break", "fieldtype": "Section Break", "hide_days": 1, "hide_seconds": 1, - "label": "Terms and Conditions", + "label": "Terms & Conditions", "oldfieldtype": "Section Break", "options": "fa fa-legal" }, @@ -1105,10 +1125,10 @@ "collapsible": 1, "collapsible_depends_on": "project", "fieldname": "more_info", - "fieldtype": "Section Break", + "fieldtype": "Tab Break", "hide_days": 1, "hide_seconds": 1, - "label": "More Information", + "label": "More Info", "oldfieldtype": "Section Break", "options": "fa fa-file-text", "print_hide": 1 @@ -1123,7 +1143,6 @@ "read_only": 1 }, { - "description": "Track this Sales Order against any Project", "fieldname": "project", "fieldtype": "Link", "hide_days": 1, @@ -1241,7 +1260,7 @@ "fieldtype": "Section Break", "hide_days": 1, "hide_seconds": 1, - "label": "Billing and Delivery Status", + "label": "Status", "oldfieldtype": "Column Break", "print_hide": 1, "width": "50%" @@ -1411,7 +1430,7 @@ "fieldtype": "Section Break", "hide_days": 1, "hide_seconds": 1, - "label": "Auto Repeat Section", + "label": "Auto Repeat", "no_copy": 1, "print_hide": 1, "read_only": 1 @@ -1543,13 +1562,88 @@ { "fieldname": "dimension_col_break", "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_7", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_28", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_49", + "fieldtype": "Column Break" + }, + { + "fieldname": "connections_tab", + "fieldtype": "Tab Break", + "label": "Connections", + "show_dashboard": 1 + }, + { + "fieldname": "payment_terms_section", + "fieldtype": "Section Break", + "label": "Payment Terms" + }, + { + "fieldname": "column_break_116", + "fieldtype": "Column Break" + }, + { + "fieldname": "billing_address_column", + "fieldtype": "Section Break", + "label": "Billing Address" + }, + { + "fieldname": "shipping_address_column", + "fieldtype": "Section Break", + "label": "Shipping Address" + }, + { + "fieldname": "column_break_93", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_84", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_92", + "fieldtype": "Column Break" + }, + { + "fieldname": "custom_dimensions_section", + "fieldtype": "Section Break" + }, + { + "collapsible": 1, + "fieldname": "additional_info_section", + "fieldtype": "Section Break", + "label": "Additional Info" + }, + { + "fieldname": "column_break_152", + "fieldtype": "Column Break" + }, + { + "fieldname": "incoterm", + "fieldtype": "Link", + "label": "Incoterm", + "options": "Incoterm" + }, + { + "depends_on": "incoterm", + "fieldname": "named_place", + "fieldtype": "Data", + "label": "Named Place" } ], "icon": "fa fa-file-text", "idx": 105, "is_submittable": 1, "links": [], - "modified": "2022-06-10 03:52:22.212953", + "modified": "2022-12-12 18:34:00.681780", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order", diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 8c03cb5b41..ca6a51a6f3 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -18,6 +18,7 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import ( update_linked_doc, validate_inter_company_party, ) +from erpnext.accounts.party import get_party_account from erpnext.controllers.selling_controller import SellingController from erpnext.manufacturing.doctype.production_plan.production_plan import ( get_items_for_material_requests, @@ -25,7 +26,7 @@ from erpnext.manufacturing.doctype.production_plan.production_plan import ( from erpnext.selling.doctype.customer.customer import check_credit_limit from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults from erpnext.stock.doctype.item.item import get_item_defaults -from erpnext.stock.get_item_details import get_default_bom +from erpnext.stock.get_item_details import get_default_bom, get_price_list_rate from erpnext.stock.stock_balance import get_reserved_qty, update_bin_qty form_grid_templates = {"items": "templates/form_grid/item_grid.html"} @@ -192,6 +193,9 @@ class SalesOrder(SellingController): {"Quotation": {"ref_dn_field": "prevdoc_docname", "compare_fields": [["company", "="]]}} ) + if cint(frappe.db.get_single_value("Selling Settings", "maintain_same_sales_rate")): + self.validate_rate_with_reference_doc([["Quotation", "prevdoc_docname", "quotation_item"]]) + def update_enquiry_status(self, prevdoc, flag): enq = frappe.db.sql( "select t2.prevdoc_docname from `tabQuotation` t1, `tabQuotation Item` t2 where t2.parent = t1.name and t1.name=%s", @@ -204,7 +208,7 @@ class SalesOrder(SellingController): for quotation in set(d.prevdoc_docname for d in self.get("items")): if quotation: doc = frappe.get_doc("Quotation", quotation) - if doc.docstatus == 2: + if doc.docstatus.is_cancelled(): frappe.throw(_("Quotation {0} is cancelled").format(quotation)) doc.set_status(update=True) @@ -245,7 +249,7 @@ class SalesOrder(SellingController): self.update_project() self.update_prevdoc_status("cancel") - frappe.db.set(self, "status", "Cancelled") + self.db_set("status", "Cancelled") self.update_blanket_order() @@ -586,6 +590,23 @@ def make_material_request(source_name, target_doc=None): target.qty = qty - requested_item_qty.get(source.name, 0) target.stock_qty = flt(target.qty) * flt(target.conversion_factor) + args = target.as_dict().copy() + args.update( + { + "company": source_parent.get("company"), + "price_list": frappe.db.get_single_value("Buying Settings", "buying_price_list"), + "currency": source_parent.get("currency"), + "conversion_rate": source_parent.get("conversion_rate"), + } + ) + + target.rate = flt( + get_price_list_rate(args=args, item_doc=frappe.get_cached_doc("Item", target.item_code)).get( + "price_list_rate" + ) + ) + target.amount = target.qty * target.rate + doc = get_mapped_doc( "Sales Order", source_name, @@ -626,6 +647,7 @@ def make_project(source_name, target_doc=None): "field_map": { "name": "sales_order", "base_grand_total": "estimated_costing", + "net_total": "total_sales_amount", }, }, }, @@ -727,6 +749,8 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False): if source.loyalty_points and source.order_type == "Shopping Cart": target.redeem_loyalty_points = 1 + target.debit_to = get_party_account("Customer", source.customer, source.company) + def update_item(source, target, source_parent): target.amount = flt(source.amount) - flt(source.billed_amt) target.base_amount = target.amount * flt(source_parent.conversion_rate) @@ -880,6 +904,9 @@ def get_events(start, end, filters=None): @frappe.whitelist() def make_purchase_order_for_default_supplier(source_name, selected_items=None, target_doc=None): """Creates Purchase Order for each Supplier. Returns a list of doc objects.""" + + from erpnext.setup.utils import get_exchange_rate + if not selected_items: return @@ -888,10 +915,20 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t def set_missing_values(source, target): target.supplier = supplier + target.currency = frappe.db.get_value( + "Supplier", filters={"name": supplier}, fieldname=["default_currency"] + ) + company_currency = frappe.db.get_value( + "Company", filters={"name": target.company}, fieldname=["default_currency"] + ) + + target.conversion_rate = get_exchange_rate(target.currency, company_currency, args="for_buying") + target.apply_discount_on = "" target.additional_discount_percentage = 0.0 target.discount_amount = 0.0 target.inter_company_order_reference = "" + target.shipping_rule = "" default_price_list = frappe.get_value("Supplier", supplier, "default_price_list") if default_price_list: @@ -1004,14 +1041,30 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None): ] items_to_map = list(set(items_to_map)) + def is_drop_ship_order(target): + drop_ship = True + for item in target.items: + if not item.delivered_by_supplier: + drop_ship = False + break + + return drop_ship + def set_missing_values(source, target): target.supplier = "" target.apply_discount_on = "" target.additional_discount_percentage = 0.0 target.discount_amount = 0.0 target.inter_company_order_reference = "" - target.customer = "" - target.customer_name = "" + target.shipping_rule = "" + + if is_drop_ship_order(target): + target.customer = source.customer + target.customer_name = source.customer_name + target.shipping_address = source.shipping_address_name + else: + target.customer = target.customer_name = target.shipping_address = None + target.run_method("set_missing_values") target.run_method("calculate_taxes_and_totals") diff --git a/erpnext/selling/doctype/sales_order/sales_order_dashboard.py b/erpnext/selling/doctype/sales_order/sales_order_dashboard.py index ace2e29c2b..cbc40bbf90 100644 --- a/erpnext/selling/doctype/sales_order/sales_order_dashboard.py +++ b/erpnext/selling/doctype/sales_order/sales_order_dashboard.py @@ -12,7 +12,9 @@ def get_data(): "Auto Repeat": "reference_document", "Maintenance Visit": "prevdoc_docname", }, - "internal_links": {"Quotation": ["items", "prevdoc_docname"]}, + "internal_links": { + "Quotation": ["items", "prevdoc_docname"], + }, "transactions": [ { "label": _("Fulfillment"), diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index adfb39c1ae..d4d7c58eb8 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -6,7 +6,7 @@ import json import frappe import frappe.permissions from frappe.core.doctype.user_permission.test_user_permission import create_user -from frappe.tests.utils import FrappeTestCase +from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import add_days, flt, getdate, nowdate, today from erpnext.controllers.accounts_controller import update_child_qty_rate @@ -552,6 +552,42 @@ class TestSalesOrder(FrappeTestCase): workflow.is_active = 0 workflow.save() + def test_bin_details_of_packed_item(self): + # test Update Items with product bundle + if not frappe.db.exists("Item", "_Test Product Bundle Item New"): + bundle_item = make_item("_Test Product Bundle Item New", {"is_stock_item": 0}) + bundle_item.append( + "item_defaults", {"company": "_Test Company", "default_warehouse": "_Test Warehouse - _TC"} + ) + bundle_item.save(ignore_permissions=True) + + make_item("_Packed Item New 1", {"is_stock_item": 1}) + make_product_bundle("_Test Product Bundle Item New", ["_Packed Item New 1"], 2) + + so = make_sales_order( + item_code="_Test Product Bundle Item New", + warehouse="_Test Warehouse - _TC", + transaction_date=add_days(nowdate(), -1), + do_not_submit=1, + ) + + make_stock_entry(item="_Packed Item New 1", target="_Test Warehouse - _TC", qty=120, rate=100) + + bin_details = frappe.db.get_value( + "Bin", + {"item_code": "_Packed Item New 1", "warehouse": "_Test Warehouse - _TC"}, + ["actual_qty", "projected_qty", "ordered_qty"], + as_dict=1, + ) + + so.transaction_date = nowdate() + so.save() + + packed_item = so.packed_items[0] + self.assertEqual(flt(bin_details.actual_qty), flt(packed_item.actual_qty)) + self.assertEqual(flt(bin_details.projected_qty), flt(packed_item.projected_qty)) + self.assertEqual(flt(bin_details.ordered_qty), flt(packed_item.ordered_qty)) + def test_update_child_product_bundle(self): # test Update Items with product bundle if not frappe.db.exists("Item", "_Product Bundle Item"): @@ -1346,6 +1382,33 @@ class TestSalesOrder(FrappeTestCase): self.assertRaises(frappe.LinkExistsError, so_doc.cancel) + @change_settings("Accounts Settings", {"unlink_advance_payment_on_cancelation_of_order": 1}) + def test_advance_paid_upon_payment_cancellation(self): + from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry + + so = make_sales_order() + + pe = get_payment_entry("Sales Order", so.name, bank_account="_Test Bank - _TC") + pe.reference_no = "1" + pe.reference_date = nowdate() + pe.paid_from_account_currency = so.currency + pe.paid_to_account_currency = so.currency + pe.source_exchange_rate = 1 + pe.target_exchange_rate = 1 + pe.paid_amount = so.grand_total + pe.save(ignore_permissions=True) + pe.submit() + so.reload() + + self.assertEqual(so.advance_paid, so.base_grand_total) + + # cancel advance payment + pe.reload() + pe.cancel() + + so.reload() + self.assertEqual(so.advance_paid, 0) + def test_cancel_sales_order_after_cancel_payment_entry(self): from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry @@ -1747,6 +1810,69 @@ class TestSalesOrder(FrappeTestCase): sales_order.save() self.assertEqual(sales_order.taxes[0].tax_amount, 0) + def test_sales_order_partial_advance_payment(self): + from erpnext.accounts.doctype.payment_entry.test_payment_entry import ( + create_payment_entry, + get_payment_entry, + ) + from erpnext.selling.doctype.customer.test_customer import get_customer_dict + + # Make a customer + customer = get_customer_dict("QA Logistics") + frappe.get_doc(customer).insert() + + # Make a Sales Order + so = make_sales_order( + customer="QA Logistics", + item_list=[ + {"item_code": "_Test Item", "qty": 1, "rate": 200}, + {"item_code": "_Test Item 2", "qty": 1, "rate": 300}, + ], + ) + + # Create a advance payment against that Sales Order + pe = get_payment_entry("Sales Order", so.name, bank_account="_Test Bank - _TC") + pe.reference_no = "1" + pe.reference_date = nowdate() + pe.paid_from_account_currency = so.currency + pe.paid_to_account_currency = so.currency + pe.source_exchange_rate = 1 + pe.target_exchange_rate = 1 + pe.paid_amount = so.grand_total + pe.save(ignore_permissions=True) + pe.submit() + + # Make standalone advance payment entry + create_payment_entry( + payment_type="Receive", + party_type="Customer", + party="QA Logistics", + paid_from="Debtors - _TC", + paid_to="_Test Bank - _TC", + save=1, + submit=1, + ) + + si = make_sales_invoice(so.name) + + item = si.get("items")[1] + si.remove(item) + + si.allocate_advances_automatically = 1 + si.save() + + self.assertEqual(len(si.get("advances")), 1) + self.assertEqual(si.get("advances")[0].allocated_amount, 200) + self.assertEqual(si.get("advances")[0].reference_name, pe.name) + + si.submit() + pe.load_from_db() + + self.assertEqual(pe.references[0].reference_name, si.name) + self.assertEqual(pe.references[0].allocated_amount, 200) + self.assertEqual(pe.references[1].reference_name, so.name) + self.assertEqual(pe.references[1].allocated_amount, 300) + def automatically_fetch_payment_terms(enable=1): accounts_settings = frappe.get_doc("Accounts Settings") 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 318799907e..d0dabad5c9 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -70,6 +70,7 @@ "warehouse", "target_warehouse", "prevdoc_docname", + "quotation_item", "col_break4", "against_blanket_order", "blanket_order", @@ -92,7 +93,13 @@ "section_break_63", "page_break", "item_tax_rate", - "transaction_date" + "transaction_date", + "inter_transfer_reference_section", + "material_request", + "purchase_order", + "column_break_89", + "material_request_item", + "purchase_order_item" ], "fields": [ { @@ -107,7 +114,6 @@ "oldfieldtype": "Link", "options": "Item", "print_width": "150px", - "reqd": 1, "width": "150px" }, { @@ -268,6 +274,7 @@ }, { "collapsible": 1, + "collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount", "fieldname": "discount_and_margin", "fieldtype": "Section Break", "label": "Discount and Margin" @@ -809,12 +816,55 @@ "label": "Picked Qty (in Stock UOM)", "no_copy": 1, "read_only": 1 + }, + { + "fieldname": "inter_transfer_reference_section", + "fieldtype": "Section Break", + "label": "Inter Transfer Reference" + }, + { + "fieldname": "purchase_order", + "fieldtype": "Link", + "label": "Purchase Order", + "options": "Purchase Order", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "column_break_89", + "fieldtype": "Column Break" + }, + { + "fieldname": "purchase_order_item", + "fieldtype": "Data", + "label": "Purchase Order Item", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "quotation_item", + "fieldtype": "Data", + "hidden": 1, + "label": "quotation_item", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "material_request", + "fieldtype": "Link", + "label": "Material Request", + "options": "Material Request" + }, + { + "fieldname": "material_request_item", + "fieldtype": "Data", + "label": "Material Request Item" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2022-06-17 05:27:41.603006", + "modified": "2022-12-25 02:51:10.247569", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order Item", diff --git a/erpnext/selling/doctype/sales_partner_type/sales_partner_type.json b/erpnext/selling/doctype/sales_partner_type/sales_partner_type.json index e7dd0d84a0..a9b500a625 100644 --- a/erpnext/selling/doctype/sales_partner_type/sales_partner_type.json +++ b/erpnext/selling/doctype/sales_partner_type/sales_partner_type.json @@ -1,94 +1,47 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "field:sales_partner_type", - "beta": 0, - "creation": "2018-06-11 13:15:57.404716", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "autoname": "field:sales_partner_type", + "creation": "2018-06-11 13:15:57.404716", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "sales_partner_type" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "sales_partner_type", - "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": "Sales Partner Type", - "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": "sales_partner_type", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Sales Partner Type", + "reqd": 1, + "unique": 1 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-06-11 13:45:13.554307", - "modified_by": "Administrator", - "module": "Selling", - "name": "Sales Partner Type", - "name_case": "", - "owner": "Administrator", + ], + "links": [], + "modified": "2023-02-10 01:00:20.110800", + "modified_by": "Administrator", + "module": "Selling", + "name": "Sales Partner Type", + "naming_rule": "By fieldname", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, "write": 1 } - ], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 0, - "track_seen": 0 + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "translated_doctype": 1 } \ No newline at end of file diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.json b/erpnext/selling/doctype/selling_settings/selling_settings.json index 2abb169b8a..6ea66a0237 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.json +++ b/erpnext/selling/doctype/selling_settings/selling_settings.json @@ -27,6 +27,7 @@ "column_break_5", "allow_multiple_items", "allow_against_multiple_purchase_orders", + "allow_sales_order_creation_for_expired_quotation", "hide_tax_id", "enable_discount_accounting" ], @@ -172,6 +173,12 @@ "fieldname": "enable_discount_accounting", "fieldtype": "Check", "label": "Enable Discount Accounting for Selling" + }, + { + "default": "0", + "fieldname": "allow_sales_order_creation_for_expired_quotation", + "fieldtype": "Check", + "label": "Allow Sales Order Creation For Expired Quotation" } ], "icon": "fa fa-cog", @@ -179,7 +186,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2022-05-31 19:39:48.398738", + "modified": "2023-02-04 12:37:53.380857", "modified_by": "Administrator", "module": "Selling", "name": "Selling Settings", diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.py b/erpnext/selling/page/point_of_sale/point_of_sale.py index 13d5069ea6..158ac1d049 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.py +++ b/erpnext/selling/page/point_of_sale/point_of_sale.py @@ -6,6 +6,7 @@ import json from typing import Dict, Optional import frappe +from frappe.utils import cint from frappe.utils.nestedset import get_root_of from erpnext.accounts.doctype.pos_invoice.pos_invoice import get_stock_availability @@ -16,45 +17,79 @@ from erpnext.stock.utils import scan_barcode def search_by_term(search_term, warehouse, price_list): result = search_for_serial_or_batch_or_barcode_number(search_term) or {} - item_code = result.get("item_code") or search_term - serial_no = result.get("serial_no") or "" - batch_no = result.get("batch_no") or "" - barcode = result.get("barcode") or "" + item_code = result.get("item_code", search_term) + serial_no = result.get("serial_no", "") + batch_no = result.get("batch_no", "") + barcode = result.get("barcode", "") - if result: - item_info = frappe.db.get_value( - "Item", - item_code, - [ - "name as item_code", - "item_name", - "description", - "stock_uom", - "image as item_image", - "is_stock_item", - ], - as_dict=1, - ) + if not result: + return - item_stock_qty, is_stock_item = get_stock_availability(item_code, warehouse) - price_list_rate, currency = frappe.db.get_value( - "Item Price", - {"price_list": price_list, "item_code": item_code}, - ["price_list_rate", "currency"], - ) or [None, None] + item_doc = frappe.get_doc("Item", item_code) - item_info.update( + if not item_doc: + return + + item = { + "barcode": barcode, + "batch_no": batch_no, + "description": item_doc.description, + "is_stock_item": item_doc.is_stock_item, + "item_code": item_doc.name, + "item_image": item_doc.image, + "item_name": item_doc.item_name, + "serial_no": serial_no, + "stock_uom": item_doc.stock_uom, + "uom": item_doc.stock_uom, + } + + if barcode: + barcode_info = next(filter(lambda x: x.barcode == barcode, item_doc.get("barcodes", [])), None) + if barcode_info and barcode_info.uom: + uom = next(filter(lambda x: x.uom == barcode_info.uom, item_doc.uoms), {}) + item.update( + { + "uom": barcode_info.uom, + "conversion_factor": uom.get("conversion_factor", 1), + } + ) + + item_stock_qty, is_stock_item = get_stock_availability(item_code, warehouse) + item_stock_qty = item_stock_qty // item.get("conversion_factor") + item.update({"actual_qty": item_stock_qty}) + + price = frappe.get_list( + doctype="Item Price", + filters={ + "price_list": price_list, + "item_code": item_code, + }, + fields=["uom", "stock_uom", "currency", "price_list_rate"], + ) + + def __sort(p): + p_uom = p.get("uom") + + if p_uom == item.get("uom"): + return 0 + elif p_uom == item.get("stock_uom"): + return 1 + else: + return 2 + + # sort by fallback preference. always pick exact uom match if available + price = sorted(price, key=__sort) + + if len(price) > 0: + p = price.pop(0) + item.update( { - "serial_no": serial_no, - "batch_no": batch_no, - "barcode": barcode, - "price_list_rate": price_list_rate, - "currency": currency, - "actual_qty": item_stock_qty, + "currency": p.get("currency"), + "price_list_rate": p.get("price_list_rate"), } ) - return {"items": [item_info]} + return {"items": [item]} @frappe.whitelist() @@ -108,10 +143,10 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te item.name asc LIMIT {page_length} offset {start}""".format( - start=start, - page_length=page_length, - lft=lft, - rgt=rgt, + start=cint(start), + page_length=cint(page_length), + lft=cint(lft), + rgt=cint(rgt), condition=condition, bin_join_selection=bin_join_selection, bin_join_condition=bin_join_condition, @@ -120,33 +155,43 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te as_dict=1, ) - if items_data: - items = [d.item_code for d in items_data] - item_prices_data = frappe.get_all( + # return (empty) list if there are no results + if not items_data: + return result + + for item in items_data: + uoms = frappe.get_doc("Item", item.item_code).get("uoms", []) + + item.actual_qty, _ = get_stock_availability(item.item_code, warehouse) + item.uom = item.stock_uom + + item_price = frappe.get_all( "Item Price", - fields=["item_code", "price_list_rate", "currency"], - filters={"price_list": price_list, "item_code": ["in", items]}, + fields=["price_list_rate", "currency", "uom"], + filters={ + "price_list": price_list, + "item_code": item.item_code, + "selling": True, + }, ) - item_prices = {} - for d in item_prices_data: - item_prices[d.item_code] = d + if not item_price: + result.append(item) - for item in items_data: - item_code = item.item_code - item_price = item_prices.get(item_code) or {} - item_stock_qty, is_stock_item = get_stock_availability(item_code, warehouse) + for price in item_price: + uom = next(filter(lambda x: x.uom == price.uom, uoms), {}) - row = {} - row.update(item) - row.update( + if price.uom != item.stock_uom and uom and uom.conversion_factor: + item.actual_qty = item.actual_qty // uom.conversion_factor + + result.append( { - "price_list_rate": item_price.get("price_list_rate"), - "currency": item_price.get("currency"), - "actual_qty": item_stock_qty, + **item, + "price_list_rate": price.get("price_list_rate"), + "currency": price.get("currency"), + "uom": price.uom or item.uom, } ) - result.append(row) return {"items": result} @@ -253,16 +298,20 @@ def get_past_order_list(search_term, status, limit=20): "POS Invoice", filters={"customer": ["like", "%{}%".format(search_term)], "status": status}, fields=fields, + page_length=limit, ) invoices_by_name = frappe.db.get_all( "POS Invoice", filters={"name": ["like", "%{}%".format(search_term)], "status": status}, fields=fields, + page_length=limit, ) invoice_list = invoices_by_customer + invoices_by_name elif status: - invoice_list = frappe.db.get_all("POS Invoice", filters={"status": status}, fields=fields) + invoice_list = frappe.db.get_all( + "POS Invoice", filters={"status": status}, fields=fields, page_length=limit + ) return invoice_list diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index da7576e08d..c442774d0f 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -67,7 +67,7 @@ erpnext.PointOfSale.Controller = class { { fieldtype: 'Link', label: __('POS Profile'), options: 'POS Profile', fieldname: 'pos_profile', reqd: 1, - get_query: () => pos_profile_query, + get_query: () => pos_profile_query(), onchange: () => fetch_pos_payment_methods() }, { @@ -101,9 +101,11 @@ erpnext.PointOfSale.Controller = class { primary_action_label: __('Submit') }); dialog.show(); - const pos_profile_query = { - query: 'erpnext.accounts.doctype.pos_profile.pos_profile.pos_profile_query', - filters: { company: dialog.fields_dict.company.get_value() } + const pos_profile_query = () => { + return { + query: 'erpnext.accounts.doctype.pos_profile.pos_profile.pos_profile_query', + filters: { company: dialog.fields_dict.company.get_value() } + } }; } @@ -540,12 +542,12 @@ erpnext.PointOfSale.Controller = class { if (!this.frm.doc.customer) return this.raise_customer_selection_alert(); - const { item_code, batch_no, serial_no, rate } = item; + const { item_code, batch_no, serial_no, rate, uom } = item; if (!item_code) return; - const new_item = { item_code, batch_no, rate, [field]: value }; + const new_item = { item_code, batch_no, rate, uom, [field]: value }; if (serial_no) { await this.check_serial_no_availablilty(item_code, this.frm.doc.set_warehouse, serial_no); @@ -647,6 +649,7 @@ erpnext.PointOfSale.Controller = class { const is_stock_item = resp[1]; frappe.dom.unfreeze(); + const bold_uom = item_row.stock_uom.bold(); const bold_item_code = item_row.item_code.bold(); const bold_warehouse = warehouse.bold(); const bold_available_qty = available_qty.toString().bold() @@ -660,9 +663,9 @@ erpnext.PointOfSale.Controller = class { } else { return; } - } else if (available_qty < qty_needed) { + } else if (is_stock_item && available_qty < qty_needed) { 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]), + message: __('Stock quantity not enough for Item Code: {0} under warehouse {1}. Available quantity {2} {3}.', [bold_item_code, bold_warehouse, bold_available_qty, bold_uom]), indicator: 'orange' }); frappe.utils.play_sound("error"); @@ -694,7 +697,7 @@ erpnext.PointOfSale.Controller = class { callback(res) { if (!me.item_stock_map[item_code]) me.item_stock_map[item_code] = {}; - me.item_stock_map[item_code][warehouse] = res.message[0]; + me.item_stock_map[item_code][warehouse] = res.message; } }); } 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 eacf480ef8..12cc629776 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_cart.js +++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js @@ -524,7 +524,8 @@ erpnext.PointOfSale.ItemCart = class { 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}%`; + // if tax rate is 0, don't print it. + const description = /[0-9]+/.test(t.description) ? t.description : ((t.rate != 0) ? `${t.description} @ ${t.rate}%`: t.description); return `
${description}
${format_currency(t.tax_amount_after_discount_amount, currency)}
@@ -608,7 +609,7 @@ erpnext.PointOfSale.ItemCart = class { if (item_data.rate && item_data.amount && item_data.rate !== item_data.amount) { return `
-
${item_data.qty || 0}
+
${item_data.qty || 0} ${item_data.uom}
${format_currency(item_data.amount, currency)}
${format_currency(item_data.rate, currency)}
@@ -617,7 +618,7 @@ erpnext.PointOfSale.ItemCart = class { } else { return `
-
${item_data.qty || 0}
+
${item_data.qty || 0} ${item_data.uom}
${format_currency(item_data.rate, currency)}
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 b75ffb235e..f9b5bb2e45 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_details.js +++ b/erpnext/selling/page/point_of_sale/pos_item_details.js @@ -242,13 +242,14 @@ erpnext.PointOfSale.ItemDetails = class { if (this.value) { me.events.form_updated(me.current_item, 'warehouse', this.value).then(() => { me.item_stock_map = me.events.get_item_stock_map(); - const available_qty = me.item_stock_map[me.item_row.item_code] && me.item_stock_map[me.item_row.item_code][this.value]; + const available_qty = me.item_stock_map[me.item_row.item_code][this.value][0]; + const is_stock_item = Boolean(me.item_stock_map[me.item_row.item_code][this.value][1]); if (available_qty === undefined) { me.events.get_available_stock(me.item_row.item_code, this.value).then(() => { // item stock map is updated now reset warehouse me.warehouse_control.set_value(this.value); }) - } else if (available_qty === 0) { + } else if (available_qty === 0 && is_stock_item) { me.warehouse_control.set_value(''); const bold_item_code = me.item_row.item_code.bold(); const bold_warehouse = this.value.bold(); 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 7a90fb044f..ec67bdfd9d 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_selector.js +++ b/erpnext/selling/page/point_of_sale/pos_item_selector.js @@ -78,7 +78,7 @@ erpnext.PointOfSale.ItemSelector = class { get_item_html(item) { const me = this; // eslint-disable-next-line no-unused-vars - const { item_image, serial_no, batch_no, barcode, actual_qty, stock_uom, price_list_rate } = item; + const { item_image, serial_no, batch_no, barcode, actual_qty, uom, price_list_rate } = item; const precision = flt(price_list_rate, 2) % 1 != 0 ? 2 : 0; let indicator_color; let qty_to_display = actual_qty; @@ -103,9 +103,9 @@ erpnext.PointOfSale.ItemSelector = class {
${frappe.get_abbr(item.item_name)} + >
`; } else { return `
@@ -118,7 +118,7 @@ erpnext.PointOfSale.ItemSelector = class { return ( `
@@ -128,7 +128,7 @@ erpnext.PointOfSale.ItemSelector = class {
${frappe.ellipsis(item.item_name, 18)}
-
${format_currency(price_list_rate, item.currency, precision) || 0}
+
${format_currency(price_list_rate, item.currency, precision) || 0} / ${uom}
` ); 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 eeb8523f19..be75bd64cf 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 @@ -94,7 +94,7 @@ erpnext.PointOfSale.PastOrderSummary = class { get_item_html(doc, item_data) { return `
${item_data.item_name}
-
${item_data.qty || 0}
+
${item_data.qty || 0} ${item_data.uom}
${get_rate_discount_html()}
`; @@ -130,7 +130,8 @@ erpnext.PointOfSale.PastOrderSummary = class { if (!doc.taxes.length) return ''; let taxes_html = doc.taxes.map(t => { - const description = /[0-9]+/.test(t.description) ? t.description : `${t.description} @ ${t.rate}%`; + // if tax rate is 0, don't print it. + const description = /[0-9]+/.test(t.description) ? t.description : ((t.rate != 0) ? `${t.description} @ ${t.rate}%`: t.description); return `
${description}
diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js index 0a356b9a6f..89ce61ab16 100644 --- a/erpnext/selling/page/point_of_sale/pos_payment.js +++ b/erpnext/selling/page/point_of_sale/pos_payment.js @@ -322,6 +322,11 @@ erpnext.PointOfSale.Payment = class { this.focus_on_default_mop(); } + after_render() { + const frm = this.events.get_frm(); + frm.script_manager.trigger("after_payment_render", frm.doc.doctype, frm.doc.docname); + } + edit_cart() { this.events.toggle_other_sections(false); this.toggle_component(false); @@ -332,6 +337,7 @@ erpnext.PointOfSale.Payment = class { this.toggle_component(true); this.render_payment_section(); + this.after_render(); } toggle_remarks_control() { diff --git a/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py b/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py index e10df2acbb..2624db3191 100644 --- a/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py +++ b/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py @@ -41,8 +41,20 @@ def get_columns(filters): {"label": _("Description"), "fieldtype": "Data", "fieldname": "description", "width": 150}, {"label": _("Quantity"), "fieldtype": "Float", "fieldname": "quantity", "width": 150}, {"label": _("UOM"), "fieldtype": "Link", "fieldname": "uom", "options": "UOM", "width": 100}, - {"label": _("Rate"), "fieldname": "rate", "options": "Currency", "width": 120}, - {"label": _("Amount"), "fieldname": "amount", "options": "Currency", "width": 120}, + { + "label": _("Rate"), + "fieldname": "rate", + "fieldtype": "Currency", + "options": "currency", + "width": 120, + }, + { + "label": _("Amount"), + "fieldname": "amount", + "fieldtype": "Currency", + "options": "currency", + "width": 120, + }, { "label": _("Sales Order"), "fieldtype": "Link", @@ -93,8 +105,9 @@ def get_columns(filters): }, { "label": _("Billed Amount"), - "fieldtype": "currency", + "fieldtype": "Currency", "fieldname": "billed_amount", + "options": "currency", "width": 120, }, { @@ -104,6 +117,13 @@ def get_columns(filters): "options": "Company", "width": 100, }, + { + "label": _("Currency"), + "fieldtype": "Link", + "fieldname": "currency", + "options": "Currency", + "hidden": 1, + }, ] @@ -141,31 +161,12 @@ def get_data(filters): "billed_amount": flt(record.get("billed_amt")), "company": record.get("company"), } + row["currency"] = frappe.get_cached_value("Company", row["company"], "default_currency") data.append(row) return data -def get_conditions(filters): - conditions = "" - if filters.get("item_group"): - conditions += "AND so_item.item_group = %s" % frappe.db.escape(filters.item_group) - - if filters.get("from_date"): - conditions += "AND so.transaction_date >= '%s'" % filters.from_date - - if filters.get("to_date"): - conditions += "AND so.transaction_date <= '%s'" % filters.to_date - - if filters.get("item_code"): - conditions += "AND so_item.item_code = %s" % frappe.db.escape(filters.item_code) - - if filters.get("customer"): - conditions += "AND so.customer = %s" % frappe.db.escape(filters.customer) - - return conditions - - def get_customer_details(): details = frappe.get_all("Customer", fields=["name", "customer_name", "customer_group"]) customer_details = {} @@ -187,29 +188,50 @@ def get_item_details(): def get_sales_order_details(company_list, filters): - conditions = get_conditions(filters) + db_so = frappe.qb.DocType("Sales Order") + db_so_item = frappe.qb.DocType("Sales Order Item") - return frappe.db.sql( - """ - SELECT - so_item.item_code, so_item.description, so_item.qty, - so_item.uom, so_item.base_rate, so_item.base_amount, - so.name, so.transaction_date, so.customer,so.territory, - so.project, so_item.delivered_qty, - so_item.billed_amt, so.company - FROM - `tabSales Order` so, `tabSales Order Item` so_item - WHERE - so.name = so_item.parent - AND so.company in ({0}) - AND so.docstatus = 1 {1} - """.format( - ",".join(["%s"] * len(company_list)), conditions - ), - tuple(company_list), - as_dict=1, + query = ( + frappe.qb.from_(db_so) + .inner_join(db_so_item) + .on(db_so_item.parent == db_so.name) + .select( + db_so.name, + db_so.customer, + db_so.transaction_date, + db_so.territory, + db_so.project, + db_so.company, + db_so_item.item_code, + db_so_item.description, + db_so_item.qty, + db_so_item.uom, + db_so_item.base_rate, + db_so_item.base_amount, + db_so_item.delivered_qty, + (db_so_item.billed_amt * db_so.conversion_rate).as_("billed_amt"), + ) + .where(db_so.docstatus == 1) + .where(db_so.company.isin(tuple(company_list))) ) + if filters.get("item_group"): + query = query.where(db_so_item.item_group == filters.item_group) + + if filters.get("from_date"): + query = query.where(db_so.transaction_date >= filters.from_date) + + if filters.get("to_date"): + query = query.where(db_so.transaction_date <= filters.to_date) + + if filters.get("item_code"): + query = query.where(db_so_item.item_code == filters.item_code) + + if filters.get("customer"): + query = query.where(db_so.customer == filters.customer) + + return query.run(as_dict=1) + def get_chart_data(data): item_wise_sales_map = {} diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js index c068ae3b5a..990d736baa 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js @@ -74,7 +74,40 @@ function get_filters() { ] } } - } + }, + { + "fieldname":"from_due_date", + "label": __("From Due Date"), + "fieldtype": "Date", + }, + { + "fieldname":"to_due_date", + "label": __("To Due Date"), + "fieldtype": "Date", + }, + { + "fieldname":"status", + "label": __("Status"), + "fieldtype": "MultiSelectList", + "width": 100, + get_data: function(txt) { + let status = ["Overdue", "Unpaid", "Completed", "Partly Paid"] + let options = [] + for (let option of status){ + options.push({ + "value": option, + "label": __(option), + "description": "" + }) + } + return options + } + }, + { + "fieldname":"only_immediate_upcoming_term", + "label": __("Show only the Immediate Upcoming Term"), + "fieldtype": "Check", + }, ] return filters; } diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py index 91f4a5e50a..3682c5fd62 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py @@ -4,6 +4,7 @@ import frappe from frappe import _, qb, query_builder from frappe.query_builder import Criterion, functions +from frappe.utils.dateutils import getdate def get_columns(): @@ -162,6 +163,12 @@ def build_filter_criterions(filters): if filters.item: qb_criterions.append(qb.DocType("Sales Order Item").item_code == filters.item) + if filters.from_due_date: + qb_criterions.append(qb.DocType("Payment Schedule").due_date.gte(filters.from_due_date)) + + if filters.to_due_date: + qb_criterions.append(qb.DocType("Payment Schedule").due_date.lte(filters.to_due_date)) + return qb_criterions @@ -202,6 +209,7 @@ def get_so_with_invoices(filters): ) .where( (so.docstatus == 1) + & (so.status.isin(["To Deliver and Bill", "To Bill"])) & (so.payment_terms_template != "NULL") & (so.company == conditions.company) & (so.transaction_date[conditions.start_date : conditions.end_date]) @@ -279,11 +287,33 @@ def prepare_chart(s_orders): return chart +def filter_on_calculated_status(filters, sales_orders): + if filters.status and sales_orders: + return [x for x in sales_orders if x.status in filters.status] + return sales_orders + + +def filter_for_immediate_upcoming_term(filters, sales_orders): + if filters.only_immediate_upcoming_term and sales_orders: + immediate_term_found = set() + filtered_data = [] + for order in sales_orders: + if order.name not in immediate_term_found and order.due_date > getdate(): + filtered_data.append(order) + immediate_term_found.add(order.name) + return filtered_data + return sales_orders + + def execute(filters=None): columns = get_columns() sales_orders, so_invoices = get_so_with_invoices(filters) sales_orders, so_invoices = set_payment_terms_statuses(sales_orders, so_invoices, filters) + sales_orders = filter_on_calculated_status(filters, sales_orders) + + sales_orders = filter_for_immediate_upcoming_term(filters, sales_orders) + prepare_chart(sales_orders) data = sales_orders diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py index 9d542f5079..525ae8e7ea 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py @@ -2,7 +2,7 @@ import datetime import frappe from frappe.tests.utils import FrappeTestCase -from frappe.utils import add_days +from frappe.utils import add_days, nowdate from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order @@ -77,12 +77,14 @@ class TestPaymentTermsStatusForSalesOrder(FrappeTestCase): sinv.insert() sinv.submit() columns, data, message, chart = execute( - { - "company": "_Test Company", - "period_start_date": "2021-06-01", - "period_end_date": "2021-06-30", - "item": item.item_code, - } + frappe._dict( + { + "company": "_Test Company", + "period_start_date": "2021-06-01", + "period_end_date": "2021-06-30", + "item": item.item_code, + } + ) ) expected_value = [ @@ -167,12 +169,14 @@ class TestPaymentTermsStatusForSalesOrder(FrappeTestCase): sinv.insert() sinv.submit() columns, data, message, chart = execute( - { - "company": "_Test Company", - "period_start_date": "2021-06-01", - "period_end_date": "2021-06-30", - "item": item.item_code, - } + frappe._dict( + { + "company": "_Test Company", + "period_start_date": "2021-06-01", + "period_end_date": "2021-06-30", + "item": item.item_code, + } + ) ) # report defaults to company currency. @@ -338,3 +342,60 @@ class TestPaymentTermsStatusForSalesOrder(FrappeTestCase): with self.subTest(filters=filters): columns, data, message, chart = execute(filters) self.assertEqual(data, expected_values_for_group_filters[idx]) + + def test_04_due_date_filter(self): + self.create_payment_terms_template() + item = create_item(item_code="_Test Excavator 1", is_stock_item=0) + transaction_date = nowdate() + so = make_sales_order( + transaction_date=add_days(transaction_date, -30), + delivery_date=add_days(transaction_date, -15), + item=item.item_code, + qty=10, + rate=100000, + do_not_save=True, + ) + so.po_no = "" + so.taxes_and_charges = "" + so.taxes = "" + so.payment_terms_template = self.template.name + so.save() + so.submit() + + # make invoice with 60% of the total sales order value + sinv = make_sales_invoice(so.name) + sinv.taxes_and_charges = "" + sinv.taxes = "" + sinv.items[0].qty = 6 + sinv.insert() + sinv.submit() + columns, data, message, chart = execute( + frappe._dict( + { + "company": "_Test Company", + "item": item.item_code, + "from_due_date": add_days(transaction_date, -30), + "to_due_date": add_days(transaction_date, -15), + } + ) + ) + + expected_value = [ + { + "name": so.name, + "customer": so.customer, + "submitted": datetime.date.fromisoformat(add_days(transaction_date, -30)), + "status": "Completed", + "payment_term": None, + "description": "_Test 50-50", + "due_date": datetime.date.fromisoformat(add_days(transaction_date, -15)), + "invoice_portion": 50.0, + "currency": "INR", + "base_payment_amount": 500000.0, + "paid_amount": 500000.0, + "invoices": "," + sinv.name, + }, + ] + # Only the first term should be pulled + self.assertEqual(len(data), 1) + self.assertEqual(data, expected_value) diff --git a/erpnext/selling/report/sales_analytics/sales_analytics.py b/erpnext/selling/report/sales_analytics/sales_analytics.py index 9d7d806c71..605d2fac44 100644 --- a/erpnext/selling/report/sales_analytics/sales_analytics.py +++ b/erpnext/selling/report/sales_analytics/sales_analytics.py @@ -168,7 +168,7 @@ class Analytics(object): def get_sales_transactions_based_on_items(self): if self.filters["value_quantity"] == "Value": - value_field = "base_amount" + value_field = "base_net_amount" else: value_field = "stock_qty" @@ -216,7 +216,7 @@ class Analytics(object): def get_sales_transactions_based_on_item_group(self): if self.filters["value_quantity"] == "Value": - value_field = "base_amount" + value_field = "base_net_amount" else: value_field = "qty" @@ -313,11 +313,13 @@ class Analytics(object): def get_period(self, posting_date): if self.filters.range == "Weekly": - period = "Week " + str(posting_date.isocalendar()[1]) + " " + str(posting_date.year) + period = _("Week {0} {1}").format(str(posting_date.isocalendar()[1]), str(posting_date.year)) elif self.filters.range == "Monthly": - period = str(self.months[posting_date.month - 1]) + " " + str(posting_date.year) + period = _(str(self.months[posting_date.month - 1])) + " " + str(posting_date.year) elif self.filters.range == "Quarterly": - period = "Quarter " + str(((posting_date.month - 1) // 3) + 1) + " " + str(posting_date.year) + period = _("Quarter {0} {1}").format( + str(((posting_date.month - 1) // 3) + 1), str(posting_date.year) + ) else: year = get_fiscal_year(posting_date, company=self.filters.company) period = str(year[0]) diff --git a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.js b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.js index 91748bc7be..f3f931edfd 100644 --- a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.js +++ b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.js @@ -44,6 +44,12 @@ frappe.query_reports["Sales Order Analysis"] = { } } }, + { + "fieldname": "warehouse", + "label": __("Warehouse"), + "fieldtype": "Link", + "options": "Warehouse" + }, { "fieldname": "status", "label": __("Status"), 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 720aa41982..29691230f2 100644 --- a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py +++ b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py @@ -53,6 +53,9 @@ def get_conditions(filters): if filters.get("status"): conditions += " and so.status in %(status)s" + if filters.get("warehouse"): + conditions += " and soi.warehouse = %(warehouse)s" + return conditions @@ -172,7 +175,9 @@ def prepare_data(data, so_elapsed_time, filters): # update existing entry so_row = sales_order_map[so_name] so_row["required_date"] = max(getdate(so_row["delivery_date"]), getdate(row["delivery_date"])) - so_row["delay"] = min(so_row["delay"], row["delay"]) + so_row["delay"] = ( + min(so_row["delay"], row["delay"]) if row["delay"] and so_row["delay"] else so_row["delay"] + ) # sum numeric columns fields = [ diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js index 8ff01f5cb4..5ce6e9c146 100644 --- a/erpnext/selling/sales_common.js +++ b/erpnext/selling/sales_common.js @@ -418,8 +418,6 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran callback: function(r) { if(r.message) { frappe.model.set_value(doc.doctype, doc.name, 'batch_no', r.message); - } else { - frappe.model.set_value(doc.doctype, doc.name, 'batch_no', r.message); } } }); diff --git a/erpnext/setup/doctype/brand/brand.js b/erpnext/setup/doctype/brand/brand.js index 3680906057..0abb71a362 100644 --- a/erpnext/setup/doctype/brand/brand.js +++ b/erpnext/setup/doctype/brand/brand.js @@ -1,13 +1,71 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt +frappe.ui.form.on('Brand', { + setup: (frm) => { + frm.fields_dict["brand_defaults"].grid.get_field("default_warehouse").get_query = function(doc, cdt, cdn) { + const row = locals[cdt][cdn]; + return { + filters: { company: row.company } + } + } + frm.fields_dict["brand_defaults"].grid.get_field("default_discount_account").get_query = function(doc, cdt, cdn) { + const row = locals[cdt][cdn]; + return { + filters: { + 'report_type': 'Profit and Loss', + 'company': row.company, + "is_group": 0 + } + }; + } -//--------- ONLOAD ------------- -cur_frm.cscript.onload = function(doc, cdt, cdn) { + frm.fields_dict["brand_defaults"].grid.get_field("buying_cost_center").get_query = function(doc, cdt, cdn) { + const row = locals[cdt][cdn]; + return { + filters: { + "is_group": 0, + "company": row.company + } + } + } -} + frm.fields_dict["brand_defaults"].grid.get_field("expense_account").get_query = function(doc, cdt, cdn) { + const row = locals[cdt][cdn]; + return { + query: "erpnext.controllers.queries.get_expense_account", + filters: { company: row.company } + } + } -cur_frm.cscript.refresh = function(doc, cdt, cdn) { + frm.fields_dict["brand_defaults"].grid.get_field("default_provisional_account").get_query = function(doc, cdt, cdn) { + const row = locals[cdt][cdn]; + return { + filters: { + "company": row.company, + "root_type": ["in", ["Liability", "Asset"]], + "is_group": 0 + } + }; + } -} + frm.fields_dict["brand_defaults"].grid.get_field("selling_cost_center").get_query = function(doc, cdt, cdn) { + const row = locals[cdt][cdn]; + return { + filters: { + "is_group": 0, + "company": row.company + } + } + } + + frm.fields_dict["brand_defaults"].grid.get_field("income_account").get_query = function(doc, cdt, cdn) { + const row = locals[cdt][cdn]; + return { + query: "erpnext.controllers.queries.get_income_account", + filters: { company: row.company } + } + } + } +}); \ No newline at end of file diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json index f34ec56dc0..f087d996ff 100644 --- a/erpnext/setup/doctype/company/company.json +++ b/erpnext/setup/doctype/company/company.json @@ -85,7 +85,6 @@ "depreciation_expense_account", "series_for_depreciation_entry", "expenses_included_in_asset_valuation", - "repair_and_maintenance_account", "column_break_40", "disposal_account", "depreciation_cost_center", @@ -234,7 +233,6 @@ "label": "Default Warehouse for Sales Return", "options": "Warehouse" }, - { "fieldname": "country", "fieldtype": "Link", @@ -678,12 +676,6 @@ "fieldtype": "Section Break", "label": "Fixed Asset Defaults" }, - { - "fieldname": "repair_and_maintenance_account", - "fieldtype": "Link", - "label": "Repair and Maintenance Account", - "options": "Account" - }, { "fieldname": "section_break_28", "fieldtype": "Section Break", @@ -709,7 +701,7 @@ "image_field": "company_logo", "is_tree": 1, "links": [], - "modified": "2022-06-30 18:03:18.701314", + "modified": "2022-08-16 16:09:02.327724", "modified_by": "Administrator", "module": "Setup", "name": "Company", diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index dc698886a0..07ee2890c4 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -70,9 +70,6 @@ class Company(NestedSet): self.abbr = self.abbr.strip() - # if self.get('__islocal') and len(self.abbr) > 5: - # frappe.throw(_("Abbreviation cannot have more than 5 characters")) - if not self.abbr.strip(): frappe.throw(_("Abbreviation is mandatory")) @@ -207,15 +204,14 @@ class Company(NestedSet): frappe.local.flags.ignore_root_company_validation = True create_charts(self.name, self.chart_of_accounts, self.existing_company) - frappe.db.set( - self, + self.db_set( "default_receivable_account", frappe.db.get_value( "Account", {"company": self.name, "account_type": "Receivable", "is_group": 0} ), ) - frappe.db.set( - self, + + self.db_set( "default_payable_account", frappe.db.get_value( "Account", {"company": self.name, "account_type": "Payable", "is_group": 0} @@ -389,6 +385,7 @@ class Company(NestedSet): "capital_work_in_progress_account": "Capital Work in Progress", "asset_received_but_not_billed": "Asset Received But Not Billed", "expenses_included_in_asset_valuation": "Expenses Included In Asset Valuation", + "default_expense_account": "Cost of Goods Sold", } if self.enable_perpetual_inventory: @@ -398,7 +395,6 @@ class Company(NestedSet): "default_inventory_account": "Stock", "stock_adjustment_account": "Stock Adjustment", "expenses_included_in_valuation": "Expenses Included In Valuation", - "default_expense_account": "Cost of Goods Sold", } ) @@ -491,12 +487,12 @@ class Company(NestedSet): cc_doc.flags.ignore_mandatory = True cc_doc.insert() - frappe.db.set(self, "cost_center", _("Main") + " - " + self.abbr) - frappe.db.set(self, "round_off_cost_center", _("Main") + " - " + self.abbr) - frappe.db.set(self, "depreciation_cost_center", _("Main") + " - " + self.abbr) + self.db_set("cost_center", _("Main") + " - " + self.abbr) + self.db_set("round_off_cost_center", _("Main") + " - " + self.abbr) + self.db_set("depreciation_cost_center", _("Main") + " - " + self.abbr) def after_rename(self, olddn, newdn, merge=False): - frappe.db.set(self, "company_name", newdn) + self.db_set("company_name", newdn) frappe.db.sql( """update `tabDefaultValue` set defvalue=%s @@ -690,11 +686,11 @@ def get_children(doctype, parent=None, company=None, is_root=False): name as value, is_group as expandable from - `tab{doctype}` comp + `tabCompany` comp where ifnull(parent_company, "")={parent} """.format( - doctype=doctype, parent=frappe.db.escape(parent) + parent=frappe.db.escape(parent) ), as_dict=1, ) diff --git a/erpnext/setup/doctype/customer_group/customer_group.json b/erpnext/setup/doctype/customer_group/customer_group.json index 0e2ed9efcf..d6a431ea61 100644 --- a/erpnext/setup/doctype/customer_group/customer_group.json +++ b/erpnext/setup/doctype/customer_group/customer_group.json @@ -139,10 +139,11 @@ "idx": 1, "is_tree": 1, "links": [], - "modified": "2021-02-08 17:01:52.162202", + "modified": "2022-12-24 11:15:17.142746", "modified_by": "Administrator", "module": "Setup", "name": "Customer Group", + "naming_rule": "By fieldname", "nsm_parent_field": "parent_customer_group", "owner": "Administrator", "permissions": [ @@ -198,10 +199,19 @@ "role": "Customer", "select": 1, "share": 1 + }, + { + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts User", + "share": 1 } ], "search_fields": "parent_customer_group", "show_name_in_global_search": 1, "sort_field": "modified", - "sort_order": "DESC" + "sort_order": "DESC", + "states": [] } \ No newline at end of file diff --git a/erpnext/setup/doctype/department/department.py b/erpnext/setup/doctype/department/department.py index c4766ee6f8..1745178574 100644 --- a/erpnext/setup/doctype/department/department.py +++ b/erpnext/setup/doctype/department/department.py @@ -63,7 +63,7 @@ def get_children(doctype, parent=None, company=None, is_root=False): else: filters["parent_department"] = parent - return frappe.get_all(doctype, fields=fields, filters=filters, order_by="name") + return frappe.get_all("Department", fields=fields, filters=filters, order_by="name") @frappe.whitelist() diff --git a/erpnext/setup/doctype/designation/designation.json b/erpnext/setup/doctype/designation/designation.json index 2cbbb04ed9..a5b2ac9128 100644 --- a/erpnext/setup/doctype/designation/designation.json +++ b/erpnext/setup/doctype/designation/designation.json @@ -31,7 +31,7 @@ "icon": "fa fa-bookmark", "idx": 1, "links": [], - "modified": "2022-06-28 17:10:26.853753", + "modified": "2023-02-10 01:53:41.319386", "modified_by": "Administrator", "module": "Setup", "name": "Designation", @@ -58,5 +58,6 @@ "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "ASC", - "states": [] + "states": [], + "translated_doctype": 1 } \ No newline at end of file diff --git a/erpnext/setup/doctype/employee/employee.json b/erpnext/setup/doctype/employee/employee.json index 7a806d5906..99693d9091 100644 --- a/erpnext/setup/doctype/employee/employee.json +++ b/erpnext/setup/doctype/employee/employee.json @@ -10,79 +10,89 @@ "editable_grid": 1, "engine": "InnoDB", "field_order": [ + "basic_details_tab", "basic_information", "employee", "naming_series", "first_name", "middle_name", "last_name", - "salutation", "employee_name", - "image", - "column_break1", - "company", - "status", + "column_break_9", "gender", "date_of_birth", + "salutation", + "column_break1", "date_of_joining", - "employee_number", - "emergency_contact_details", - "person_to_be_contacted", - "relation", - "column_break_19", - "emergency_phone_number", + "image", + "status", "erpnext_user", "user_id", "create_user", "create_user_permission", - "employment_details", - "scheduled_confirmation_date", - "final_confirmation_date", - "col_break_22", - "contract_end_date", - "notice_number_of_days", - "date_of_retirement", - "job_profile", + "company_details_section", + "company", "department", + "employee_number", + "column_break_25", "designation", "reports_to", - "column_break_31", + "column_break_18", "branch", + "employment_details", + "scheduled_confirmation_date", + "column_break_32", + "final_confirmation_date", + "contract_end_date", + "col_break_22", + "notice_number_of_days", + "date_of_retirement", + "contact_details", + "cell_number", + "column_break_40", + "personal_email", + "company_email", + "column_break4", + "prefered_contact_email", + "prefered_email", + "unsubscribed", + "address_section", + "current_address", + "current_accommodation_type", + "column_break_46", + "permanent_address", + "permanent_accommodation_type", + "emergency_contact_details", + "person_to_be_contacted", + "column_break_55", + "emergency_phone_number", + "column_break_19", + "relation", "attendance_and_leave_details", "attendance_device_id", "column_break_44", "holiday_list", "salary_information", - "salary_currency", "ctc", - "payroll_cost_center", - "column_break_52", + "salary_currency", + "salary_mode", + "bank_details_section", "bank_name", "bank_ac_no", - "contact_details", - "cell_number", - "prefered_email", - "personal_email", - "unsubscribed", - "permanent_accommodation_type", - "permanent_address", - "column_break4", - "prefered_contact_email", - "company_email", - "current_accommodation_type", - "current_address", - "sb53", - "bio", "personal_details", - "passport_number", - "date_of_issue", - "valid_upto", - "place_of_issue", "marital_status", - "blood_group", - "column_break6", "family_background", + "column_break6", + "blood_group", "health_details", + "passport_details_section", + "passport_number", + "valid_upto", + "column_break_73", + "date_of_issue", + "place_of_issue", + "profile_tab", + "bio", "educational_qualification", "education", "previous_work_experience", @@ -92,16 +102,20 @@ "exit", "resignation_letter_date", "relieving_date", - "reason_for_leaving", - "leave_encashed", - "encashment_date", "exit_interview_details", "held_on", "new_workplace", + "column_break_99", + "leave_encashed", + "encashment_date", + "feedback_section", + "reason_for_leaving", + "column_break_104", "feedback", "lft", "rgt", - "old_parent" + "old_parent", + "connections_tab" ], "fields": [ { @@ -261,7 +275,7 @@ "collapsible": 1, "fieldname": "erpnext_user", "fieldtype": "Section Break", - "label": "ERPNext User" + "label": "User Details" }, { "description": "System User (login) ID. If set, it will become default for all HR forms.", @@ -289,8 +303,8 @@ "allow_in_quick_entry": 1, "collapsible": 1, "fieldname": "employment_details", - "fieldtype": "Section Break", - "label": "Joining Details" + "fieldtype": "Tab Break", + "label": "Joining" }, { "fieldname": "scheduled_confirmation_date", @@ -331,12 +345,6 @@ "oldfieldname": "date_of_retirement", "oldfieldtype": "Date" }, - { - "collapsible": 1, - "fieldname": "job_profile", - "fieldtype": "Section Break", - "label": "Department" - }, { "fieldname": "department", "fieldtype": "Link", @@ -366,10 +374,6 @@ "oldfieldtype": "Link", "options": "Employee" }, - { - "fieldname": "column_break_31", - "fieldtype": "Column Break" - }, { "fieldname": "branch", "fieldtype": "Link", @@ -391,8 +395,8 @@ { "collapsible": 1, "fieldname": "salary_information", - "fieldtype": "Section Break", - "label": "Salary Details", + "fieldtype": "Tab Break", + "label": "Salary", "oldfieldtype": "Section Break", "width": "50%" }, @@ -423,8 +427,8 @@ { "collapsible": 1, "fieldname": "contact_details", - "fieldtype": "Section Break", - "label": "Contact Details" + "fieldtype": "Tab Break", + "label": "Address & Contacts" }, { "fieldname": "cell_number", @@ -493,12 +497,6 @@ "fieldtype": "Small Text", "label": "Current Address" }, - { - "collapsible": 1, - "fieldname": "sb53", - "fieldtype": "Section Break", - "label": "Personal Bio" - }, { "description": "Short biography for website and other publications.", "fieldname": "bio", @@ -508,8 +506,8 @@ { "collapsible": 1, "fieldname": "personal_details", - "fieldtype": "Section Break", - "label": "Personal Details" + "fieldtype": "Tab Break", + "label": "Personal" }, { "fieldname": "passport_number", @@ -601,7 +599,7 @@ { "collapsible": 1, "fieldname": "exit", - "fieldtype": "Section Break", + "fieldtype": "Tab Break", "label": "Exit", "oldfieldtype": "Section Break" }, @@ -702,8 +700,8 @@ { "collapsible": 1, "fieldname": "attendance_and_leave_details", - "fieldtype": "Section Break", - "label": "Attendance and Leave Details" + "fieldtype": "Tab Break", + "label": "Attendance & Leaves" }, { "fieldname": "column_break_44", @@ -713,10 +711,6 @@ "fieldname": "column_break_19", "fieldtype": "Column Break" }, - { - "fieldname": "column_break_52", - "fieldtype": "Column Break" - }, { "fieldname": "salary_currency", "fieldtype": "Link", @@ -728,13 +722,95 @@ "fieldtype": "Currency", "label": "Cost to Company (CTC)", "options": "salary_currency" + }, + { + "fieldname": "basic_details_tab", + "fieldtype": "Tab Break", + "label": "Overview" + }, + { + "fieldname": "company_details_section", + "fieldtype": "Section Break", + "label": "Company Details" + }, + { + "fieldname": "column_break_18", + "fieldtype": "Column Break" + }, + { + "collapsible": 1, + "fieldname": "address_section", + "fieldtype": "Section Break", + "label": "Address" + }, + { + "fieldname": "column_break_46", + "fieldtype": "Column Break" + }, + { + "fieldname": "profile_tab", + "fieldtype": "Tab Break", + "label": "Profile" + }, + { + "fieldname": "passport_details_section", + "fieldtype": "Section Break", + "label": "Passport Details" + }, + { + "fieldname": "column_break_73", + "fieldtype": "Column Break" + }, + { + "fieldname": "bank_details_section", + "fieldtype": "Section Break", + "label": "Bank Details" + }, + { + "fieldname": "column_break_9", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_25", + "fieldtype": "Column Break" + }, + { + "fieldname": "connections_tab", + "fieldtype": "Tab Break", + "label": "Connections", + "show_dashboard": 1 + }, + { + "fieldname": "column_break_32", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_40", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_55", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_99", + "fieldtype": "Column Break" + }, + { + "fieldname": "feedback_section", + "fieldtype": "Section Break", + "label": "Feedback" + }, + { + "fieldname": "column_break_104", + "fieldtype": "Column Break" } ], "icon": "fa fa-user", "idx": 24, "image_field": "image", "links": [], - "modified": "2022-06-27 01:29:32.952091", + "modified": "2022-09-13 10:27:14.579197", "modified_by": "Administrator", "module": "Setup", "name": "Employee", diff --git a/erpnext/setup/doctype/employee/employee.py b/erpnext/setup/doctype/employee/employee.py index 13a6f20db2..ece5a7d554 100755 --- a/erpnext/setup/doctype/employee/employee.py +++ b/erpnext/setup/doctype/employee/employee.py @@ -8,7 +8,6 @@ from frappe.permissions import ( get_doc_permissions, has_permission, remove_user_permission, - set_user_permission_if_allowed, ) from frappe.utils import cstr, getdate, today, validate_email_address from frappe.utils.nestedset import NestedSet @@ -96,7 +95,7 @@ class Employee(NestedSet): return add_user_permission("Employee", self.name, self.user_id) - set_user_permission_if_allowed("Company", self.company, self.user_id) + add_user_permission("Company", self.company, self.user_id) def update_user(self): # add employee role if missing @@ -145,33 +144,10 @@ class Employee(NestedSet): if self.date_of_birth and getdate(self.date_of_birth) > getdate(today()): throw(_("Date of Birth cannot be greater than today.")) - if ( - self.date_of_birth - and self.date_of_joining - and getdate(self.date_of_birth) >= getdate(self.date_of_joining) - ): - throw(_("Date of Joining must be greater than Date of Birth")) - - elif ( - self.date_of_retirement - and self.date_of_joining - and (getdate(self.date_of_retirement) <= getdate(self.date_of_joining)) - ): - throw(_("Date Of Retirement must be greater than Date of Joining")) - - elif ( - self.relieving_date - and self.date_of_joining - and (getdate(self.relieving_date) < getdate(self.date_of_joining)) - ): - throw(_("Relieving Date must be greater than or equal to Date of Joining")) - - elif ( - self.contract_end_date - and self.date_of_joining - and (getdate(self.contract_end_date) <= getdate(self.date_of_joining)) - ): - throw(_("Contract End Date must be greater than Date of Joining")) + self.validate_from_to_dates("date_of_birth", "date_of_joining") + self.validate_from_to_dates("date_of_joining", "date_of_retirement") + self.validate_from_to_dates("date_of_joining", "relieving_date") + self.validate_from_to_dates("date_of_joining", "contract_end_date") def validate_email(self): if self.company_email: diff --git a/erpnext/regional/saudi_arabia/wizard/__init__.py b/erpnext/setup/doctype/incoterm/__init__.py similarity index 100% rename from erpnext/regional/saudi_arabia/wizard/__init__.py rename to erpnext/setup/doctype/incoterm/__init__.py diff --git a/erpnext/setup/doctype/incoterm/incoterm.js b/erpnext/setup/doctype/incoterm/incoterm.js new file mode 100644 index 0000000000..bc65123dc1 --- /dev/null +++ b/erpnext/setup/doctype/incoterm/incoterm.js @@ -0,0 +1,8 @@ +// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("Incoterm", { +// refresh(frm) { + +// }, +// }); diff --git a/erpnext/setup/doctype/incoterm/incoterm.json b/erpnext/setup/doctype/incoterm/incoterm.json new file mode 100644 index 0000000000..c547b7c4ff --- /dev/null +++ b/erpnext/setup/doctype/incoterm/incoterm.json @@ -0,0 +1,168 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "field:code", + "creation": "2022-11-17 15:17:34.717467", + "default_view": "List", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "code", + "title", + "description" + ], + "fields": [ + { + "fieldname": "code", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Code", + "length": 3, + "reqd": 1, + "unique": 1 + }, + { + "fieldname": "title", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Title", + "reqd": 1 + }, + { + "fieldname": "description", + "fieldtype": "Long Text", + "label": "Description" + } + ], + "links": [ + { + "group": "Selling", + "link_doctype": "Quotation", + "link_fieldname": "incoterm" + }, + { + "group": "Selling", + "link_doctype": "Sales Order", + "link_fieldname": "incoterm" + }, + { + "group": "Buying", + "link_doctype": "Request for Quotation", + "link_fieldname": "incoterm" + }, + { + "group": "Buying", + "link_doctype": "Supplier Quotation", + "link_fieldname": "incoterm" + }, + { + "group": "Buying", + "link_doctype": "Purchase Order", + "link_fieldname": "incoterm" + }, + { + "group": "Stock", + "link_doctype": "Delivery Note", + "link_fieldname": "incoterm" + }, + { + "group": "Stock", + "link_doctype": "Purchase Receipt", + "link_fieldname": "incoterm" + }, + { + "group": "Stock", + "link_doctype": "Shipment", + "link_fieldname": "incoterm" + }, + { + "group": "Accounts", + "link_doctype": "Sales Invoice", + "link_fieldname": "incoterm" + }, + { + "group": "Accounts", + "link_doctype": "Purchase Invoice", + "link_fieldname": "incoterm" + } + ], + "modified": "2022-11-17 22:35:52.084553", + "modified_by": "Administrator", + "module": "Setup", + "name": "Incoterm", + "naming_rule": "By fieldname", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Sales Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Purchase Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Stock Manager", + "share": 1, + "write": 1 + }, + { + "read": 1, + "role": "Purchase User" + }, + { + "read": 1, + "role": "Sales User" + }, + { + "read": 1, + "role": "Accounts User" + }, + { + "read": 1, + "role": "Stock User" + } + ], + "show_title_field_in_link": 1, + "sort_field": "name", + "sort_order": "ASC", + "states": [], + "title_field": "title", + "translated_doctype": 1 +} \ No newline at end of file diff --git a/erpnext/setup/doctype/incoterm/incoterm.py b/erpnext/setup/doctype/incoterm/incoterm.py new file mode 100644 index 0000000000..7e2e622c24 --- /dev/null +++ b/erpnext/setup/doctype/incoterm/incoterm.py @@ -0,0 +1,24 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe.model.document import Document + + +class Incoterm(Document): + pass + + +def create_incoterms(): + """Create Incoterm records from incoterms.csv.""" + import os + from csv import DictReader + + with open(os.path.join(os.path.dirname(__file__), "incoterms.csv"), "r") as f: + for incoterm in DictReader(f): + if frappe.db.exists("Incoterm", incoterm["code"]): + continue + + doc = frappe.new_doc("Incoterm") + doc.update(incoterm) + doc.save() diff --git a/erpnext/setup/doctype/incoterm/incoterms.csv b/erpnext/setup/doctype/incoterm/incoterms.csv new file mode 100644 index 0000000000..af532cff16 --- /dev/null +++ b/erpnext/setup/doctype/incoterm/incoterms.csv @@ -0,0 +1,12 @@ +code,title +EXW,Ex Works +FCA,Free Carrier +FAS,Free Alongside Ship +FOB,Free On Board +CPT,Carriage Paid To +CIP,Carriage and Insurance Paid to +CFR,Cost and Freight +CIF,"Cost, Insurance and Freight" +DAP,Delivered At Place +DPU,Delivered At Place Unloaded +DDP,Delivered Duty Paid diff --git a/erpnext/setup/doctype/incoterm/test_incoterm.py b/erpnext/setup/doctype/incoterm/test_incoterm.py new file mode 100644 index 0000000000..06b8c3bda9 --- /dev/null +++ b/erpnext/setup/doctype/incoterm/test_incoterm.py @@ -0,0 +1,9 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestIncoterm(FrappeTestCase): + pass diff --git a/erpnext/setup/doctype/item_group/item_group.json b/erpnext/setup/doctype/item_group/item_group.json index 50f923d87e..2986087277 100644 --- a/erpnext/setup/doctype/item_group/item_group.json +++ b/erpnext/setup/doctype/item_group/item_group.json @@ -123,6 +123,7 @@ "fieldname": "route", "fieldtype": "Data", "label": "Route", + "no_copy": 1, "unique": 1 }, { @@ -232,11 +233,10 @@ "is_tree": 1, "links": [], "max_attachments": 3, - "modified": "2022-03-09 12:27:11.055782", + "modified": "2023-01-05 12:21:30.458628", "modified_by": "Administrator", "module": "Setup", "name": "Item Group", - "name_case": "Title Case", "naming_rule": "By fieldname", "nsm_parent_field": "parent_item_group", "owner": "Administrator", diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py index 6e940d0cfd..2fdfcf647d 100644 --- a/erpnext/setup/doctype/item_group/item_group.py +++ b/erpnext/setup/doctype/item_group/item_group.py @@ -142,22 +142,18 @@ def get_item_for_list_in_html(context): if (context.get("website_image") or "").startswith("files/"): context["website_image"] = "/" + quote(context["website_image"]) - context["show_availability_status"] = cint( - frappe.db.get_single_value("E Commerce Settings", "show_availability_status") - ) - products_template = "templates/includes/products_as_list.html" return frappe.get_template(products_template).render(context) def get_parent_item_groups(item_group_name, from_item=False): - base_nav_page = {"name": _("Shop by Category"), "route": "/shop-by-category"} + base_nav_page = {"name": _("All Products"), "route": "/all-products"} if from_item and frappe.request.environ.get("HTTP_REFERER"): # base page after 'Home' will vary on Item page - last_page = frappe.request.environ["HTTP_REFERER"].split("/")[-1] - if last_page and last_page in ("shop-by-category", "all-products"): + last_page = frappe.request.environ["HTTP_REFERER"].split("/")[-1].split("?")[0] + if last_page and last_page == "shop-by-category": base_nav_page_title = " ".join(last_page.split("-")).title() base_nav_page = {"name": _(base_nav_page_title), "route": "/" + last_page} diff --git a/erpnext/setup/doctype/sales_person/sales_person.py b/erpnext/setup/doctype/sales_person/sales_person.py index 0082c70075..beff7f5374 100644 --- a/erpnext/setup/doctype/sales_person/sales_person.py +++ b/erpnext/setup/doctype/sales_person/sales_person.py @@ -2,8 +2,13 @@ # License: GNU General Public License v3. See license.txt +from collections import defaultdict +from itertools import chain + import frappe from frappe import _ +from frappe.query_builder import Interval +from frappe.query_builder.functions import Count, CurDate, UnixTimestamp from frappe.utils import flt from frappe.utils.nestedset import NestedSet, get_root_of @@ -77,61 +82,31 @@ def on_doctype_update(): frappe.db.add_index("Sales Person", ["lft", "rgt"]) -def get_timeline_data(doctype, name): +def get_timeline_data(doctype: str, name: str) -> dict[int, int]: + def _fetch_activity(doctype: str, date_field: str): + sales_team = frappe.qb.DocType("Sales Team") + transaction = frappe.qb.DocType(doctype) - out = {} - - out.update( - dict( - frappe.db.sql( - """select - unix_timestamp(dt.transaction_date), count(st.parenttype) - from - `tabSales Order` dt, `tabSales Team` st - where - st.sales_person = %s and st.parent = dt.name and dt.transaction_date > date_sub(curdate(), interval 1 year) - group by dt.transaction_date """, - name, - ) + return dict( + frappe.qb.from_(transaction) + .join(sales_team) + .on(transaction.name == sales_team.parent) + .select(UnixTimestamp(transaction[date_field]), Count("*")) + .where(sales_team.sales_person == name) + .where(transaction[date_field] > CurDate() - Interval(years=1)) + .groupby(transaction[date_field]) + .run() ) - ) - sales_invoice = dict( - frappe.db.sql( - """select - unix_timestamp(dt.posting_date), count(st.parenttype) - from - `tabSales Invoice` dt, `tabSales Team` st - where - st.sales_person = %s and st.parent = dt.name and dt.posting_date > date_sub(curdate(), interval 1 year) - group by dt.posting_date """, - name, - ) - ) + sales_order_activity = _fetch_activity("Sales Order", "transaction_date") + sales_invoice_activity = _fetch_activity("Sales Invoice", "posting_date") + delivery_note_activity = _fetch_activity("Delivery Note", "posting_date") - for key in sales_invoice: - if out.get(key): - out[key] += sales_invoice[key] - else: - out[key] = sales_invoice[key] + merged_activities = defaultdict(int) - delivery_note = dict( - frappe.db.sql( - """select - unix_timestamp(dt.posting_date), count(st.parenttype) - from - `tabDelivery Note` dt, `tabSales Team` st - where - st.sales_person = %s and st.parent = dt.name and dt.posting_date > date_sub(curdate(), interval 1 year) - group by dt.posting_date """, - name, - ) - ) + for ts, count in chain( + sales_order_activity.items(), sales_invoice_activity.items(), delivery_note_activity.items() + ): + merged_activities[ts] += count - for key in delivery_note: - if out.get(key): - out[key] += delivery_note[key] - else: - out[key] = delivery_note[key] - - return out + return merged_activities diff --git a/erpnext/setup/doctype/supplier_group/supplier_group.json b/erpnext/setup/doctype/supplier_group/supplier_group.json index 9119bb947c..b3ed608cd0 100644 --- a/erpnext/setup/doctype/supplier_group/supplier_group.json +++ b/erpnext/setup/doctype/supplier_group/supplier_group.json @@ -6,6 +6,7 @@ "creation": "2013-01-10 16:34:24", "doctype": "DocType", "document_type": "Setup", + "engine": "InnoDB", "field_order": [ "supplier_group_name", "parent_supplier_group", @@ -106,10 +107,11 @@ "idx": 1, "is_tree": 1, "links": [], - "modified": "2020-03-18 18:10:49.228407", + "modified": "2022-12-24 11:16:12.486719", "modified_by": "Administrator", "module": "Setup", "name": "Supplier Group", + "naming_rule": "By fieldname", "nsm_parent_field": "parent_supplier_group", "owner": "Administrator", "permissions": [ @@ -156,8 +158,18 @@ "permlevel": 1, "read": 1, "role": "Purchase User" + }, + { + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts User", + "share": 1 } ], "show_name_in_global_search": 1, - "sort_order": "ASC" + "sort_field": "modified", + "sort_order": "ASC", + "states": [] } \ No newline at end of file diff --git a/erpnext/setup/doctype/terms_and_conditions/terms_and_conditions.js b/erpnext/setup/doctype/terms_and_conditions/terms_and_conditions.js index 3680906057..c3605bf0e8 100644 --- a/erpnext/setup/doctype/terms_and_conditions/terms_and_conditions.js +++ b/erpnext/setup/doctype/terms_and_conditions/terms_and_conditions.js @@ -1,13 +1,6 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt - - -//--------- ONLOAD ------------- -cur_frm.cscript.onload = function(doc, cdt, cdn) { - -} - -cur_frm.cscript.refresh = function(doc, cdt, cdn) { - -} +// frappe.ui.form.on("Terms and Conditions", { +// refresh(frm) {} +// }); diff --git a/erpnext/setup/doctype/terms_and_conditions/terms_and_conditions.json b/erpnext/setup/doctype/terms_and_conditions/terms_and_conditions.json index f14b243512..f884864acf 100644 --- a/erpnext/setup/doctype/terms_and_conditions/terms_and_conditions.json +++ b/erpnext/setup/doctype/terms_and_conditions/terms_and_conditions.json @@ -33,7 +33,6 @@ "default": "0", "fieldname": "disabled", "fieldtype": "Check", - "in_list_view": 1, "label": "Disabled" }, { @@ -60,12 +59,14 @@ "default": "1", "fieldname": "selling", "fieldtype": "Check", + "in_list_view": 1, "label": "Selling" }, { "default": "1", "fieldname": "buying", "fieldtype": "Check", + "in_list_view": 1, "label": "Buying" }, { @@ -76,10 +77,11 @@ "icon": "icon-legal", "idx": 1, "links": [], - "modified": "2022-06-16 15:07:38.094844", + "modified": "2023-02-01 14:33:39.246532", "modified_by": "Administrator", "module": "Setup", "name": "Terms and Conditions", + "naming_rule": "By fieldname", "owner": "Administrator", "permissions": [ { @@ -133,5 +135,6 @@ "quick_entry": 1, "show_name_in_global_search": 1, "sort_field": "modified", - "sort_order": "ASC" + "sort_order": "ASC", + "states": [] } \ No newline at end of file diff --git a/erpnext/setup/doctype/territory/territory.json b/erpnext/setup/doctype/territory/territory.json index a25bda054b..c3a4993374 100644 --- a/erpnext/setup/doctype/territory/territory.json +++ b/erpnext/setup/doctype/territory/territory.json @@ -123,11 +123,12 @@ "idx": 1, "is_tree": 1, "links": [], - "modified": "2021-02-08 17:10:03.767426", + "modified": "2022-12-24 11:16:39.964956", "modified_by": "Administrator", "module": "Setup", "name": "Territory", "name_case": "Title Case", + "naming_rule": "By fieldname", "nsm_parent_field": "parent_territory", "owner": "Administrator", "permissions": [ @@ -175,10 +176,19 @@ "role": "Customer", "select": 1, "share": 1 + }, + { + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts User", + "share": 1 } ], "search_fields": "parent_territory,territory_manager", "show_name_in_global_search": 1, "sort_field": "modified", - "sort_order": "DESC" + "sort_order": "DESC", + "states": [] } \ No newline at end of file diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py index c18a4b2214..481a3a5ebe 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py @@ -3,13 +3,17 @@ import frappe -from frappe import _ +from frappe import _, qb from frappe.desk.notifications import clear_notifications from frappe.model.document import Document -from frappe.utils import cint +from frappe.utils import cint, create_batch class TransactionDeletionRecord(Document): + def __init__(self, *args, **kwargs): + super(TransactionDeletionRecord, self).__init__(*args, **kwargs) + self.batch_size = 5000 + def validate(self): frappe.only_for("System Manager") self.validate_doctypes_to_be_ignored() @@ -155,8 +159,9 @@ class TransactionDeletionRecord(Document): "DocField", filters={"fieldtype": "Table", "parent": doctype}, pluck="options" ) - for table in child_tables: - frappe.db.delete(table, {"parent": ["in", parent_docs_to_be_deleted]}) + for batch in create_batch(parent_docs_to_be_deleted, self.batch_size): + for table in child_tables: + frappe.db.delete(table, {"parent": ["in", batch]}) def delete_docs_linked_with_specified_company(self, doctype, company_fieldname): frappe.db.delete(doctype, {company_fieldname: self.company}) @@ -181,13 +186,16 @@ class TransactionDeletionRecord(Document): frappe.db.sql("""update `tabSeries` set current = %s where name=%s""", (last, prefix)) def delete_version_log(self, doctype, company_fieldname): - frappe.db.sql( - """delete from `tabVersion` where ref_doctype=%s and docname in - (select name from `tab{0}` where `{1}`=%s)""".format( - doctype, company_fieldname - ), - (doctype, self.company), - ) + dt = qb.DocType(doctype) + names = qb.from_(dt).select(dt.name).where(dt[company_fieldname] == self.company).run(as_list=1) + names = [x[0] for x in names] + + if names: + versions = qb.DocType("Version") + for batch in create_batch(names, self.batch_size): + qb.from_(versions).delete().where( + (versions.ref_doctype == doctype) & (versions.docname.isin(batch)) + ).run() def delete_communications(self, doctype, company_fieldname): reference_docs = frappe.get_all(doctype, filters={company_fieldname: self.company}) @@ -199,12 +207,13 @@ class TransactionDeletionRecord(Document): ) communication_names = [c.name for c in communications] - frappe.delete_doc("Communication", communication_names, ignore_permissions=True) + for batch in create_batch(communication_names, self.batch_size): + frappe.delete_doc("Communication", batch, ignore_permissions=True) @frappe.whitelist() def get_doctypes_to_be_ignored(): - doctypes_to_be_ignored_list = [ + doctypes_to_be_ignored = [ "Account", "Cost Center", "Warehouse", @@ -223,4 +232,7 @@ def get_doctypes_to_be_ignored(): "Customer", "Supplier", ] - return doctypes_to_be_ignored_list + + doctypes_to_be_ignored.extend(frappe.get_hooks("company_data_to_be_ignored") or []) + + return doctypes_to_be_ignored diff --git a/erpnext/setup/install.py b/erpnext/setup/install.py index 7d7e6b5e07..1f7dddfb95 100644 --- a/erpnext/setup/install.py +++ b/erpnext/setup/install.py @@ -4,12 +4,13 @@ import frappe from frappe import _ -from frappe.custom.doctype.custom_field.custom_field import create_custom_field +from frappe.custom.doctype.custom_field.custom_field import create_custom_fields from frappe.desk.page.setup_wizard.setup_wizard import add_all_roles_to from frappe.utils import cint from erpnext.accounts.doctype.cash_flow_mapper.default_cash_flow_mapper import DEFAULT_MAPPERS from erpnext.setup.default_energy_point_rules import get_default_energy_point_rules +from erpnext.setup.doctype.incoterm.incoterm import create_incoterms from .default_success_action import get_default_success_action @@ -25,9 +26,11 @@ def after_install(): create_default_cash_flow_mapper_templates() create_default_success_action() create_default_energy_point_rules() + create_incoterms() add_company_to_session_defaults() add_standard_navbar_items() add_app_name() + setup_log_settings() frappe.db.commit() @@ -83,35 +86,32 @@ def setup_currency_exchange(): def create_print_setting_custom_fields(): - create_custom_field( - "Print Settings", + create_custom_fields( { - "label": _("Compact Item Print"), - "fieldname": "compact_item_print", - "fieldtype": "Check", - "default": 1, - "insert_after": "with_letterhead", - }, - ) - create_custom_field( - "Print Settings", - { - "label": _("Print UOM after Quantity"), - "fieldname": "print_uom_after_quantity", - "fieldtype": "Check", - "default": 0, - "insert_after": "compact_item_print", - }, - ) - create_custom_field( - "Print Settings", - { - "label": _("Print taxes with zero amount"), - "fieldname": "print_taxes_with_zero_amount", - "fieldtype": "Check", - "default": 0, - "insert_after": "allow_print_for_cancelled", - }, + "Print Settings": [ + { + "label": _("Compact Item Print"), + "fieldname": "compact_item_print", + "fieldtype": "Check", + "default": "1", + "insert_after": "with_letterhead", + }, + { + "label": _("Print UOM after Quantity"), + "fieldname": "print_uom_after_quantity", + "fieldtype": "Check", + "default": "0", + "insert_after": "compact_item_print", + }, + { + "label": _("Print taxes with zero amount"), + "fieldname": "print_taxes_with_zero_amount", + "fieldtype": "Check", + "default": "0", + "insert_after": "allow_print_for_cancelled", + }, + ] + } ) @@ -198,3 +198,10 @@ def add_standard_navbar_items(): def add_app_name(): frappe.db.set_value("System Settings", None, "app_name", "ERPNext") + + +def setup_log_settings(): + log_settings = frappe.get_single("Log Settings") + log_settings.append("logs_to_clear", {"ref_doctype": "Repost Item Valuation", "days": 60}) + + log_settings.save(ignore_permissions=True) diff --git a/erpnext/setup/setup_wizard/data/country_wise_tax.json b/erpnext/setup/setup_wizard/data/country_wise_tax.json index 98817253a4..5750914b9a 100644 --- a/erpnext/setup/setup_wizard/data/country_wise_tax.json +++ b/erpnext/setup/setup_wizard/data/country_wise_tax.json @@ -481,136 +481,461 @@ }, "Germany": { - "tax_categories": [ - "Umsatzsteuer", - "Vorsteuer" - ], + "tax_categories": [], "chart_of_accounts": { "SKR04 mit Kontonummern": { "sales_tax_templates": [ { - "title": "Umsatzsteuer", - "tax_category": "Umsatzsteuer", + "title": "Lieferung oder sonstige Leistung im Inland", "is_default": 1, "taxes": [ { "account_head": { - "account_name": "Umsatzsteuer 19%", + "account_name": "Umsatzsteuer 19 %", "account_number": "3806", "tax_rate": 19.00 }, + "description": "Umsatzsteuer 19 %", "rate": 0.00 }, { "account_head": { - "account_name": "Umsatzsteuer 7%", + "account_name": "Umsatzsteuer 7 %", "account_number": "3801", "tax_rate": 7.00 }, - "rate": 0.00 - } - ] - } - ], - "purchase_tax_templates": [ - { - "title": "Vorsteuer", - "tax_category": "Vorsteuer", - "is_default": 1, - "taxes": [ - { - "account_head": { - "account_name": "Abziehbare Vorsteuer 19%", - "account_number": "1406", - "root_type": "Asset", - "tax_rate": 19.00 - }, - "rate": 0.00 - }, - { - "account_head": { - "account_name": "Abziehbare Vorsteuer 7%", - "account_number": "1401", - "root_type": "Asset", - "tax_rate": 7.00 - }, + "description": "Umsatzsteuer 7 %", "rate": 0.00 } ] }, { - "title": "Innergemeinschaftlicher Erwerb 19% Umsatzsteuer und 19% Vorsteuer", + "title": "Lieferung an Unternehmen in der EU", + "is_default": 0, + "taxes": [] + }, + { + "title": "Sonstige Leistung an Unternehmen in der EU", + "is_default": 0, + "taxes": [] + }, + { + "title": "Lieferung oder sonstige Leistung an nicht-Unternehmen in der EU", + "is_default": 0, "taxes": [ { "account_head": { - "account_name": "Abziehbare Vorsteuer nach § 13b UStG 19%", - "account_number": "1407", - "root_type": "Asset", + "account_name": "Umsatzsteuer 19 %", + "account_number": "3806", "tax_rate": 19.00 }, - "add_deduct_tax": "Add" + "description": "Umsatzsteuer 19 %", + "rate": 0.00 }, { "account_head": { - "account_name": "Umsatzsteuer nach § 13b UStG 19%", + "account_name": "Umsatzsteuer 7 %", + "account_number": "3801", + "tax_rate": 7.00 + }, + "description": "Umsatzsteuer 7 %", + "rate": 0.00 + } + ] + }, + { + "title": "Lieferung in Drittland", + "is_default": 0, + "taxes": [] + }, + { + "title": "Sonstige Leistung an Unternehmen in Drittland", + "is_default": 0, + "taxes": [] + }, + { + "title": "Sonstige Leistung an nicht-Unternehmen in Drittland", + "is_default": 0, + "taxes": [ + { + "account_head": { + "account_name": "Umsatzsteuer 19 %", + "account_number": "3806", + "tax_rate": 19.00 + }, + "description": "Umsatzsteuer 19 %", + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Umsatzsteuer 7 %", + "account_number": "3801", + "tax_rate": 7.00 + }, + "description": "Umsatzsteuer 7 %", + "rate": 0.00 + } + ] + }, + { + "title": "Bauleistungen nach § 13b UStG", + "is_default": 0, + "taxes": [] + } + ], + "purchase_tax_templates": [ + { + "title": "Lieferung aus dem Inland", + "is_default": 1, + "taxes": [ + { + "account_head": { + "account_name": "Abziehbare Vorsteuer 19 %", + "account_number": "1406", + "root_type": "Asset", + "tax_rate": 19.00 + }, + "description": "Abziehbare Vorsteuer 19 %", + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Abziehbare Vorsteuer 7 %", + "account_number": "1401", + "root_type": "Asset", + "tax_rate": 7.00 + }, + "description": "Abziehbare Vorsteuer 7 %", + "rate": 0.00 + } + ] + }, + { + "title": "Sonstige Leistung aus dem Inland", + "is_default": 0, + "taxes": [ + { + "account_head": { + "account_name": "Abziehbare Vorsteuer 19 %", + "account_number": "1406", + "root_type": "Asset", + "tax_rate": 19.00 + }, + "description": "Abziehbare Vorsteuer 19 %", + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Abziehbare Vorsteuer 7 %", + "account_number": "1401", + "root_type": "Asset", + "tax_rate": 7.00 + }, + "description": "Abziehbare Vorsteuer 7 %", + "rate": 0.00 + } + ] + }, + { + "title": "Lieferung aus der EU", + "is_default": 0, + "taxes": [ + { + "account_head": { + "account_name": "Abziehbare Vorsteuer aus innergemeinschaftlichem Erwerb 19 %", + "account_number": "1404", + "root_type": "Asset", + "tax_rate": 19.00 + }, + "description": "Abziehbare Vorsteuer aus innergemeinschaftlichem Erwerb 19 %", + "add_deduct_tax": "Add", + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Abziehbare Vorsteuer aus innergemeinschaftlichem Erwerb", + "account_number": "1402", + "root_type": "Asset", + "tax_rate": 7.00 + }, + "description": "Abziehbare Vorsteuer aus innergemeinschaftlichem Erwerb", + "add_deduct_tax": "Add", + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Umsatzsteuer aus innergemeinschaftlichem Erwerb 19 %", + "account_number": "3804", + "root_type": "Liability", + "tax_rate": 19.00 + }, + "description": "Umsatzsteuer aus innergemeinschaftlichem Erwerb 19 %", + "add_deduct_tax": "Deduct", + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Umsatzsteuer aus innergemeinschaftlichem Erwerb", + "account_number": "3802", + "root_type": "Liability", + "tax_rate": 7.00 + }, + "description": "Umsatzsteuer aus innergemeinschaftlichem Erwerb", + "add_deduct_tax": "Deduct", + "rate": 0.00 + } + ] + }, + { + "title": "Sonstige Leistung aus der EU", + "is_default": 0, + "taxes": [ + { + "account_head": { + "account_name": "Abziehbare Vorsteuer nach § 13b UStG 19 %", + "account_number": "1540", + "root_type": "Asset", + "tax_rate": 19.00 + }, + "description": "Abziehbare Vorsteuer nach § 13b UStG 19 %", + "add_deduct_tax": "Add", + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Abziehbare Vorsteuer nach § 13b UStG", + "account_number": "1541", + "root_type": "Asset", + "tax_rate": 7.00 + }, + "description": "Abziehbare Vorsteuer nach § 13b UStG", + "add_deduct_tax": "Add", + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Umsatzsteuer nach § 13b UStG 19 %", "account_number": "3837", "root_type": "Liability", "tax_rate": 19.00 }, - "add_deduct_tax": "Deduct" + "description": "Umsatzsteuer nach § 13b UStG 19 %", + "add_deduct_tax": "Deduct", + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Umsatzsteuer nach § 13b UStG", + "account_number": "3835", + "root_type": "Liability", + "tax_rate": 7.00 + }, + "description": "Umsatzsteuer nach § 13b UStG", + "add_deduct_tax": "Deduct", + "rate": 0.00 + } + ] + }, + { + "title": "Sonstige Leistung aus Drittland", + "is_default": 0, + "taxes": [ + { + "account_head": { + "account_name": "Abziehbare Vorsteuer nach § 13b UStG 19 %", + "account_number": "1540", + "root_type": "Asset", + "tax_rate": 19.00 + }, + "description": "Abziehbare Vorsteuer nach § 13b UStG 19 %", + "add_deduct_tax": "Add", + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Abziehbare Vorsteuer nach § 13b UStG", + "account_number": "1541", + "root_type": "Asset", + "tax_rate": 7.00 + }, + "description": "Abziehbare Vorsteuer nach § 13b UStG", + "add_deduct_tax": "Add", + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Umsatzsteuer nach § 13b UStG 19 %", + "account_number": "3837", + "root_type": "Liability", + "tax_rate": 19.00 + }, + "description": "Umsatzsteuer nach § 13b UStG 19 %", + "add_deduct_tax": "Deduct", + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Umsatzsteuer nach § 13b UStG", + "account_number": "3835", + "root_type": "Liability", + "tax_rate": 7.00 + }, + "description": "Umsatzsteuer nach § 13b UStG", + "add_deduct_tax": "Deduct", + "rate": 0.00 + } + ] + }, + { + "title": "Lieferung aus Drittland", + "is_default": 0, + "taxes": [ + { + "account_head": { + "account_name": "Abziehbare Vorsteuer 19 %", + "account_number": "1406", + "root_type": "Asset", + "tax_rate": 19.00 + }, + "description": "Abziehbare Vorsteuer 19 %", + "add_deduct_tax": "Add", + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Abziehbare Vorsteuer 7 %", + "account_number": "1401", + "root_type": "Asset", + "tax_rate": 7.00 + }, + "description": "Abziehbare Vorsteuer 7 %", + "add_deduct_tax": "Add", + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Entstandene Einfuhrumsatzsteuer", + "account_number": "1433", + "root_type": "Asset" + }, + "description": "Entstandene Einfuhrumsatzsteuer", + "add_deduct_tax": "Deduct", + "rate": 0.00 + } + ] + }, + { + "title": "Bauleistungen nach § 13b UStG", + "is_default": 0, + "taxes": [ + { + "account_head": { + "account_name": "Abziehbare Vorsteuer nach § 13b UStG 19 %", + "account_number": "1540", + "root_type": "Asset", + "tax_rate": 19.00 + }, + "description": "Abziehbare Vorsteuer nach § 13b UStG 19 %", + "add_deduct_tax": "Add", + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Abziehbare Vorsteuer nach § 13b UStG", + "account_number": "1541", + "root_type": "Asset", + "tax_rate": 7.00 + }, + "description": "Abziehbare Vorsteuer nach § 13b UStG", + "add_deduct_tax": "Add", + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Umsatzsteuer nach § 13b UStG 19 %", + "account_number": "3837", + "root_type": "Liability", + "tax_rate": 19.00 + }, + "description": "Umsatzsteuer nach § 13b UStG 19 %", + "add_deduct_tax": "Deduct", + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Umsatzsteuer nach § 13b UStG", + "account_number": "3835", + "root_type": "Liability", + "tax_rate": 7.00 + }, + "description": "Umsatzsteuer nach § 13b UStG", + "add_deduct_tax": "Deduct", + "rate": 0.00 } ] } ], "item_tax_templates": [ { - "title": "Umsatzsteuer 19%", + "title": "19 %", "taxes": [ { "tax_type": { - "account_name": "Umsatzsteuer 19%", + "account_name": "Umsatzsteuer 19 %", "account_number": "3806", + "root_type": "Liability", "tax_rate": 19.00 }, "tax_rate": 19.00 }, { "tax_type": { - "account_name": "Umsatzsteuer 7%", + "account_name": "Umsatzsteuer 7 %", "account_number": "3801", + "root_type": "Liability", "tax_rate": 7.00 }, "tax_rate": 0.00 - } - ] - }, - { - "title": "Umsatzsteuer 7%", - "taxes": [ - { - "tax_type": { - "account_name": "Umsatzsteuer 19%", - "account_number": "3806", - "tax_rate": 19.00 - }, - "tax_rate": 0.00 }, { "tax_type": { - "account_name": "Umsatzsteuer 7%", - "account_number": "3801", - "tax_rate": 7.00 + "account_name": "Umsatzsteuer aus innergemeinschaftlichem Erwerb 19 %", + "account_number": "3804", + "root_type": "Liability", + "tax_rate": 19.00 }, - "tax_rate": 7.00 - } - ] - }, - { - "title": "Vorsteuer 19%", - "taxes": [ + "tax_rate": 19.00 + }, { "tax_type": { - "account_name": "Abziehbare Vorsteuer 19%", + "account_name": "Umsatzsteuer aus innergemeinschaftlichem Erwerb", + "account_number": "3802", + "root_type": "Liability", + "tax_rate": 7.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Umsatzsteuer nach § 13b UStG 19 %", + "account_number": "3837", + "root_type": "Liability", + "tax_rate": 19.00 + }, + "tax_rate": 19.00 + }, + { + "tax_type": { + "account_name": "Umsatzsteuer nach § 13b UStG", + "account_number": "3835", + "root_type": "Liability", + "tax_rate": 7.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer 19 %", "account_number": "1406", "root_type": "Asset", "tax_rate": 19.00 @@ -619,21 +944,119 @@ }, { "tax_type": { - "account_name": "Abziehbare Vorsteuer 7%", + "account_name": "Abziehbare Vorsteuer 7 %", "account_number": "1401", "root_type": "Asset", "tax_rate": 7.00 }, "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer aus innergemeinschaftlichem Erwerb 19 %", + "account_number": "1404", + "root_type": "Asset", + "tax_rate": 19.00 + }, + "tax_rate": 19.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer aus innergemeinschaftlichem Erwerb", + "account_number": "1402", + "root_type": "Asset", + "tax_rate": 7.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer nach § 13b UStG 19 %", + "account_number": "1540", + "root_type": "Asset", + "tax_rate": 19.00 + }, + "tax_rate": 19.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer nach § 13b UStG", + "account_number": "1541", + "root_type": "Asset", + "tax_rate": 7.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Entstandene Einfuhrumsatzsteuer", + "account_number": "1433", + "root_type": "Asset" + }, + "tax_rate": 19.00 } ] }, { - "title": "Vorsteuer 7%", + "title": "7 %", "taxes": [ { "tax_type": { - "account_name": "Abziehbare Vorsteuer 19%", + "account_name": "Umsatzsteuer 19 %", + "account_number": "3806", + "root_type": "Liability", + "tax_rate": 19.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Umsatzsteuer 7 %", + "account_number": "3801", + "root_type": "Liability", + "tax_rate": 7.00 + }, + "tax_rate": 7.00 + }, + { + "tax_type": { + "account_name": "Umsatzsteuer aus innergemeinschaftlichem Erwerb 19 %", + "account_number": "3804", + "root_type": "Liability", + "tax_rate": 19.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Umsatzsteuer aus innergemeinschaftlichem Erwerb", + "account_number": "3802", + "root_type": "Liability", + "tax_rate": 7.00 + }, + "tax_rate": 7.00 + }, + { + "tax_type": { + "account_name": "Umsatzsteuer nach § 13b UStG 19 %", + "account_number": "3837", + "root_type": "Liability", + "tax_rate": 19.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Umsatzsteuer nach § 13b UStG", + "account_number": "3835", + "root_type": "Liability", + "tax_rate": 7.00 + }, + "tax_rate": 7.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer 19 %", "account_number": "1406", "root_type": "Asset", "tax_rate": 19.00 @@ -642,12 +1065,177 @@ }, { "tax_type": { - "account_name": "Abziehbare Vorsteuer 7%", + "account_name": "Abziehbare Vorsteuer 7 %", "account_number": "1401", "root_type": "Asset", "tax_rate": 7.00 }, "tax_rate": 7.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer aus innergemeinschaftlichem Erwerb 19 %", + "account_number": "1404", + "root_type": "Asset", + "tax_rate": 19.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer aus innergemeinschaftlichem Erwerb", + "account_number": "1402", + "root_type": "Asset", + "tax_rate": 7.00 + }, + "tax_rate": 7.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer nach § 13b UStG 19 %", + "account_number": "1540", + "root_type": "Asset", + "tax_rate": 19.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer nach § 13b UStG", + "account_number": "1541", + "root_type": "Asset", + "tax_rate": 7.00 + }, + "tax_rate": 7.00 + }, + { + "tax_type": { + "account_name": "Entstandene Einfuhrumsatzsteuer", + "account_number": "1433", + "root_type": "Asset" + }, + "tax_rate": 7.00 + } + ] + }, + { + "title": "0 %", + "taxes": [ + { + "tax_type": { + "account_name": "Umsatzsteuer 19 %", + "account_number": "3806", + "root_type": "Liability", + "tax_rate": 19.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Umsatzsteuer 7 %", + "account_number": "3801", + "root_type": "Liability", + "tax_rate": 7.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Umsatzsteuer aus innergemeinschaftlichem Erwerb 19 %", + "account_number": "3804", + "root_type": "Liability", + "tax_rate": 19.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Umsatzsteuer aus innergemeinschaftlichem Erwerb", + "account_number": "3802", + "root_type": "Liability", + "tax_rate": 7.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Umsatzsteuer nach § 13b UStG 19 %", + "account_number": "3837", + "root_type": "Liability", + "tax_rate": 19.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Umsatzsteuer nach § 13b UStG", + "account_number": "3835", + "root_type": "Liability", + "tax_rate": 7.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer 19 %", + "account_number": "1406", + "root_type": "Asset", + "tax_rate": 19.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer 7 %", + "account_number": "1401", + "root_type": "Asset", + "tax_rate": 7.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer aus innergemeinschaftlichem Erwerb 19 %", + "account_number": "1404", + "root_type": "Asset", + "tax_rate": 19.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer aus innergemeinschaftlichem Erwerb", + "account_number": "1402", + "root_type": "Asset", + "tax_rate": 7.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer nach § 13b UStG 19 %", + "account_number": "1540", + "root_type": "Asset", + "tax_rate": 19.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer nach § 13b UStG", + "account_number": "1541", + "root_type": "Asset", + "tax_rate": 7.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Entstandene Einfuhrumsatzsteuer", + "account_number": "1433", + "root_type": "Asset" + }, + "tax_rate": 0.00 } ] } @@ -656,51 +1244,390 @@ "SKR03 mit Kontonummern": { "sales_tax_templates": [ { - "title": "Umsatzsteuer", - "tax_category": "Umsatzsteuer", + "title": "Lieferung oder sonstige Leistung im Inland", "is_default": 1, "taxes": [ { "account_head": { - "account_name": "Umsatzsteuer 19%", + "account_name": "Umsatzsteuer 19 %", "account_number": "1776", "tax_rate": 19.00 }, + "description": "Umsatzsteuer 19 %", "rate": 0.00 }, { "account_head": { - "account_name": "Umsatzsteuer 7%", + "account_name": "Umsatzsteuer 7 %", "account_number": "1771", "tax_rate": 7.00 }, + "description": "Umsatzsteuer 7 %", "rate": 0.00 } ] + }, + { + "title": "Lieferung an Unternehmen in der EU", + "is_default": 0, + "taxes": [] + }, + { + "title": "Sonstige Leistung an Unternehmen in der EU", + "is_default": 0, + "taxes": [] + }, + { + "title": "Lieferung oder sonstige Leistung an nicht-Unternehmen in der EU", + "is_default": 0, + "taxes": [ + { + "account_head": { + "account_name": "Umsatzsteuer 19 %", + "account_number": "1776", + "tax_rate": 19.00 + }, + "description": "Umsatzsteuer 19 %", + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Umsatzsteuer 7 %", + "account_number": "1771", + "tax_rate": 7.00 + }, + "description": "Umsatzsteuer 7 %", + "rate": 0.00 + } + ] + }, + { + "title": "Lieferung in Drittland", + "is_default": 0, + "taxes": [] + }, + { + "title": "Sonstige Leistung an Unternehmen in Drittland", + "is_default": 0, + "taxes": [] + }, + { + "title": "Sonstige Leistung an nicht-Unternehmen in Drittland", + "is_default": 0, + "taxes": [ + { + "account_head": { + "account_name": "Umsatzsteuer 19 %", + "account_number": "1776", + "tax_rate": 19.00 + }, + "description": "Umsatzsteuer 19 %", + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Umsatzsteuer 7 %", + "account_number": "1771", + "tax_rate": 7.00 + }, + "description": "Umsatzsteuer 7 %", + "rate": 0.00 + } + ] + }, + { + "title": "Bauleistungen nach § 13b UStG", + "is_default": 0, + "taxes": [] } ], "purchase_tax_templates": [ { - "title": "Vorsteuer", - "tax_category": "Vorsteuer", + "title": "Lieferung aus dem Inland", "is_default": 1, "taxes": [ { "account_head": { - "account_name": "Abziehbare Vorsteuer 19%", + "account_name": "Abziehbare Vorsteuer 19 %", "account_number": "1576", "root_type": "Asset", "tax_rate": 19.00 }, + "description": "Abziehbare Vorsteuer 19 %", "rate": 0.00 }, { "account_head": { - "account_name": "Abziehbare Vorsteuer 7%", + "account_name": "Abziehbare Vorsteuer 7 %", "account_number": "1571", "root_type": "Asset", "tax_rate": 7.00 }, + "description": "Abziehbare Vorsteuer 7 %", + "rate": 0.00 + } + ] + }, + { + "title": "Sonstige Leistung aus dem Inland", + "is_default": 0, + "taxes": [ + { + "account_head": { + "account_name": "Abziehbare Vorsteuer 19 %", + "account_number": "1576", + "root_type": "Asset", + "tax_rate": 19.00 + }, + "description": "Abziehbare Vorsteuer 19 %", + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Abziehbare Vorsteuer 7 %", + "account_number": "1571", + "root_type": "Asset", + "tax_rate": 7.00 + }, + "description": "Abziehbare Vorsteuer 7 %", + "rate": 0.00 + } + ] + }, + { + "title": "Lieferung aus der EU", + "is_default": 0, + "taxes": [ + { + "account_head": { + "account_name": "Abziehbare Vorsteuer aus innergemeinschaftlichem Erwerb 19 %", + "account_number": "1574", + "root_type": "Asset", + "tax_rate": 19.00 + }, + "description": "Abziehbare Vorsteuer aus innergemeinschaftlichem Erwerb 19 %", + "add_deduct_tax": "Add", + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Abziehbare Vorsteuer aus innergemeinschaftlichem Erwerb", + "account_number": "1572", + "root_type": "Asset", + "tax_rate": 7.00 + }, + "description": "Abziehbare Vorsteuer aus innergemeinschaftlichem Erwerb", + "add_deduct_tax": "Add", + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Umsatzsteuer aus innergemeinschaftlichem Erwerb 19 %", + "account_number": "1774", + "root_type": "Liability", + "tax_rate": 19.00 + }, + "description": "Umsatzsteuer aus innergemeinschaftlichem Erwerb 19 %", + "add_deduct_tax": "Deduct", + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Umsatzsteuer aus innergemeinschaftlichem Erwerb", + "account_number": "1772", + "root_type": "Liability", + "tax_rate": 7.00 + }, + "description": "Umsatzsteuer aus innergemeinschaftlichem Erwerb", + "add_deduct_tax": "Deduct", + "rate": 0.00 + } + ] + }, + { + "title": "Sonstige Leistung aus der EU", + "is_default": 0, + "taxes": [ + { + "account_head": { + "account_name": "Abziehbare Vorsteuer nach § 13b UStG 19 %", + "account_number": "1540", + "root_type": "Asset", + "tax_rate": 19.00 + }, + "description": "Abziehbare Vorsteuer nach § 13b UStG 19 %", + "add_deduct_tax": "Add", + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Abziehbare Vorsteuer nach § 13b UStG", + "account_number": "1541", + "root_type": "Asset", + "tax_rate": 7.00 + }, + "description": "Abziehbare Vorsteuer nach § 13b UStG", + "add_deduct_tax": "Add", + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Umsatzsteuer nach § 13b UStG 19 %", + "account_number": "1787", + "root_type": "Liability", + "tax_rate": 19.00 + }, + "description": "Umsatzsteuer nach § 13b UStG 19 %", + "add_deduct_tax": "Deduct", + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Umsatzsteuer nach § 13b UStG", + "account_number": "1785", + "root_type": "Liability", + "tax_rate": 7.00 + }, + "description": "Umsatzsteuer nach § 13b UStG", + "add_deduct_tax": "Deduct", + "rate": 0.00 + } + ] + }, + { + "title": "Sonstige Leistung aus Drittland", + "is_default": 0, + "taxes": [ + { + "account_head": { + "account_name": "Abziehbare Vorsteuer nach § 13b UStG 19 %", + "account_number": "1540", + "root_type": "Asset", + "tax_rate": 19.00 + }, + "description": "Abziehbare Vorsteuer nach § 13b UStG 19 %", + "add_deduct_tax": "Add", + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Abziehbare Vorsteuer nach § 13b UStG", + "account_number": "1541", + "root_type": "Asset", + "tax_rate": 7.00 + }, + "description": "Abziehbare Vorsteuer nach § 13b UStG", + "add_deduct_tax": "Add", + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Umsatzsteuer nach § 13b UStG 19 %", + "account_number": "1787", + "root_type": "Liability", + "tax_rate": 19.00 + }, + "description": "Umsatzsteuer nach § 13b UStG 19 %", + "add_deduct_tax": "Deduct", + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Umsatzsteuer nach § 13b UStG", + "account_number": "1785", + "root_type": "Liability", + "tax_rate": 7.00 + }, + "description": "Umsatzsteuer nach § 13b UStG", + "add_deduct_tax": "Deduct", + "rate": 0.00 + } + ] + }, + { + "title": "Lieferung aus Drittland", + "is_default": 0, + "taxes": [ + { + "account_head": { + "account_name": "Abziehbare Vorsteuer 19 %", + "account_number": "1576", + "root_type": "Asset", + "tax_rate": 19.00 + }, + "description": "Abziehbare Vorsteuer 19 %", + "add_deduct_tax": "Add", + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Abziehbare Vorsteuer 7 %", + "account_number": "1571", + "root_type": "Asset", + "tax_rate": 7.00 + }, + "description": "Abziehbare Vorsteuer 7 %", + "add_deduct_tax": "Add", + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Entstandene Einfuhrumsatzsteuer", + "account_number": "1588", + "root_type": "Asset" + }, + "description": "Entstandene Einfuhrumsatzsteuer", + "add_deduct_tax": "Deduct", + "rate": 0.00 + } + ] + }, + { + "title": "Bauleistungen nach § 13b UStG", + "is_default": 0, + "taxes": [ + { + "account_head": { + "account_name": "Abziehbare Vorsteuer nach § 13b UStG 19 %", + "account_number": "1540", + "root_type": "Asset", + "tax_rate": 19.00 + }, + "description": "Abziehbare Vorsteuer nach § 13b UStG 19 %", + "add_deduct_tax": "Add", + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Abziehbare Vorsteuer nach § 13b UStG", + "account_number": "1541", + "root_type": "Asset", + "tax_rate": 7.00 + }, + "description": "Abziehbare Vorsteuer nach § 13b UStG", + "add_deduct_tax": "Add", + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Umsatzsteuer nach § 13b UStG 19 %", + "account_number": "1787", + "root_type": "Liability", + "tax_rate": 19.00 + }, + "description": "Umsatzsteuer nach § 13b UStG 19 %", + "add_deduct_tax": "Deduct", + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Umsatzsteuer nach § 13b UStG", + "account_number": "1785", + "root_type": "Liability", + "tax_rate": 7.00 + }, + "description": "Umsatzsteuer nach § 13b UStG", + "add_deduct_tax": "Deduct", "rate": 0.00 } ] @@ -708,53 +1635,65 @@ ], "item_tax_templates": [ { - "title": "Umsatzsteuer 19%", + "title": "19 %", "taxes": [ { "tax_type": { - "account_name": "Umsatzsteuer 19%", + "account_name": "Umsatzsteuer 19 %", "account_number": "1776", + "root_type": "Liability", "tax_rate": 19.00 }, "tax_rate": 19.00 }, { "tax_type": { - "account_name": "Umsatzsteuer 7%", + "account_name": "Umsatzsteuer 7 %", "account_number": "1771", + "root_type": "Liability", "tax_rate": 7.00 }, "tax_rate": 0.00 - } - ] - }, - { - "title": "Umsatzsteuer 7%", - "taxes": [ - { - "tax_type": { - "account_name": "Umsatzsteuer 19%", - "account_number": "1776", - "tax_rate": 19.00 - }, - "tax_rate": 0.00 }, { "tax_type": { - "account_name": "Umsatzsteuer 7%", - "account_number": "1771", - "tax_rate": 7.00 + "account_name": "Umsatzsteuer aus innergemeinschaftlichem Erwerb 19 %", + "account_number": "1774", + "root_type": "Liability", + "tax_rate": 19.00 }, - "tax_rate": 7.00 - } - ] - }, - { - "title": "Vorsteuer 19%", - "taxes": [ + "tax_rate": 19.00 + }, { "tax_type": { - "account_name": "Abziehbare Vorsteuer 19%", + "account_name": "Umsatzsteuer aus innergemeinschaftlichem Erwerb", + "account_number": "1772", + "root_type": "Liability", + "tax_rate": 7.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Umsatzsteuer nach § 13b UStG 19 %", + "account_number": "1787", + "root_type": "Liability", + "tax_rate": 19.00 + }, + "tax_rate": 19.00 + }, + { + "tax_type": { + "account_name": "Umsatzsteuer nach § 13b UStG", + "account_number": "1785", + "root_type": "Liability", + "tax_rate": 7.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer 19 %", "account_number": "1576", "root_type": "Asset", "tax_rate": 19.00 @@ -763,21 +1702,119 @@ }, { "tax_type": { - "account_name": "Abziehbare Vorsteuer 7%", + "account_name": "Abziehbare Vorsteuer 7 %", "account_number": "1571", "root_type": "Asset", "tax_rate": 7.00 }, "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer aus innergemeinschaftlichem Erwerb 19 %", + "account_number": "1574", + "root_type": "Asset", + "tax_rate": 19.00 + }, + "tax_rate": 19.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer aus innergemeinschaftlichem Erwerb", + "account_number": "1572", + "root_type": "Asset", + "tax_rate": 7.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer nach § 13b UStG 19 %", + "account_number": "1540", + "root_type": "Asset", + "tax_rate": 19.00 + }, + "tax_rate": 19.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer nach § 13b UStG", + "account_number": "1541", + "root_type": "Asset", + "tax_rate": 7.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Entstandene Einfuhrumsatzsteuer", + "account_number": "1588", + "root_type": "Asset" + }, + "tax_rate": 19.00 } ] }, { - "title": "Vorsteuer 7%", + "title": "7 %", "taxes": [ { "tax_type": { - "account_name": "Abziehbare Vorsteuer 19%", + "account_name": "Umsatzsteuer 19 %", + "account_number": "1776", + "root_type": "Liability", + "tax_rate": 19.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Umsatzsteuer 7 %", + "account_number": "1771", + "root_type": "Liability", + "tax_rate": 7.00 + }, + "tax_rate": 7.00 + }, + { + "tax_type": { + "account_name": "Umsatzsteuer aus innergemeinschaftlichem Erwerb 19 %", + "account_number": "1774", + "root_type": "Liability", + "tax_rate": 19.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Umsatzsteuer aus innergemeinschaftlichem Erwerb", + "account_number": "1772", + "root_type": "Liability", + "tax_rate": 7.00 + }, + "tax_rate": 7.00 + }, + { + "tax_type": { + "account_name": "Umsatzsteuer nach § 13b UStG 19 %", + "account_number": "1787", + "root_type": "Liability", + "tax_rate": 19.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Umsatzsteuer nach § 13b UStG", + "account_number": "1785", + "root_type": "Liability", + "tax_rate": 7.00 + }, + "tax_rate": 7.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer 19 %", "account_number": "1576", "root_type": "Asset", "tax_rate": 19.00 @@ -786,12 +1823,177 @@ }, { "tax_type": { - "account_name": "Abziehbare Vorsteuer 7%", + "account_name": "Abziehbare Vorsteuer 7 %", "account_number": "1571", "root_type": "Asset", "tax_rate": 7.00 }, "tax_rate": 7.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer aus innergemeinschaftlichem Erwerb 19 %", + "account_number": "1574", + "root_type": "Asset", + "tax_rate": 19.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer aus innergemeinschaftlichem Erwerb", + "account_number": "1572", + "root_type": "Asset", + "tax_rate": 7.00 + }, + "tax_rate": 7.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer nach § 13b UStG 19 %", + "account_number": "1540", + "root_type": "Asset", + "tax_rate": 19.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer nach § 13b UStG", + "account_number": "1541", + "root_type": "Asset", + "tax_rate": 7.00 + }, + "tax_rate": 7.00 + }, + { + "tax_type": { + "account_name": "Entstandene Einfuhrumsatzsteuer", + "account_number": "1588", + "root_type": "Asset" + }, + "tax_rate": 7.00 + } + ] + }, + { + "title": "0 %", + "taxes": [ + { + "tax_type": { + "account_name": "Umsatzsteuer 19 %", + "account_number": "1776", + "root_type": "Liability", + "tax_rate": 19.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Umsatzsteuer 7 %", + "account_number": "1771", + "root_type": "Liability", + "tax_rate": 7.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Umsatzsteuer aus innergemeinschaftlichem Erwerb 19 %", + "account_number": "1774", + "root_type": "Liability", + "tax_rate": 19.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Umsatzsteuer aus innergemeinschaftlichem Erwerb", + "account_number": "1772", + "root_type": "Liability", + "tax_rate": 7.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Umsatzsteuer nach § 13b UStG 19 %", + "account_number": "1787", + "root_type": "Liability", + "tax_rate": 19.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Umsatzsteuer nach § 13b UStG", + "account_number": "1785", + "root_type": "Liability", + "tax_rate": 7.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer 19 %", + "account_number": "1576", + "root_type": "Asset", + "tax_rate": 19.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer 7 %", + "account_number": "1571", + "root_type": "Asset", + "tax_rate": 7.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer aus innergemeinschaftlichem Erwerb 19 %", + "account_number": "1574", + "root_type": "Asset", + "tax_rate": 19.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer aus innergemeinschaftlichem Erwerb", + "account_number": "1572", + "root_type": "Asset", + "tax_rate": 7.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer nach § 13b UStG 19 %", + "account_number": "1540", + "root_type": "Asset", + "tax_rate": 19.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer nach § 13b UStG", + "account_number": "1541", + "root_type": "Asset", + "tax_rate": 7.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Entstandene Einfuhrumsatzsteuer", + "account_number": "1588", + "root_type": "Asset" + }, + "tax_rate": 0.00 } ] } @@ -800,51 +2002,390 @@ "Standard with Numbers": { "sales_tax_templates": [ { - "title": "Umsatzsteuer", - "tax_category": "Umsatzsteuer", + "title": "Lieferung oder sonstige Leistung im Inland", "is_default": 1, "taxes": [ { "account_head": { - "account_name": "Umsatzsteuer 19%", - "account_number": "2301", + "account_name": "Umsatzsteuer 19 %", + "account_number": "2320", "tax_rate": 19.00 }, + "description": "Umsatzsteuer 19 %", "rate": 0.00 }, { "account_head": { - "account_name": "Umsatzsteuer 7%", - "account_number": "2302", + "account_name": "Umsatzsteuer 7 %", + "account_number": "2321", "tax_rate": 7.00 }, + "description": "Umsatzsteuer 7 %", "rate": 0.00 } ] + }, + { + "title": "Lieferung an Unternehmen in der EU", + "is_default": 0, + "taxes": [] + }, + { + "title": "Sonstige Leistung an Unternehmen in der EU", + "is_default": 0, + "taxes": [] + }, + { + "title": "Lieferung oder sonstige Leistung an nicht-Unternehmen in der EU", + "is_default": 0, + "taxes": [ + { + "account_head": { + "account_name": "Umsatzsteuer 19 %", + "account_number": "2320", + "tax_rate": 19.00 + }, + "description": "Umsatzsteuer 19 %", + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Umsatzsteuer 7 %", + "account_number": "2321", + "tax_rate": 7.00 + }, + "description": "Umsatzsteuer 7 %", + "rate": 0.00 + } + ] + }, + { + "title": "Lieferung in Drittland", + "is_default": 0, + "taxes": [] + }, + { + "title": "Sonstige Leistung an Unternehmen in Drittland", + "is_default": 0, + "taxes": [] + }, + { + "title": "Sonstige Leistung an nicht-Unternehmen in Drittland", + "is_default": 0, + "taxes": [ + { + "account_head": { + "account_name": "Umsatzsteuer 19 %", + "account_number": "2320", + "tax_rate": 19.00 + }, + "description": "Umsatzsteuer 19 %", + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Umsatzsteuer 7 %", + "account_number": "2321", + "tax_rate": 7.00 + }, + "description": "Umsatzsteuer 7 %", + "rate": 0.00 + } + ] + }, + { + "title": "Bauleistungen nach § 13b UStG", + "is_default": 0, + "taxes": [] } ], "purchase_tax_templates": [ { - "title": "Vorsteuer", - "tax_category": "Vorsteuer", + "title": "Lieferung aus dem Inland", "is_default": 1, "taxes": [ { "account_head": { - "account_name": "Abziehbare Vorsteuer 19%", - "account_number": "1501", + "account_name": "Abziehbare Vorsteuer 19 %", + "account_number": "1520", "root_type": "Asset", "tax_rate": 19.00 }, + "description": "Abziehbare Vorsteuer 19 %", "rate": 0.00 }, { "account_head": { - "account_name": "Abziehbare Vorsteuer 7%", - "account_number": "1502", + "account_name": "Abziehbare Vorsteuer 7 %", + "account_number": "1521", "root_type": "Asset", "tax_rate": 7.00 }, + "description": "Abziehbare Vorsteuer 7 %", + "rate": 0.00 + } + ] + }, + { + "title": "Sonstige Leistung aus dem Inland", + "is_default": 0, + "taxes": [ + { + "account_head": { + "account_name": "Abziehbare Vorsteuer 19 %", + "account_number": "1520", + "root_type": "Asset", + "tax_rate": 19.00 + }, + "description": "Abziehbare Vorsteuer 19 %", + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Abziehbare Vorsteuer 7 %", + "account_number": "1521", + "root_type": "Asset", + "tax_rate": 7.00 + }, + "description": "Abziehbare Vorsteuer 7 %", + "rate": 0.00 + } + ] + }, + { + "title": "Lieferung aus der EU", + "is_default": 0, + "taxes": [ + { + "account_head": { + "account_name": "Abziehbare Vorsteuer aus innergemeinschaftlichen Erwerb 19 %", + "account_number": "1530", + "root_type": "Asset", + "tax_rate": 19.00 + }, + "description": "Abziehbare Vorsteuer aus innergemeinschaftlichen Erwerb 19 %", + "add_deduct_tax": "Add", + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Abziehbare Vorsteuer aus innergemeinschaftlichen Erwerb 7 %", + "account_number": "1531", + "root_type": "Asset", + "tax_rate": 7.00 + }, + "description": "Abziehbare Vorsteuer aus innergemeinschaftlichen Erwerb 7 %", + "add_deduct_tax": "Add", + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Umsatzsteuer aus innergemeinschaftlichen Erwerb 19 %", + "account_number": "2330", + "root_type": "Liability", + "tax_rate": 19.00 + }, + "description": "Umsatzsteuer aus innergemeinschaftlichen Erwerb 19 %", + "add_deduct_tax": "Deduct", + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Umsatzsteuer aus innergemeinschaftlichen Erwerb 7 %", + "account_number": "2331", + "root_type": "Liability", + "tax_rate": 7.00 + }, + "description": "Umsatzsteuer aus innergemeinschaftlichen Erwerb 7 %", + "add_deduct_tax": "Deduct", + "rate": 0.00 + } + ] + }, + { + "title": "Sonstige Leistung aus der EU", + "is_default": 0, + "taxes": [ + { + "account_head": { + "account_name": "Abziehbare Vorsteuer nach § 13b UStG 19%", + "account_number": "1540", + "root_type": "Asset", + "tax_rate": 19.00 + }, + "description": "Abziehbare Vorsteuer nach § 13b UStG 19%", + "add_deduct_tax": "Add", + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Abziehbare Vorsteuer nach § 13b UStG 7%", + "account_number": "1541", + "root_type": "Asset", + "tax_rate": 7.00 + }, + "description": "Abziehbare Vorsteuer nach § 13b UStG 7%", + "add_deduct_tax": "Add", + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Umsatzsteuer nach § 13b UStG 19%", + "account_number": "2340", + "root_type": "Liability", + "tax_rate": 19.00 + }, + "description": "Umsatzsteuer nach § 13b UStG 19%", + "add_deduct_tax": "Deduct", + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Umsatzsteuer nach § 13b UStG 7%", + "account_number": "2341", + "root_type": "Liability", + "tax_rate": 7.00 + }, + "description": "Umsatzsteuer nach § 13b UStG 7%", + "add_deduct_tax": "Deduct", + "rate": 0.00 + } + ] + }, + { + "title": "Sonstige Leistung aus Drittland", + "is_default": 0, + "taxes": [ + { + "account_head": { + "account_name": "Abziehbare Vorsteuer nach § 13b UStG 19%", + "account_number": "1540", + "root_type": "Asset", + "tax_rate": 19.00 + }, + "description": "Abziehbare Vorsteuer nach § 13b UStG 19%", + "add_deduct_tax": "Add", + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Abziehbare Vorsteuer nach § 13b UStG 7%", + "account_number": "1541", + "root_type": "Asset", + "tax_rate": 7.00 + }, + "description": "Abziehbare Vorsteuer nach § 13b UStG 7%", + "add_deduct_tax": "Add", + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Umsatzsteuer nach § 13b UStG 19%", + "account_number": "2340", + "root_type": "Liability", + "tax_rate": 19.00 + }, + "description": "Umsatzsteuer nach § 13b UStG 19%", + "add_deduct_tax": "Deduct", + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Umsatzsteuer nach § 13b UStG 7%", + "account_number": "2341", + "root_type": "Liability", + "tax_rate": 7.00 + }, + "description": "Umsatzsteuer nach § 13b UStG 7%", + "add_deduct_tax": "Deduct", + "rate": 0.00 + } + ] + }, + { + "title": "Lieferung aus Drittland", + "is_default": 0, + "taxes": [ + { + "account_head": { + "account_name": "Abziehbare Vorsteuer 19 %", + "account_number": "1520", + "root_type": "Asset", + "tax_rate": 19.00 + }, + "description": "Abziehbare Vorsteuer 19 %", + "add_deduct_tax": "Add", + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Abziehbare Vorsteuer 7 %", + "account_number": "1521", + "root_type": "Asset", + "tax_rate": 7.00 + }, + "description": "Abziehbare Vorsteuer 7 %", + "add_deduct_tax": "Add", + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Entstandene Einfuhrumsatzsteuer", + "account_number": "1550", + "root_type": "Asset" + }, + "description": "Entstandene Einfuhrumsatzsteuer", + "add_deduct_tax": "Deduct", + "rate": 0.00 + } + ] + }, + { + "title": "Bauleistungen nach § 13b UStG", + "is_default": 0, + "taxes": [ + { + "account_head": { + "account_name": "Abziehbare Vorsteuer nach § 13b UStG 19%", + "account_number": "1540", + "root_type": "Asset", + "tax_rate": 19.00 + }, + "description": "Abziehbare Vorsteuer nach § 13b UStG 19%", + "add_deduct_tax": "Add", + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Abziehbare Vorsteuer nach § 13b UStG 7%", + "account_number": "1541", + "root_type": "Asset", + "tax_rate": 7.00 + }, + "description": "Abziehbare Vorsteuer nach § 13b UStG 7%", + "add_deduct_tax": "Add", + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Umsatzsteuer nach § 13b UStG 19%", + "account_number": "2340", + "root_type": "Liability", + "tax_rate": 19.00 + }, + "description": "Umsatzsteuer nach § 13b UStG 19%", + "add_deduct_tax": "Deduct", + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Umsatzsteuer nach § 13b UStG 7%", + "account_number": "2341", + "root_type": "Liability", + "tax_rate": 7.00 + }, + "description": "Umsatzsteuer nach § 13b UStG 7%", + "add_deduct_tax": "Deduct", "rate": 0.00 } ] @@ -852,54 +2393,66 @@ ], "item_tax_templates": [ { - "title": "Umsatzsteuer 19%", + "title": "19%", "taxes": [ { "tax_type": { - "account_name": "Umsatzsteuer 19%", - "account_number": "2301", + "account_name": "Umsatzsteuer 19 %", + "account_number": "2320", + "root_type": "Liability", "tax_rate": 19.00 }, "tax_rate": 19.00 }, { "tax_type": { - "account_name": "Umsatzsteuer 7%", - "account_number": "2302", + "account_name": "Umsatzsteuer 7 %", + "account_number": "2321", + "root_type": "Liability", "tax_rate": 7.00 }, "tax_rate": 0.00 - } - ] - }, - { - "title": "Umsatzsteuer 7%", - "taxes": [ - { - "tax_type": { - "account_name": "Umsatzsteuer 19%", - "account_number": "2301", - "tax_rate": 19.00 - }, - "tax_rate": 0.00 }, { "tax_type": { - "account_name": "Umsatzsteuer 7%", - "account_number": "2302", - "tax_rate": 7.00 + "account_name": "Umsatzsteuer aus innergemeinschaftlichen Erwerb 19 %", + "account_number": "2330", + "root_type": "Liability", + "tax_rate": 19.00 }, - "tax_rate": 7.00 - } - ] - }, - { - "title": "Vorsteuer 19%", - "taxes": [ + "tax_rate": 19.00 + }, { "tax_type": { - "account_name": "Abziehbare Vorsteuer 19%", - "account_number": "1501", + "account_name": "Umsatzsteuer aus innergemeinschaftlichen Erwerb 7 %", + "account_number": "2331", + "root_type": "Liability", + "tax_rate": 7.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Umsatzsteuer nach § 13b UStG 19%", + "account_number": "2340", + "root_type": "Liability", + "tax_rate": 19.00 + }, + "tax_rate": 19.00 + }, + { + "tax_type": { + "account_name": "Umsatzsteuer nach § 13b UStG 7%", + "account_number": "2341", + "root_type": "Liability", + "tax_rate": 7.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer 19 %", + "account_number": "1520", "root_type": "Asset", "tax_rate": 19.00 }, @@ -907,22 +2460,120 @@ }, { "tax_type": { - "account_name": "Abziehbare Vorsteuer 7%", - "account_number": "1502", + "account_name": "Abziehbare Vorsteuer 7 %", + "account_number": "1521", "root_type": "Asset", "tax_rate": 7.00 }, "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer aus innergemeinschaftlichen Erwerb 19 %", + "account_number": "1530", + "root_type": "Asset", + "tax_rate": 19.00 + }, + "tax_rate": 19.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer aus innergemeinschaftlichen Erwerb 7 %", + "account_number": "1531", + "root_type": "Asset", + "tax_rate": 7.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer nach § 13b UStG 19%", + "account_number": "1540", + "root_type": "Asset", + "tax_rate": 19.00 + }, + "tax_rate": 19.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer nach § 13b UStG 7%", + "account_number": "1541", + "root_type": "Asset", + "tax_rate": 7.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Entstandene Einfuhrumsatzsteuer", + "account_number": "1550", + "root_type": "Asset" + }, + "tax_rate": 19.00 } ] }, { - "title": "Vorsteuer 7%", + "title": "7%", "taxes": [ { "tax_type": { - "account_name": "Abziehbare Vorsteuer 19%", - "account_number": "1501", + "account_name": "Umsatzsteuer 19 %", + "account_number": "2320", + "root_type": "Liability", + "tax_rate": 19.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Umsatzsteuer 7 %", + "account_number": "2321", + "root_type": "Liability", + "tax_rate": 7.00 + }, + "tax_rate": 7.00 + }, + { + "tax_type": { + "account_name": "Umsatzsteuer aus innergemeinschaftlichen Erwerb 19 %", + "account_number": "2330", + "root_type": "Liability", + "tax_rate": 19.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Umsatzsteuer aus innergemeinschaftlichen Erwerb 7 %", + "account_number": "2331", + "root_type": "Liability", + "tax_rate": 7.00 + }, + "tax_rate": 7.00 + }, + { + "tax_type": { + "account_name": "Umsatzsteuer nach § 13b UStG 19%", + "account_number": "2340", + "root_type": "Liability", + "tax_rate": 19.00 + }, + "tax_rate": 19.00 + }, + { + "tax_type": { + "account_name": "Umsatzsteuer nach § 13b UStG 7%", + "account_number": "2341", + "root_type": "Liability", + "tax_rate": 7.00 + }, + "tax_rate": 7.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer 19 %", + "account_number": "1520", "root_type": "Asset", "tax_rate": 19.00 }, @@ -930,12 +2581,177 @@ }, { "tax_type": { - "account_name": "Abziehbare Vorsteuer 7%", - "account_number": "1502", + "account_name": "Abziehbare Vorsteuer 7 %", + "account_number": "1521", "root_type": "Asset", "tax_rate": 7.00 }, "tax_rate": 7.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer aus innergemeinschaftlichen Erwerb 19 %", + "account_number": "1530", + "root_type": "Asset", + "tax_rate": 19.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer aus innergemeinschaftlichen Erwerb 7 %", + "account_number": "1531", + "root_type": "Asset", + "tax_rate": 7.00 + }, + "tax_rate": 7.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer nach § 13b UStG 19%", + "account_number": "1540", + "root_type": "Asset", + "tax_rate": 19.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer nach § 13b UStG 7%", + "account_number": "1541", + "root_type": "Asset", + "tax_rate": 7.00 + }, + "tax_rate": 7.00 + }, + { + "tax_type": { + "account_name": "Entstandene Einfuhrumsatzsteuer", + "account_number": "1550", + "root_type": "Asset" + }, + "tax_rate": 7.00 + } + ] + }, + { + "title": "0 %", + "taxes": [ + { + "tax_type": { + "account_name": "Umsatzsteuer 19 %", + "account_number": "2320", + "root_type": "Liability", + "tax_rate": 19.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Umsatzsteuer 7 %", + "account_number": "2321", + "root_type": "Liability", + "tax_rate": 7.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Umsatzsteuer aus innergemeinschaftlichen Erwerb 19 %", + "account_number": "2330", + "root_type": "Liability", + "tax_rate": 19.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Umsatzsteuer aus innergemeinschaftlichen Erwerb 7 %", + "account_number": "2331", + "root_type": "Liability", + "tax_rate": 7.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Umsatzsteuer nach § 13b UStG 19%", + "account_number": "2340", + "root_type": "Liability", + "tax_rate": 19.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Umsatzsteuer nach § 13b UStG 7%", + "account_number": "2341", + "root_type": "Liability", + "tax_rate": 7.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer 19 %", + "account_number": "1520", + "root_type": "Asset", + "tax_rate": 19.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer 7 %", + "account_number": "1521", + "root_type": "Asset", + "tax_rate": 7.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer aus innergemeinschaftlichen Erwerb 19 %", + "account_number": "1530", + "root_type": "Asset", + "tax_rate": 19.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer aus innergemeinschaftlichen Erwerb 7 %", + "account_number": "1531", + "root_type": "Asset", + "tax_rate": 7.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer nach § 13b UStG 19%", + "account_number": "1540", + "root_type": "Asset", + "tax_rate": 19.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer nach § 13b UStG 7%", + "account_number": "1541", + "root_type": "Asset", + "tax_rate": 7.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Entstandene Einfuhrumsatzsteuer", + "account_number": "1550", + "root_type": "Asset" + }, + "tax_rate": 0.00 } ] } @@ -944,47 +2760,361 @@ "*": { "sales_tax_templates": [ { - "title": "Umsatzsteuer", - "tax_category": "Umsatzsteuer", + "title": "Lieferung oder sonstige Leistung im Inland", "is_default": 1, "taxes": [ { "account_head": { - "account_name": "Umsatzsteuer 19%", + "account_name": "Umsatzsteuer 19 %", "tax_rate": 19.00 }, + "description": "Umsatzsteuer 19 %", "rate": 0.00 }, { "account_head": { - "account_name": "Umsatzsteuer 7%", + "account_name": "Umsatzsteuer 7 %", "tax_rate": 7.00 }, + "description": "Umsatzsteuer 7 %", "rate": 0.00 } ] + }, + { + "title": "Lieferung an Unternehmen in der EU", + "is_default": 0, + "taxes": [] + }, + { + "title": "Sonstige Leistung an Unternehmen in der EU", + "is_default": 0, + "taxes": [] + }, + { + "title": "Lieferung oder sonstige Leistung an nicht-Unternehmen in der EU", + "is_default": 0, + "taxes": [ + { + "account_head": { + "account_name": "Umsatzsteuer 19 %", + "tax_rate": 19.00 + }, + "description": "Umsatzsteuer 19 %", + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Umsatzsteuer 7 %", + "tax_rate": 7.00 + }, + "description": "Umsatzsteuer 7 %", + "rate": 0.00 + } + ] + }, + { + "title": "Lieferung in Drittland", + "is_default": 0, + "taxes": [] + }, + { + "title": "Sonstige Leistung an Unternehmen in Drittland", + "is_default": 0, + "taxes": [] + }, + { + "title": "Sonstige Leistung an nicht-Unternehmen in Drittland", + "is_default": 0, + "taxes": [ + { + "account_head": { + "account_name": "Umsatzsteuer 19 %", + "tax_rate": 19.00 + }, + "description": "Umsatzsteuer 19 %", + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Umsatzsteuer 7 %", + "tax_rate": 7.00 + }, + "description": "Umsatzsteuer 7 %", + "rate": 0.00 + } + ] + }, + { + "title": "Bauleistungen nach § 13b UStG", + "is_default": 0, + "taxes": [] } ], "purchase_tax_templates": [ { - "title": "Vorsteuer 19%", - "tax_category": "Vorsteuer", + "title": "Lieferung aus dem Inland", "is_default": 1, "taxes": [ { "account_head": { - "account_name": "Abziehbare Vorsteuer 19%", - "tax_rate": 19.00, - "root_type": "Asset" + "account_name": "Abziehbare Vorsteuer 19 %", + "root_type": "Asset", + "tax_rate": 19.00 }, + "description": "Abziehbare Vorsteuer 19 %", "rate": 0.00 }, { "account_head": { - "account_name": "Abziehbare Vorsteuer 7%", + "account_name": "Abziehbare Vorsteuer 7 %", "root_type": "Asset", "tax_rate": 7.00 }, + "description": "Abziehbare Vorsteuer 7 %", + "rate": 0.00 + } + ] + }, + { + "title": "Sonstige Leistung aus dem Inland", + "is_default": 0, + "taxes": [ + { + "account_head": { + "account_name": "Abziehbare Vorsteuer 19 %", + "root_type": "Asset", + "tax_rate": 19.00 + }, + "description": "Abziehbare Vorsteuer 19 %", + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Abziehbare Vorsteuer 7 %", + "root_type": "Asset", + "tax_rate": 7.00 + }, + "description": "Abziehbare Vorsteuer 7 %", + "rate": 0.00 + } + ] + }, + { + "title": "Lieferung aus der EU", + "is_default": 0, + "taxes": [ + { + "account_head": { + "account_name": "Abziehbare Vorsteuer aus innergemeinschaftlichem Erwerb 19 %", + "root_type": "Asset", + "tax_rate": 19.00 + }, + "description": "Abziehbare Vorsteuer aus innergemeinschaftlichem Erwerb 19 %", + "add_deduct_tax": "Add", + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Abziehbare Vorsteuer aus innergemeinschaftlichem Erwerb", + "root_type": "Asset", + "tax_rate": 7.00 + }, + "description": "Abziehbare Vorsteuer aus innergemeinschaftlichem Erwerb", + "add_deduct_tax": "Add", + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Umsatzsteuer aus innergemeinschaftlichem Erwerb 19 %", + "root_type": "Liability", + "tax_rate": 19.00 + }, + "description": "Umsatzsteuer aus innergemeinschaftlichem Erwerb 19 %", + "add_deduct_tax": "Deduct", + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Umsatzsteuer aus innergemeinschaftlichem Erwerb", + "root_type": "Liability", + "tax_rate": 7.00 + }, + "description": "Umsatzsteuer aus innergemeinschaftlichem Erwerb", + "add_deduct_tax": "Deduct", + "rate": 0.00 + } + ] + }, + { + "title": "Sonstige Leistung aus der EU", + "is_default": 0, + "taxes": [ + { + "account_head": { + "account_name": "Abziehbare Vorsteuer nach § 13b UStG 19 %", + "root_type": "Asset", + "tax_rate": 19.00 + }, + "description": "Abziehbare Vorsteuer nach § 13b UStG 19 %", + "add_deduct_tax": "Add", + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Abziehbare Vorsteuer nach § 13b UStG", + "root_type": "Asset", + "tax_rate": 7.00 + }, + "description": "Abziehbare Vorsteuer nach § 13b UStG", + "add_deduct_tax": "Add", + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Umsatzsteuer nach § 13b UStG 19 %", + "root_type": "Liability", + "tax_rate": 19.00 + }, + "description": "Umsatzsteuer nach § 13b UStG 19 %", + "add_deduct_tax": "Deduct", + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Umsatzsteuer nach § 13b UStG", + "root_type": "Liability", + "tax_rate": 7.00 + }, + "description": "Umsatzsteuer nach § 13b UStG", + "add_deduct_tax": "Deduct", + "rate": 0.00 + } + ] + }, + { + "title": "Sonstige Leistung aus Drittland", + "is_default": 0, + "taxes": [ + { + "account_head": { + "account_name": "Abziehbare Vorsteuer nach § 13b UStG 19 %", + "root_type": "Asset", + "tax_rate": 19.00 + }, + "description": "Abziehbare Vorsteuer nach § 13b UStG 19 %", + "add_deduct_tax": "Add", + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Abziehbare Vorsteuer nach § 13b UStG", + "root_type": "Asset", + "tax_rate": 7.00 + }, + "description": "Abziehbare Vorsteuer nach § 13b UStG", + "add_deduct_tax": "Add", + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Umsatzsteuer nach § 13b UStG 19 %", + "root_type": "Liability", + "tax_rate": 19.00 + }, + "description": "Umsatzsteuer nach § 13b UStG 19 %", + "add_deduct_tax": "Deduct", + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Umsatzsteuer nach § 13b UStG", + "root_type": "Liability", + "tax_rate": 7.00 + }, + "description": "Umsatzsteuer nach § 13b UStG", + "add_deduct_tax": "Deduct", + "rate": 0.00 + } + ] + }, + { + "title": "Lieferung aus Drittland", + "is_default": 0, + "taxes": [ + { + "account_head": { + "account_name": "Abziehbare Vorsteuer 19 %", + "root_type": "Asset", + "tax_rate": 19.00 + }, + "description": "Abziehbare Vorsteuer 19 %", + "add_deduct_tax": "Add", + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Abziehbare Vorsteuer 7 %", + "root_type": "Asset", + "tax_rate": 7.00 + }, + "description": "Abziehbare Vorsteuer 7 %", + "add_deduct_tax": "Add", + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Entstandene Einfuhrumsatzsteuer", + "root_type": "Asset" + }, + "description": "Entstandene Einfuhrumsatzsteuer", + "add_deduct_tax": "Deduct", + "rate": 0.00 + } + ] + }, + { + "title": "Bauleistungen nach § 13b UStG", + "is_default": 0, + "taxes": [ + { + "account_head": { + "account_name": "Abziehbare Vorsteuer nach § 13b UStG 19 %", + "root_type": "Asset", + "tax_rate": 19.00 + }, + "description": "Abziehbare Vorsteuer nach § 13b UStG 19 %", + "add_deduct_tax": "Add", + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Abziehbare Vorsteuer nach § 13b UStG", + "root_type": "Asset", + "tax_rate": 7.00 + }, + "description": "Abziehbare Vorsteuer nach § 13b UStG", + "add_deduct_tax": "Add", + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Umsatzsteuer nach § 13b UStG 19 %", + "root_type": "Liability", + "tax_rate": 19.00 + }, + "description": "Umsatzsteuer nach § 13b UStG 19 %", + "add_deduct_tax": "Deduct", + "rate": 0.00 + }, + { + "account_head": { + "account_name": "Umsatzsteuer nach § 13b UStG", + "root_type": "Liability", + "tax_rate": 7.00 + }, + "description": "Umsatzsteuer nach § 13b UStG", + "add_deduct_tax": "Deduct", "rate": 0.00 } ] @@ -992,49 +3122,59 @@ ], "item_tax_templates": [ { - "title": "Umsatzsteuer 19%", + "title": "19 %", "taxes": [ { "tax_type": { - "account_name": "Umsatzsteuer 19%", + "account_name": "Umsatzsteuer 19 %", + "root_type": "Liability", "tax_rate": 19.00 }, "tax_rate": 19.00 }, { "tax_type": { - "account_name": "Umsatzsteuer 7%", + "account_name": "Umsatzsteuer 7 %", + "root_type": "Liability", "tax_rate": 7.00 }, "tax_rate": 0.00 - } - ] - }, - { - "title": "Umsatzsteuer 7%", - "taxes": [ - { - "tax_type": { - "account_name": "Umsatzsteuer 19%", - "tax_rate": 19.00 - }, - "tax_rate": 0.00 }, { "tax_type": { - "account_name": "Umsatzsteuer 7%", - "tax_rate": 7.00 + "account_name": "Umsatzsteuer aus innergemeinschaftlichem Erwerb 19 %", + "root_type": "Liability", + "tax_rate": 19.00 }, - "tax_rate": 7.00 - } - ] - }, - { - "title": "Vorsteuer 19%", - "taxes": [ + "tax_rate": 19.00 + }, { "tax_type": { - "account_name": "Abziehbare Vorsteuer 19%", + "account_name": "Umsatzsteuer aus innergemeinschaftlichem Erwerb", + "root_type": "Liability", + "tax_rate": 7.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Umsatzsteuer nach § 13b UStG 19 %", + "root_type": "Liability", + "tax_rate": 19.00 + }, + "tax_rate": 19.00 + }, + { + "tax_type": { + "account_name": "Umsatzsteuer nach § 13b UStG", + "root_type": "Liability", + "tax_rate": 7.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer 19 %", "root_type": "Asset", "tax_rate": 19.00 }, @@ -1042,20 +3182,107 @@ }, { "tax_type": { - "account_name": "Abziehbare Vorsteuer 7%", + "account_name": "Abziehbare Vorsteuer 7 %", "root_type": "Asset", "tax_rate": 7.00 }, "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer aus innergemeinschaftlichem Erwerb 19 %", + "root_type": "Asset", + "tax_rate": 19.00 + }, + "tax_rate": 19.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer aus innergemeinschaftlichem Erwerb", + "root_type": "Asset", + "tax_rate": 7.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer nach § 13b UStG 19 %", + "root_type": "Asset", + "tax_rate": 19.00 + }, + "tax_rate": 19.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer nach § 13b UStG", + "root_type": "Asset", + "tax_rate": 7.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Entstandene Einfuhrumsatzsteuer", + "root_type": "Asset" + }, + "tax_rate": 19.00 } ] }, { - "title": "Vorsteuer 7%", + "title": "7 %", "taxes": [ { "tax_type": { - "account_name": "Abziehbare Vorsteuer 19%", + "account_name": "Umsatzsteuer 19 %", + "root_type": "Liability", + "tax_rate": 19.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Umsatzsteuer 7 %", + "root_type": "Liability", + "tax_rate": 7.00 + }, + "tax_rate": 7.00 + }, + { + "tax_type": { + "account_name": "Umsatzsteuer aus innergemeinschaftlichem Erwerb 19 %", + "root_type": "Liability", + "tax_rate": 19.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Umsatzsteuer aus innergemeinschaftlichem Erwerb", + "root_type": "Liability", + "tax_rate": 7.00 + }, + "tax_rate": 7.00 + }, + { + "tax_type": { + "account_name": "Umsatzsteuer nach § 13b UStG 19 %", + "root_type": "Liability", + "tax_rate": 19.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Umsatzsteuer nach § 13b UStG", + "root_type": "Liability", + "tax_rate": 7.00 + }, + "tax_rate": 7.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer 19 %", "root_type": "Asset", "tax_rate": 19.00 }, @@ -1063,11 +3290,158 @@ }, { "tax_type": { - "account_name": "Abziehbare Vorsteuer 7%", + "account_name": "Abziehbare Vorsteuer 7 %", "root_type": "Asset", "tax_rate": 7.00 }, "tax_rate": 7.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer aus innergemeinschaftlichem Erwerb 19 %", + "root_type": "Asset", + "tax_rate": 19.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer aus innergemeinschaftlichem Erwerb", + "root_type": "Asset", + "tax_rate": 7.00 + }, + "tax_rate": 7.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer nach § 13b UStG 19 %", + "root_type": "Asset", + "tax_rate": 19.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer nach § 13b UStG", + "root_type": "Asset", + "tax_rate": 7.00 + }, + "tax_rate": 7.00 + }, + { + "tax_type": { + "account_name": "Entstandene Einfuhrumsatzsteuer", + "root_type": "Asset" + }, + "tax_rate": 7.00 + } + ] + }, + { + "title": "0%", + "taxes": [ + { + "tax_type": { + "account_name": "Umsatzsteuer 19 %", + "root_type": "Liability", + "tax_rate": 19.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Umsatzsteuer 7 %", + "root_type": "Liability", + "tax_rate": 7.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Umsatzsteuer aus innergemeinschaftlichem Erwerb 19 %", + "root_type": "Liability", + "tax_rate": 19.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Umsatzsteuer aus innergemeinschaftlichem Erwerb", + "root_type": "Liability", + "tax_rate": 7.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Umsatzsteuer nach § 13b UStG 19 %", + "root_type": "Liability", + "tax_rate": 19.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Umsatzsteuer nach § 13b UStG", + "root_type": "Liability", + "tax_rate": 7.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer 19 %", + "root_type": "Asset", + "tax_rate": 19.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer 7 %", + "root_type": "Asset", + "tax_rate": 7.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer aus innergemeinschaftlichem Erwerb 19 %", + "root_type": "Asset", + "tax_rate": 19.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer aus innergemeinschaftlichem Erwerb", + "root_type": "Asset", + "tax_rate": 7.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer nach § 13b UStG 19 %", + "root_type": "Asset", + "tax_rate": 19.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Abziehbare Vorsteuer nach § 13b UStG", + "root_type": "Asset", + "tax_rate": 7.00 + }, + "tax_rate": 0.00 + }, + { + "tax_type": { + "account_name": "Entstandene Einfuhrumsatzsteuer", + "root_type": "Asset" + }, + "tax_rate": 0.00 } ] } @@ -1641,34 +4015,6 @@ "tax_rate": 18.00 } }, - - "Saudi Arabia": { - "KSA VAT 15%": { - "account_name": "VAT 15%", - "tax_rate": 15.00 - }, - "KSA VAT 5%": { - "account_name": "VAT 5%", - "tax_rate": 5.00 - }, - "KSA VAT Zero": { - "account_name": "VAT Zero", - "tax_rate": 0.00 - }, - "KSA VAT Exempted": { - "account_name": "VAT Exempted", - "tax_rate": 0.00 - }, - "KSA Excise 50%": { - "account_name": "Excise 50%", - "tax_rate": 50.00 - }, - "KSA Excise 100%": { - "account_name": "Excise 100%", - "tax_rate": 100.00 - } - }, - "Serbia": { "Serbia Tax": { "account_name": "VAT", diff --git a/erpnext/setup/setup_wizard/data/designation.txt b/erpnext/setup/setup_wizard/data/designation.txt new file mode 100644 index 0000000000..4c6d7bdea8 --- /dev/null +++ b/erpnext/setup/setup_wizard/data/designation.txt @@ -0,0 +1,31 @@ +Accountant +Administrative Assistant +Administrative Officer +Analyst +Associate +Business Analyst +Business Development Manager +Consultant +Chief Executive Officer +Chief Financial Officer +Chief Operating Officer +Chief Technology Officer +Customer Service Representative +Designer +Engineer +Executive Assistant +Finance Manager +HR Manager +Head of Marketing and Sales +Manager +Managing Director +Marketing Manager +Marketing Specialist +President +Product Manager +Project Manager +Researcher +Sales Representative +Secretary +Software Developer +Vice President diff --git a/erpnext/setup/setup_wizard/data/industry_type.py b/erpnext/setup/setup_wizard/data/industry_type.py deleted file mode 100644 index 0bc3f32eb0..0000000000 --- a/erpnext/setup/setup_wizard/data/industry_type.py +++ /dev/null @@ -1,57 +0,0 @@ -from frappe import _ - - -def get_industry_types(): - return [ - _("Accounting"), - _("Advertising"), - _("Aerospace"), - _("Agriculture"), - _("Airline"), - _("Apparel & Accessories"), - _("Automotive"), - _("Banking"), - _("Biotechnology"), - _("Broadcasting"), - _("Brokerage"), - _("Chemical"), - _("Computer"), - _("Consulting"), - _("Consumer Products"), - _("Cosmetics"), - _("Defense"), - _("Department Stores"), - _("Education"), - _("Electronics"), - _("Energy"), - _("Entertainment & Leisure"), - _("Executive Search"), - _("Financial Services"), - _("Food, Beverage & Tobacco"), - _("Grocery"), - _("Health Care"), - _("Internet Publishing"), - _("Investment Banking"), - _("Legal"), - _("Manufacturing"), - _("Motion Picture & Video"), - _("Music"), - _("Newspaper Publishers"), - _("Online Auctions"), - _("Pension Funds"), - _("Pharmaceuticals"), - _("Private Equity"), - _("Publishing"), - _("Real Estate"), - _("Retail & Wholesale"), - _("Securities & Commodity Exchanges"), - _("Service"), - _("Soap & Detergent"), - _("Software"), - _("Sports"), - _("Technology"), - _("Telecommunications"), - _("Television"), - _("Transportation"), - _("Venture Capital"), - ] diff --git a/erpnext/setup/setup_wizard/data/industry_type.txt b/erpnext/setup/setup_wizard/data/industry_type.txt new file mode 100644 index 0000000000..eadc689e31 --- /dev/null +++ b/erpnext/setup/setup_wizard/data/industry_type.txt @@ -0,0 +1,51 @@ +Accounting +Advertising +Aerospace +Agriculture +Airline +Apparel & Accessories +Automotive +Banking +Biotechnology +Broadcasting +Brokerage +Chemical +Computer +Consulting +Consumer Products +Cosmetics +Defense +Department Stores +Education +Electronics +Energy +Entertainment & Leisure +Executive Search +Financial Services +Food, Beverage & Tobacco +Grocery +Health Care +Internet Publishing +Investment Banking +Legal +Manufacturing +Motion Picture & Video +Music +Newspaper Publishers +Online Auctions +Pension Funds +Pharmaceuticals +Private Equity +Publishing +Real Estate +Retail & Wholesale +Securities & Commodity Exchanges +Service +Soap & Detergent +Software +Sports +Technology +Telecommunications +Television +Transportation +Venture Capital diff --git a/erpnext/setup/setup_wizard/data/lead_source.txt b/erpnext/setup/setup_wizard/data/lead_source.txt new file mode 100644 index 0000000000..00ca1808bb --- /dev/null +++ b/erpnext/setup/setup_wizard/data/lead_source.txt @@ -0,0 +1,10 @@ +Existing Customer +Reference +Advertisement +Cold Calling +Exhibition +Supplier Reference +Mass Mailing +Customer's Vendor +Campaign +Walk In diff --git a/erpnext/setup/setup_wizard/data/sales_partner_type.txt b/erpnext/setup/setup_wizard/data/sales_partner_type.txt new file mode 100644 index 0000000000..68e9b9ac73 --- /dev/null +++ b/erpnext/setup/setup_wizard/data/sales_partner_type.txt @@ -0,0 +1,7 @@ +Channel Partner +Distributor +Dealer +Agent +Retailer +Implementation Partner +Reseller diff --git a/erpnext/setup/setup_wizard/data/sales_stage.txt b/erpnext/setup/setup_wizard/data/sales_stage.txt new file mode 100644 index 0000000000..2808ce7985 --- /dev/null +++ b/erpnext/setup/setup_wizard/data/sales_stage.txt @@ -0,0 +1,8 @@ +Prospecting +Qualification +Needs Analysis +Value Proposition +Identifying Decision Makers +Perception Analysis +Proposal/Price Quote +Negotiation/Review diff --git a/erpnext/setup/setup_wizard/operations/company_setup.py b/erpnext/setup/setup_wizard/operations/company_setup.py index aadc98989f..ace5cca0b0 100644 --- a/erpnext/setup/setup_wizard/operations/company_setup.py +++ b/erpnext/setup/setup_wizard/operations/company_setup.py @@ -4,7 +4,6 @@ import frappe from frappe import _ from frappe.utils import cstr, getdate -from .default_website import website_maker def create_fiscal_year_and_company(args): @@ -48,83 +47,6 @@ def enable_shopping_cart(args): # nosemgrep ).insert() -def create_email_digest(): - from frappe.utils.user import get_system_managers - - system_managers = get_system_managers(only_name=True) - - if not system_managers: - return - - recipients = [] - for d in system_managers: - recipients.append({"recipient": d}) - - companies = frappe.db.sql_list("select name FROM `tabCompany`") - for company in companies: - if not frappe.db.exists("Email Digest", "Default Weekly Digest - " + company): - edigest = frappe.get_doc( - { - "doctype": "Email Digest", - "name": "Default Weekly Digest - " + company, - "company": company, - "frequency": "Weekly", - "recipients": recipients, - } - ) - - for df in edigest.meta.get("fields", {"fieldtype": "Check"}): - if df.fieldname != "scheduler_errors": - edigest.set(df.fieldname, 1) - - edigest.insert() - - # scheduler errors digest - if companies: - edigest = frappe.new_doc("Email Digest") - edigest.update( - { - "name": "Scheduler Errors", - "company": companies[0], - "frequency": "Daily", - "recipients": recipients, - "scheduler_errors": 1, - "enabled": 1, - } - ) - edigest.insert() - - -def create_logo(args): - if args.get("attach_logo"): - attach_logo = args.get("attach_logo").split(",") - if len(attach_logo) == 3: - filename, filetype, content = attach_logo - _file = frappe.get_doc( - { - "doctype": "File", - "file_name": filename, - "attached_to_doctype": "Website Settings", - "attached_to_name": "Website Settings", - "decode": True, - } - ) - _file.save() - fileurl = _file.file_url - frappe.db.set_value( - "Website Settings", - "Website Settings", - "brand_html", - " {1}".format( - fileurl, args.get("company_name") - ), - ) - - -def create_website(args): - website_maker(args) - - def get_fy_details(fy_start_date, fy_end_date): start_year = getdate(fy_start_date).year if start_year == getdate(fy_end_date).year: diff --git a/erpnext/setup/setup_wizard/operations/default_website.py b/erpnext/setup/setup_wizard/operations/default_website.py deleted file mode 100644 index 40b02b35df..0000000000 --- a/erpnext/setup/setup_wizard/operations/default_website.py +++ /dev/null @@ -1,89 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# License: GNU General Public License v3. See license.txt - - -import frappe -from frappe import _ -from frappe.utils import nowdate - - -class website_maker(object): - def __init__(self, args): - self.args = args - self.company = args.company_name - self.tagline = args.company_tagline - self.user = args.get("email") - self.make_web_page() - self.make_website_settings() - self.make_blog() - - def make_web_page(self): - # home page - homepage = frappe.get_doc("Homepage", "Homepage") - homepage.company = self.company - homepage.tag_line = self.tagline - homepage.setup_items() - homepage.save() - - def make_website_settings(self): - # update in home page in settings - website_settings = frappe.get_doc("Website Settings", "Website Settings") - website_settings.home_page = "home" - website_settings.brand_html = self.company - website_settings.copyright = self.company - website_settings.top_bar_items = [] - website_settings.append( - "top_bar_items", {"doctype": "Top Bar Item", "label": "Contact", "url": "/contact"} - ) - website_settings.append( - "top_bar_items", {"doctype": "Top Bar Item", "label": "Blog", "url": "/blog"} - ) - website_settings.append( - "top_bar_items", {"doctype": "Top Bar Item", "label": _("Products"), "url": "/all-products"} - ) - website_settings.save() - - def make_blog(self): - blog_category = frappe.get_doc( - {"doctype": "Blog Category", "category_name": "general", "published": 1, "title": _("General")} - ).insert() - - if not self.user: - # Admin setup - return - - blogger = frappe.new_doc("Blogger") - user = frappe.get_doc("User", self.user) - blogger.user = self.user - blogger.full_name = user.first_name + (" " + user.last_name if user.last_name else "") - blogger.short_name = user.first_name.lower() - blogger.avatar = user.user_image - blogger.insert() - - frappe.get_doc( - { - "doctype": "Blog Post", - "title": "Welcome", - "published": 1, - "published_on": nowdate(), - "blogger": blogger.name, - "blog_category": blog_category.name, - "blog_intro": "My First Blog", - "content": frappe.get_template("setup/setup_wizard/data/sample_blog_post.html").render(), - } - ).insert() - - -def test(): - frappe.delete_doc("Web Page", "test-company") - frappe.delete_doc("Blog Post", "welcome") - frappe.delete_doc("Blogger", "administrator") - frappe.delete_doc("Blog Category", "general") - website_maker( - { - "company": "Test Company", - "company_tagline": "Better Tools for Everyone", - "name": "Administrator", - } - ) - frappe.db.commit() diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py index 4d9b871e5e..6bc17718ae 100644 --- a/erpnext/setup/setup_wizard/operations/install_fixtures.py +++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py @@ -4,6 +4,7 @@ import json import os +from pathlib import Path import frappe from frappe import _ @@ -16,28 +17,10 @@ from frappe.utils import cstr, getdate from erpnext.accounts.doctype.account.account import RootNotEditable from erpnext.regional.address_template.setup import set_up_address_templates -default_lead_sources = [ - "Existing Customer", - "Reference", - "Advertisement", - "Cold Calling", - "Exhibition", - "Supplier Reference", - "Mass Mailing", - "Customer's Vendor", - "Campaign", - "Walk In", -] -default_sales_partner_type = [ - "Channel Partner", - "Distributor", - "Dealer", - "Agent", - "Retailer", - "Implementation Partner", - "Reseller", -] +def read_lines(filename: str) -> list[str]: + """Return a list of lines from a file in the data directory.""" + return (Path(__file__).parent.parent / "data" / filename).read_text().splitlines() def install(country=None): @@ -85,7 +68,11 @@ def install(country=None): # Stock Entry Type {"doctype": "Stock Entry Type", "name": "Material Issue", "purpose": "Material Issue"}, {"doctype": "Stock Entry Type", "name": "Material Receipt", "purpose": "Material Receipt"}, - {"doctype": "Stock Entry Type", "name": "Material Transfer", "purpose": "Material Transfer"}, + { + "doctype": "Stock Entry Type", + "name": "Material Transfer", + "purpose": "Material Transfer", + }, {"doctype": "Stock Entry Type", "name": "Manufacture", "purpose": "Manufacture"}, {"doctype": "Stock Entry Type", "name": "Repack", "purpose": "Repack"}, { @@ -103,22 +90,6 @@ def install(country=None): "name": "Material Consumption for Manufacture", "purpose": "Material Consumption for Manufacture", }, - # Designation - {"doctype": "Designation", "designation_name": _("CEO")}, - {"doctype": "Designation", "designation_name": _("Manager")}, - {"doctype": "Designation", "designation_name": _("Analyst")}, - {"doctype": "Designation", "designation_name": _("Engineer")}, - {"doctype": "Designation", "designation_name": _("Accountant")}, - {"doctype": "Designation", "designation_name": _("Secretary")}, - {"doctype": "Designation", "designation_name": _("Associate")}, - {"doctype": "Designation", "designation_name": _("Administrative Officer")}, - {"doctype": "Designation", "designation_name": _("Business Development Manager")}, - {"doctype": "Designation", "designation_name": _("HR Manager")}, - {"doctype": "Designation", "designation_name": _("Project Manager")}, - {"doctype": "Designation", "designation_name": _("Head of Marketing and Sales")}, - {"doctype": "Designation", "designation_name": _("Software Developer")}, - {"doctype": "Designation", "designation_name": _("Designer")}, - {"doctype": "Designation", "designation_name": _("Researcher")}, # territory: with two default territories, one for home country and one named Rest of the World { "doctype": "Territory", @@ -291,28 +262,18 @@ def install(country=None): {"doctype": "Market Segment", "market_segment": _("Lower Income")}, {"doctype": "Market Segment", "market_segment": _("Middle Income")}, {"doctype": "Market Segment", "market_segment": _("Upper Income")}, - # Sales Stages - {"doctype": "Sales Stage", "stage_name": _("Prospecting")}, - {"doctype": "Sales Stage", "stage_name": _("Qualification")}, - {"doctype": "Sales Stage", "stage_name": _("Needs Analysis")}, - {"doctype": "Sales Stage", "stage_name": _("Value Proposition")}, - {"doctype": "Sales Stage", "stage_name": _("Identifying Decision Makers")}, - {"doctype": "Sales Stage", "stage_name": _("Perception Analysis")}, - {"doctype": "Sales Stage", "stage_name": _("Proposal/Price Quote")}, - {"doctype": "Sales Stage", "stage_name": _("Negotiation/Review")}, # Warehouse Type {"doctype": "Warehouse Type", "name": "Transit"}, ] - from erpnext.setup.setup_wizard.data.industry_type import get_industry_types - - records += [{"doctype": "Industry Type", "industry": d} for d in get_industry_types()] - # records += [{"doctype":"Operation", "operation": d} for d in get_operations()] - records += [{"doctype": "Lead Source", "source_name": _(d)} for d in default_lead_sources] - - records += [ - {"doctype": "Sales Partner Type", "sales_partner_type": _(d)} for d in default_sales_partner_type - ] + for doctype, title_field, filename in ( + ("Designation", "designation_name", "designation.txt"), + ("Sales Stage", "stage_name", "sales_stage.txt"), + ("Industry Type", "industry", "industry_type.txt"), + ("Lead Source", "source_name", "lead_source.txt"), + ("Sales Partner Type", "sales_partner_type", "sales_partner_type.txt"), + ): + records += [{"doctype": doctype, title_field: title} for title in read_lines(filename)] base_path = frappe.get_app_path("erpnext", "stock", "doctype") response = frappe.read_file( @@ -335,16 +296,11 @@ def install(country=None): make_default_records() make_records(records) set_up_address_templates(default_country=country) - set_more_defaults() - update_global_search_doctypes() - - -def set_more_defaults(): - # Do more setup stuff that can be done here with no dependencies update_selling_defaults() update_buying_defaults() add_uom_data() update_item_variant_settings() + update_global_search_doctypes() def update_selling_defaults(): @@ -381,7 +337,7 @@ def add_uom_data(): ) for d in uoms: if not frappe.db.exists("UOM", _(d.get("uom_name"))): - uom_doc = frappe.get_doc( + frappe.get_doc( { "doctype": "UOM", "uom_name": _(d.get("uom_name")), @@ -402,9 +358,10 @@ def add_uom_data(): frappe.get_doc({"doctype": "UOM Category", "category_name": _(d.get("category"))}).db_insert() if not frappe.db.exists( - "UOM Conversion Factor", {"from_uom": _(d.get("from_uom")), "to_uom": _(d.get("to_uom"))} + "UOM Conversion Factor", + {"from_uom": _(d.get("from_uom")), "to_uom": _(d.get("to_uom"))}, ): - uom_conversion = frappe.get_doc( + frappe.get_doc( { "doctype": "UOM Conversion Factor", "category": _(d.get("category")), @@ -412,7 +369,7 @@ def add_uom_data(): "to_uom": _(d.get("to_uom")), "value": d.get("value"), } - ).insert(ignore_permissions=True) + ).db_insert() def add_market_segments(): @@ -468,7 +425,7 @@ def install_company(args): make_records(records) -def install_defaults(args=None): +def install_defaults(args=None): # nosemgrep records = [ # Price Lists { @@ -493,7 +450,7 @@ def install_defaults(args=None): # enable default currency frappe.db.set_value("Currency", args.get("currency"), "enabled", 1) - frappe.db.set_value("Stock Settings", None, "email_footer_address", args.get("company_name")) + frappe.db.set_single_value("Stock Settings", "email_footer_address", args.get("company_name")) set_global_defaults(args) update_stock_settings() @@ -540,7 +497,8 @@ def create_bank_account(args): company_name = args.get("company_name") bank_account_group = frappe.db.get_value( - "Account", {"account_type": "Bank", "is_group": 1, "root_type": "Asset", "company": company_name} + "Account", + {"account_type": "Bank", "is_group": 1, "root_type": "Asset", "company": company_name}, ) if bank_account_group: bank_account = frappe.get_doc( diff --git a/erpnext/setup/setup_wizard/operations/taxes_setup.py b/erpnext/setup/setup_wizard/operations/taxes_setup.py index 2f77dd6ae5..49ba78c63a 100644 --- a/erpnext/setup/setup_wizard/operations/taxes_setup.py +++ b/erpnext/setup/setup_wizard/operations/taxes_setup.py @@ -158,6 +158,7 @@ def make_taxes_and_charges_template(company_name, doctype, template): # Ingone validations to make doctypes faster doc.flags.ignore_links = True doc.flags.ignore_validate = True + doc.flags.ignore_mandatory = True doc.insert(ignore_permissions=True) return doc diff --git a/erpnext/setup/setup_wizard/setup_wizard.py b/erpnext/setup/setup_wizard/setup_wizard.py index bd86a5b969..65b268e385 100644 --- a/erpnext/setup/setup_wizard/setup_wizard.py +++ b/erpnext/setup/setup_wizard/setup_wizard.py @@ -5,7 +5,6 @@ import frappe from frappe import _ -from .operations import company_setup from .operations import install_fixtures as fixtures @@ -35,7 +34,6 @@ def get_setup_stages(args=None): "fail_msg": "Failed to set defaults", "tasks": [ {"fn": setup_defaults, "args": args, "fail_msg": _("Failed to setup defaults")}, - {"fn": stage_four, "args": args, "fail_msg": _("Failed to create website")}, ], }, { @@ -60,12 +58,6 @@ def setup_defaults(args): fixtures.install_defaults(frappe._dict(args)) -def stage_four(args): - company_setup.create_website(args) - company_setup.create_email_digest() - company_setup.create_logo(args) - - def fin(args): frappe.local.message_log = [] login_as_first_user(args) @@ -81,5 +73,4 @@ def setup_complete(args=None): stage_fixtures(args) setup_company(args) setup_defaults(args) - stage_four(args) fin(args) diff --git a/erpnext/setup/utils.py b/erpnext/setup/utils.py index 54bd8c355d..bab57fe267 100644 --- a/erpnext/setup/utils.py +++ b/erpnext/setup/utils.py @@ -81,6 +81,11 @@ def get_exchange_rate(from_currency, to_currency, transaction_date=None, args=No if entries: return flt(entries[0].exchange_rate) + if frappe.get_cached_value( + "Currency Exchange Settings", "Currency Exchange Settings", "disabled" + ): + return 0.00 + try: cache = frappe.cache() key = "currency_exchange_rate_{0}:{1}:{2}".format(transaction_date, from_currency, to_currency) diff --git a/erpnext/startup/boot.py b/erpnext/startup/boot.py index bb120eaa6b..62936fcfb8 100644 --- a/erpnext/startup/boot.py +++ b/erpnext/startup/boot.py @@ -25,6 +25,12 @@ def boot_session(bootinfo): frappe.db.get_single_value("CRM Settings", "default_valid_till") ) + bootinfo.sysdefaults.allow_sales_order_creation_for_expired_quotation = cint( + frappe.db.get_single_value( + "Selling Settings", "allow_sales_order_creation_for_expired_quotation" + ) + ) + # if no company, show a dialog box to create a new company bootinfo.customer_count = frappe.db.sql("""SELECT count(*) FROM `tabCustomer`""")[0][0] diff --git a/erpnext/startup/report_data_map.py b/erpnext/startup/report_data_map.py deleted file mode 100644 index f8c1b6cca0..0000000000 --- a/erpnext/startup/report_data_map.py +++ /dev/null @@ -1,327 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# License: GNU General Public License v3. See license.txt - - -# mappings for table dumps -# "remember to add indexes!" - -data_map = { - "Company": {"columns": ["name"], "conditions": ["docstatus < 2"]}, - "Fiscal Year": { - "columns": ["name", "year_start_date", "year_end_date"], - "conditions": ["docstatus < 2"], - }, - # Accounts - "Account": { - "columns": ["name", "parent_account", "lft", "rgt", "report_type", "company", "is_group"], - "conditions": ["docstatus < 2"], - "order_by": "lft", - "links": { - "company": ["Company", "name"], - }, - }, - "Cost Center": { - "columns": ["name", "lft", "rgt"], - "conditions": ["docstatus < 2"], - "order_by": "lft", - }, - "GL Entry": { - "columns": [ - "name", - "account", - "posting_date", - "cost_center", - "debit", - "credit", - "is_opening", - "company", - "voucher_type", - "voucher_no", - "remarks", - ], - "order_by": "posting_date, account", - "links": { - "account": ["Account", "name"], - "company": ["Company", "name"], - "cost_center": ["Cost Center", "name"], - }, - }, - # Stock - "Item": { - "columns": [ - "name", - "if(item_name=name, '', item_name) as item_name", - "description", - "item_group as parent_item_group", - "stock_uom", - "brand", - "valuation_method", - ], - # "conditions": ["docstatus < 2"], - "order_by": "name", - "links": {"parent_item_group": ["Item Group", "name"], "brand": ["Brand", "name"]}, - }, - "Item Group": { - "columns": ["name", "parent_item_group"], - # "conditions": ["docstatus < 2"], - "order_by": "lft", - }, - "Brand": {"columns": ["name"], "conditions": ["docstatus < 2"], "order_by": "name"}, - "Project": {"columns": ["name"], "conditions": ["docstatus < 2"], "order_by": "name"}, - "Warehouse": {"columns": ["name"], "conditions": ["docstatus < 2"], "order_by": "name"}, - "Stock Ledger Entry": { - "columns": [ - "name", - "posting_date", - "posting_time", - "item_code", - "warehouse", - "actual_qty as qty", - "voucher_type", - "voucher_no", - "project", - "incoming_rate as incoming_rate", - "stock_uom", - "serial_no", - "qty_after_transaction", - "valuation_rate", - ], - "order_by": "posting_date, posting_time, creation", - "links": { - "item_code": ["Item", "name"], - "warehouse": ["Warehouse", "name"], - "project": ["Project", "name"], - }, - "force_index": "posting_sort_index", - }, - "Serial No": { - "columns": ["name", "purchase_rate as incoming_rate"], - "conditions": ["docstatus < 2"], - "order_by": "name", - }, - "Stock Entry": { - "columns": ["name", "purpose"], - "conditions": ["docstatus=1"], - "order_by": "posting_date, posting_time, name", - }, - "Material Request Item": { - "columns": ["item.name as name", "item_code", "warehouse", "(qty - ordered_qty) as qty"], - "from": "`tabMaterial Request Item` item, `tabMaterial Request` main", - "conditions": [ - "item.parent = main.name", - "main.docstatus=1", - "main.status != 'Stopped'", - "ifnull(warehouse, '')!=''", - "qty > ordered_qty", - ], - "links": {"item_code": ["Item", "name"], "warehouse": ["Warehouse", "name"]}, - }, - "Purchase Order Item": { - "columns": [ - "item.name as name", - "item_code", - "warehouse", - "(qty - received_qty)*conversion_factor as qty", - ], - "from": "`tabPurchase Order Item` item, `tabPurchase Order` main", - "conditions": [ - "item.parent = main.name", - "main.docstatus=1", - "main.status != 'Stopped'", - "ifnull(warehouse, '')!=''", - "qty > received_qty", - ], - "links": {"item_code": ["Item", "name"], "warehouse": ["Warehouse", "name"]}, - }, - "Sales Order Item": { - "columns": [ - "item.name as name", - "item_code", - "(qty - delivered_qty)*conversion_factor as qty", - "warehouse", - ], - "from": "`tabSales Order Item` item, `tabSales Order` main", - "conditions": [ - "item.parent = main.name", - "main.docstatus=1", - "main.status != 'Stopped'", - "ifnull(warehouse, '')!=''", - "qty > delivered_qty", - ], - "links": {"item_code": ["Item", "name"], "warehouse": ["Warehouse", "name"]}, - }, - # Sales - "Customer": { - "columns": [ - "name", - "if(customer_name=name, '', customer_name) as customer_name", - "customer_group as parent_customer_group", - "territory as parent_territory", - ], - "conditions": ["docstatus < 2"], - "order_by": "name", - "links": { - "parent_customer_group": ["Customer Group", "name"], - "parent_territory": ["Territory", "name"], - }, - }, - "Customer Group": { - "columns": ["name", "parent_customer_group"], - "conditions": ["docstatus < 2"], - "order_by": "lft", - }, - "Territory": { - "columns": ["name", "parent_territory"], - "conditions": ["docstatus < 2"], - "order_by": "lft", - }, - "Sales Invoice": { - "columns": ["name", "customer", "posting_date", "company"], - "conditions": ["docstatus=1"], - "order_by": "posting_date", - "links": {"customer": ["Customer", "name"], "company": ["Company", "name"]}, - }, - "Sales Invoice Item": { - "columns": ["name", "parent", "item_code", "stock_qty as qty", "base_net_amount"], - "conditions": ["docstatus=1", "ifnull(parent, '')!=''"], - "order_by": "parent", - "links": {"parent": ["Sales Invoice", "name"], "item_code": ["Item", "name"]}, - }, - "Sales Order": { - "columns": ["name", "customer", "transaction_date as posting_date", "company"], - "conditions": ["docstatus=1"], - "order_by": "transaction_date", - "links": {"customer": ["Customer", "name"], "company": ["Company", "name"]}, - }, - "Sales Order Item[Sales Analytics]": { - "columns": ["name", "parent", "item_code", "stock_qty as qty", "base_net_amount"], - "conditions": ["docstatus=1", "ifnull(parent, '')!=''"], - "order_by": "parent", - "links": {"parent": ["Sales Order", "name"], "item_code": ["Item", "name"]}, - }, - "Delivery Note": { - "columns": ["name", "customer", "posting_date", "company"], - "conditions": ["docstatus=1"], - "order_by": "posting_date", - "links": {"customer": ["Customer", "name"], "company": ["Company", "name"]}, - }, - "Delivery Note Item[Sales Analytics]": { - "columns": ["name", "parent", "item_code", "stock_qty as qty", "base_net_amount"], - "conditions": ["docstatus=1", "ifnull(parent, '')!=''"], - "order_by": "parent", - "links": {"parent": ["Delivery Note", "name"], "item_code": ["Item", "name"]}, - }, - "Supplier": { - "columns": [ - "name", - "if(supplier_name=name, '', supplier_name) as supplier_name", - "supplier_group as parent_supplier_group", - ], - "conditions": ["docstatus < 2"], - "order_by": "name", - "links": { - "parent_supplier_group": ["Supplier Group", "name"], - }, - }, - "Supplier Group": { - "columns": ["name", "parent_supplier_group"], - "conditions": ["docstatus < 2"], - "order_by": "name", - }, - "Purchase Invoice": { - "columns": ["name", "supplier", "posting_date", "company"], - "conditions": ["docstatus=1"], - "order_by": "posting_date", - "links": {"supplier": ["Supplier", "name"], "company": ["Company", "name"]}, - }, - "Purchase Invoice Item": { - "columns": ["name", "parent", "item_code", "stock_qty as qty", "base_net_amount"], - "conditions": ["docstatus=1", "ifnull(parent, '')!=''"], - "order_by": "parent", - "links": {"parent": ["Purchase Invoice", "name"], "item_code": ["Item", "name"]}, - }, - "Purchase Order": { - "columns": ["name", "supplier", "transaction_date as posting_date", "company"], - "conditions": ["docstatus=1"], - "order_by": "posting_date", - "links": {"supplier": ["Supplier", "name"], "company": ["Company", "name"]}, - }, - "Purchase Order Item[Purchase Analytics]": { - "columns": ["name", "parent", "item_code", "stock_qty as qty", "base_net_amount"], - "conditions": ["docstatus=1", "ifnull(parent, '')!=''"], - "order_by": "parent", - "links": {"parent": ["Purchase Order", "name"], "item_code": ["Item", "name"]}, - }, - "Purchase Receipt": { - "columns": ["name", "supplier", "posting_date", "company"], - "conditions": ["docstatus=1"], - "order_by": "posting_date", - "links": {"supplier": ["Supplier", "name"], "company": ["Company", "name"]}, - }, - "Purchase Receipt Item[Purchase Analytics]": { - "columns": ["name", "parent", "item_code", "stock_qty as qty", "base_net_amount"], - "conditions": ["docstatus=1", "ifnull(parent, '')!=''"], - "order_by": "parent", - "links": {"parent": ["Purchase Receipt", "name"], "item_code": ["Item", "name"]}, - }, - # Support - "Issue": { - "columns": ["name", "status", "creation", "resolution_date", "first_responded_on"], - "conditions": ["docstatus < 2"], - "order_by": "creation", - }, - # Manufacturing - "Work Order": { - "columns": [ - "name", - "status", - "creation", - "planned_start_date", - "planned_end_date", - "status", - "actual_start_date", - "actual_end_date", - "modified", - ], - "conditions": ["docstatus = 1"], - "order_by": "creation", - }, - # Medical - "Patient": { - "columns": [ - "name", - "creation", - "owner", - "if(patient_name=name, '', patient_name) as patient_name", - ], - "conditions": ["docstatus < 2"], - "order_by": "name", - "links": {"owner": ["User", "name"]}, - }, - "Patient Appointment": { - "columns": [ - "name", - "appointment_type", - "patient", - "practitioner", - "appointment_date", - "department", - "status", - "company", - ], - "order_by": "name", - "links": { - "practitioner": ["Healthcare Practitioner", "name"], - "appointment_type": ["Appointment Type", "name"], - }, - }, - "Healthcare Practitioner": { - "columns": ["name", "department"], - "order_by": "name", - "links": { - "department": ["Department", "name"], - }, - }, - "Appointment Type": {"columns": ["name"], "order_by": "name"}, - "Medical Department": {"columns": ["name"], "order_by": "name"}, -} diff --git a/erpnext/stock/dashboard/item_dashboard.js b/erpnext/stock/dashboard/item_dashboard.js index 6e7622c067..b09b715275 100644 --- a/erpnext/stock/dashboard/item_dashboard.js +++ b/erpnext/stock/dashboard/item_dashboard.js @@ -42,7 +42,7 @@ erpnext.stock.ItemDashboard = class ItemDashboard { let warehouse = unescape(element.attr('data-warehouse')); let actual_qty = unescape(element.attr('data-actual_qty')); let disable_quick_entry = Number(unescape(element.attr('data-disable_quick_entry'))); - let entry_type = action === "Move" ? "Material Transfer" : null; + let entry_type = action === "Move" ? "Material Transfer" : "Material Receipt"; if (disable_quick_entry) { open_stock_entry(item, warehouse, entry_type); @@ -63,11 +63,19 @@ erpnext.stock.ItemDashboard = class ItemDashboard { function open_stock_entry(item, warehouse, entry_type) { frappe.model.with_doctype('Stock Entry', function () { var doc = frappe.model.get_new_doc('Stock Entry'); - if (entry_type) doc.stock_entry_type = entry_type; + if (entry_type) { + doc.stock_entry_type = entry_type; + } var row = frappe.model.add_child(doc, 'items'); row.item_code = item; - row.s_warehouse = warehouse; + + if (entry_type === "Material Transfer") { + row.s_warehouse = warehouse; + } + else { + row.t_warehouse = warehouse; + } frappe.set_route('Form', doc.doctype, doc.name); }); @@ -102,6 +110,9 @@ erpnext.stock.ItemDashboard = class ItemDashboard { args: args, callback: function (r) { me.render(r.message); + if(me.after_refresh) { + me.after_refresh(); + } } }); } diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index 52854a0f01..f14288beb2 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -291,7 +291,7 @@ def get_batch_no(item_code, warehouse, qty=1, throw=False, serial_no=None): batches = get_batches(item_code, warehouse, qty, throw, serial_no) for batch in batches: - if cint(qty) <= cint(batch.qty): + if flt(qty) <= flt(batch.qty): batch_no = batch.batch_id break diff --git a/erpnext/stock/doctype/batch/test_batch.py b/erpnext/stock/doctype/batch/test_batch.py index 3e470d4ce4..271e2e0298 100644 --- a/erpnext/stock/doctype/batch/test_batch.py +++ b/erpnext/stock/doctype/batch/test_batch.py @@ -473,7 +473,13 @@ def make_new_batch(**args): "doctype": "Batch", "batch_id": args.batch_id, "item": args.item_code, + "expiry_date": args.expiry_date, } - ).insert() + ) + + if args.expiry_date: + batch.expiry_date = args.expiry_date + + batch.insert() return batch diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py index 548df318fa..72654e6f81 100644 --- a/erpnext/stock/doctype/bin/bin.py +++ b/erpnext/stock/doctype/bin/bin.py @@ -37,8 +37,10 @@ class Bin(Document): self.set_projected_qty() - self.db_set("reserved_qty_for_production", flt(self.reserved_qty_for_production)) - self.db_set("projected_qty", self.projected_qty) + self.db_set( + "reserved_qty_for_production", flt(self.reserved_qty_for_production), update_modified=True + ) + self.db_set("projected_qty", self.projected_qty, update_modified=True) def update_reserved_qty_for_sub_contracting(self, subcontract_doctype="Subcontracting Order"): # reserved qty @@ -118,9 +120,9 @@ class Bin(Document): else: reserved_qty_for_sub_contract = 0 - self.db_set("reserved_qty_for_sub_contract", reserved_qty_for_sub_contract) + self.db_set("reserved_qty_for_sub_contract", reserved_qty_for_sub_contract, update_modified=True) self.set_projected_qty() - self.db_set("projected_qty", self.projected_qty) + self.db_set("projected_qty", self.projected_qty, update_modified=True) def on_doctype_update(): @@ -157,12 +159,18 @@ def update_qty(bin_name, args): last_sle_qty = ( frappe.qb.from_(sle) .select(sle.qty_after_transaction) - .where((sle.item_code == args.get("item_code")) & (sle.warehouse == args.get("warehouse"))) + .where( + (sle.item_code == args.get("item_code")) + & (sle.warehouse == args.get("warehouse")) + & (sle.is_cancelled == 0) + ) .orderby(CombineDatetime(sle.posting_date, sle.posting_time), order=Order.desc) .orderby(sle.creation, order=Order.desc) + .limit(1) .run() ) + actual_qty = 0.0 if last_sle_qty: actual_qty = last_sle_qty[0][0] @@ -193,4 +201,5 @@ def update_qty(bin_name, args): "planned_qty": planned_qty, "projected_qty": projected_qty, }, + update_modified=True, ) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js index ea3cf1948b..ae56645b73 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note.js @@ -97,12 +97,12 @@ frappe.ui.form.on("Delivery Note", { } if (frm.doc.docstatus == 1 && !frm.doc.inter_company_reference) { - let internal = me.frm.doc.is_internal_customer; + let internal = frm.doc.is_internal_customer; if (internal) { - let button_label = (me.frm.doc.company === me.frm.doc.represents_company) ? "Internal Purchase Receipt" : + let button_label = (frm.doc.company === frm.doc.represents_company) ? "Internal Purchase Receipt" : "Inter Company Purchase Receipt"; - me.frm.add_custom_button(button_label, function() { + frm.add_custom_button(__(button_label), function() { frappe.model.open_mapped_doc({ method: 'erpnext.stock.doctype.delivery_note.delivery_note.make_inter_company_purchase_receipt', frm: frm, diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json index f9e934921d..0c1f82029e 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.json +++ b/erpnext/stock/doctype/delivery_note/delivery_note.json @@ -8,46 +8,29 @@ "document_type": "Document", "engine": "InnoDB", "field_order": [ - "delivery_to_section", - "column_break0", "title", "naming_series", "customer", + "tax_id", "customer_name", "column_break1", - "amended_from", - "company", "posting_date", "posting_time", "set_posting_time", + "column_break_10", + "company", + "amended_from", "is_return", "issue_credit_note", "return_against", "accounting_dimensions_section", "cost_center", - "dimension_col_break", + "column_break_18", "project", - "customer_po_details", - "po_no", - "column_break_17", - "po_date", - "section_break_18", - "pick_list", - "contact_info", - "shipping_address_name", - "shipping_address", - "dispatch_address_name", - "dispatch_address", - "contact_person", - "contact_display", - "contact_mobile", - "contact_email", - "col_break21", - "customer_address", - "tax_id", - "address_display", - "company_address", - "company_address_display", + "dimension_col_break", + "campaign", + "source", + "custom_dimensions_section", "currency_and_price_list", "currency", "conversion_rate", @@ -56,45 +39,37 @@ "price_list_currency", "plc_conversion_rate", "ignore_pricing_rule", - "sec_warehouse", - "set_warehouse", - "col_break_warehouse", - "set_target_warehouse", "items_section", "scan_barcode", + "pick_list", + "col_break_warehouse", + "set_warehouse", + "set_target_warehouse", + "section_break_30", "items", - "pricing_rule_details", - "pricing_rules", - "packing_list", - "packed_items", - "product_bundle_help", "section_break_31", "total_qty", + "total_net_weight", + "column_break_35", "base_total", "base_net_total", "column_break_33", - "total_net_weight", "total", "net_total", "taxes_section", "tax_category", - "column_break_39", - "shipping_rule", - "section_break_41", "taxes_and_charges", + "column_break_43", + "shipping_rule", + "column_break_39", + "incoterm", + "named_place", + "section_break_41", "taxes", - "sec_tax_breakup", - "other_charges_calculation", "section_break_44", "base_total_taxes_and_charges", "column_break_47", "total_taxes_and_charges", - "section_break_49", - "apply_discount_on", - "base_discount_amount", - "column_break_51", - "additional_discount_percentage", - "discount_amount", "totals", "base_grand_total", "base_rounding_adjustment", @@ -106,9 +81,50 @@ "rounded_total", "in_words", "disable_rounded_total", - "terms_section_break", + "section_break_49", + "apply_discount_on", + "base_discount_amount", + "column_break_51", + "additional_discount_percentage", + "discount_amount", + "sec_tax_breakup", + "other_charges_calculation", + "packing_list", + "packed_items", + "product_bundle_help", + "pricing_rule_details", + "pricing_rules", + "address_and_contact_tab", + "contact_info", + "customer_address", + "address_display", + "col_break21", + "contact_person", + "contact_display", + "contact_mobile", + "contact_email", + "shipping_address_section", + "shipping_address_name", + "shipping_address", + "column_break_95", + "dispatch_address_name", + "dispatch_address", + "company_address_section", + "company_address", + "column_break_101", + "company_address_display", + "terms_tab", "tc_name", "terms", + "more_info_tab", + "section_break_83", + "per_billed", + "status", + "column_break_112", + "per_installed", + "installation_status", + "column_break_89", + "per_returned", "transporter_info", "transporter", "driver", @@ -118,56 +134,40 @@ "transporter_name", "driver_name", "lr_date", - "more_info", - "campaign", - "source", - "column_break5", - "is_internal_customer", - "represents_company", - "inter_company_reference", - "per_billed", - "customer_group", - "territory", - "printing_details", - "letter_head", - "select_print_heading", - "language", - "column_break_88", - "print_without_amount", - "group_same_items", - "section_break_83", - "status", - "per_installed", - "installation_status", - "column_break_89", - "per_returned", - "excise_page", - "instructions", - "subscription_section", - "auto_repeat", + "customer_po_details", + "po_no", + "column_break_17", + "po_date", "sales_team_section_break", "sales_partner", - "column_break7", "amount_eligible_for_commission", + "column_break7", "commission_rate", "total_commission", "section_break1", - "sales_team" + "sales_team", + "subscription_section", + "auto_repeat", + "printing_details", + "letter_head", + "print_without_amount", + "group_same_items", + "column_break_88", + "select_print_heading", + "language", + "more_info", + "is_internal_customer", + "represents_company", + "inter_company_reference", + "customer_group", + "territory", + "column_break5", + "excise_page", + "instructions", + "connections_tab", + "column_break_25" ], "fields": [ - { - "fieldname": "delivery_to_section", - "fieldtype": "Section Break", - "label": "Delivery To", - "options": "fa fa-user" - }, - { - "fieldname": "column_break0", - "fieldtype": "Column Break", - "oldfieldtype": "Column Break", - "print_width": "50%", - "width": "50%" - }, { "allow_on_submit": 1, "default": "{customer_name}", @@ -337,11 +337,10 @@ "width": "100px" }, { - "collapsible": 1, "depends_on": "customer", "fieldname": "contact_info", "fieldtype": "Section Break", - "label": "Address and Contact", + "label": "Billing Address", "options": "fa fa-bullhorn" }, { @@ -490,14 +489,9 @@ "fieldname": "ignore_pricing_rule", "fieldtype": "Check", "label": "Ignore Pricing Rule", - "no_copy": 1, "permlevel": 1, "print_hide": 1 }, - { - "fieldname": "sec_warehouse", - "fieldtype": "Section Break" - }, { "fieldname": "set_warehouse", "fieldtype": "Link", @@ -512,6 +506,8 @@ { "fieldname": "items_section", "fieldtype": "Section Break", + "hide_border": 1, + "label": "Items", "oldfieldtype": "Section Break", "options": "fa fa-shopping-cart" }, @@ -525,7 +521,7 @@ "allow_bulk_edit": 1, "fieldname": "items", "fieldtype": "Table", - "label": "Items", + "label": "Delivery Note Item", "oldfieldname": "delivery_note_details", "oldfieldtype": "Table", "options": "Delivery Note Item", @@ -620,6 +616,7 @@ "read_only": 1 }, { + "depends_on": "total_net_weight", "fieldname": "total_net_weight", "fieldtype": "Float", "label": "Total Net Weight", @@ -629,6 +626,7 @@ { "fieldname": "taxes_section", "fieldtype": "Section Break", + "hide_border": 1, "label": "Taxes and Charges", "oldfieldtype": "Section Break", "options": "fa fa-money" @@ -654,10 +652,10 @@ }, { "fieldname": "section_break_41", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "hide_border": 1 }, { - "description": "If you have created a standard template in Sales Taxes and Charges Template, select one and click on the button below.", "fieldname": "taxes_and_charges", "fieldtype": "Link", "label": "Sales Taxes and Charges Template", @@ -719,7 +717,6 @@ }, { "collapsible": 1, - "collapsible_depends_on": "discount_amount", "fieldname": "section_break_49", "fieldtype": "Section Break", "label": "Additional Discount" @@ -760,6 +757,7 @@ { "fieldname": "totals", "fieldtype": "Section Break", + "label": "Totals", "oldfieldtype": "Section Break", "options": "fa fa-money" }, @@ -864,15 +862,6 @@ "read_only": 1, "width": "150px" }, - { - "collapsible": 1, - "collapsible_depends_on": "terms", - "fieldname": "terms_section_break", - "fieldtype": "Section Break", - "label": "Terms and Conditions", - "oldfieldtype": "Section Break", - "options": "fa fa-legal" - }, { "fieldname": "tc_name", "fieldtype": "Link", @@ -966,13 +955,12 @@ "collapsible": 1, "fieldname": "more_info", "fieldtype": "Section Break", - "label": "More Information", + "label": "Additional Info", "oldfieldtype": "Section Break", "options": "fa fa-file-text", "print_hide": 1 }, { - "description": "Track this Delivery Note against any Project", "fieldname": "project", "fieldtype": "Link", "label": "Project", @@ -1111,7 +1099,6 @@ }, { "depends_on": "eval:!doc.__islocal", - "description": "% of materials delivered against this Delivery Note", "fieldname": "per_installed", "fieldtype": "Percent", "in_list_view": 1, @@ -1238,10 +1225,6 @@ "options": "Pick List", "read_only": 1 }, - { - "fieldname": "section_break_18", - "fieldtype": "Section Break" - }, { "default": "0", "fetch_from": "customer.is_internal_customer", @@ -1330,13 +1313,97 @@ { "fieldname": "dimension_col_break", "fieldtype": "Column Break" + }, + { + "fieldname": "address_and_contact_tab", + "fieldtype": "Tab Break", + "label": "Address & Contact" + }, + { + "fieldname": "terms_tab", + "fieldtype": "Tab Break", + "label": "Terms" + }, + { + "fieldname": "more_info_tab", + "fieldtype": "Tab Break", + "label": "More Info" + }, + { + "fieldname": "connections_tab", + "fieldtype": "Tab Break", + "label": "Connections", + "show_dashboard": 1 + }, + { + "fieldname": "column_break_10", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_25", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_30", + "fieldtype": "Section Break", + "hide_border": 1 + }, + { + "fieldname": "column_break_35", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_43", + "fieldtype": "Column Break" + }, + { + "fieldname": "custom_dimensions_section", + "fieldtype": "Section Break" + }, + { + "fieldname": "shipping_address_section", + "fieldtype": "Section Break", + "label": "Shipping Address" + }, + { + "fieldname": "column_break_95", + "fieldtype": "Column Break" + }, + { + "fieldname": "company_address_section", + "fieldtype": "Section Break", + "label": "Company Address" + }, + { + "fieldname": "column_break_101", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_112", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_18", + "fieldtype": "Column Break" + }, + { + "fieldname": "incoterm", + "fieldtype": "Link", + "label": "Incoterm", + "options": "Incoterm" + }, + { + "depends_on": "incoterm", + "fieldname": "named_place", + "fieldtype": "Data", + "label": "Named Place" } ], "icon": "fa fa-truck", "idx": 146, "is_submittable": 1, "links": [], - "modified": "2022-06-10 03:52:04.197415", + "modified": "2023-02-14 04:45:44.179670", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note", diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 0e68e85806..9f9f5cbe2a 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -178,6 +178,7 @@ class DeliveryNote(SellingController): if ( cint(frappe.db.get_single_value("Selling Settings", "maintain_same_sales_rate")) and not self.is_return + and not self.is_internal_customer ): self.validate_rate_with_reference_doc( [ @@ -227,6 +228,7 @@ class DeliveryNote(SellingController): def on_submit(self): self.validate_packed_qty() + self.update_pick_list_status() # Check for Approving Authority frappe.get_doc("Authorization Control").validate_approving_authority( @@ -312,6 +314,11 @@ class DeliveryNote(SellingController): if has_error: raise frappe.ValidationError + def update_pick_list_status(self): + from erpnext.stock.doctype.pick_list.pick_list import update_pick_list_status + + update_pick_list_status(self.pick_list) + def check_next_docstatus(self): submit_rv = frappe.db.sql( """select t1.name @@ -841,6 +848,9 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): update_address( target_doc, "shipping_address", "shipping_address_display", source_doc.customer_address ) + update_address( + target_doc, "billing_address", "billing_address_display", source_doc.customer_address + ) update_taxes( target_doc, @@ -896,6 +906,10 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): "name": "delivery_note_item", "batch_no": "batch_no", "serial_no": "serial_no", + "purchase_order": "purchase_order", + "purchase_order_item": "purchase_order_item", + "material_request": "material_request", + "Material_request_item": "material_request_item", }, "field_no_map": ["warehouse"], }, diff --git a/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py b/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py index fd44e9cee5..b6b5ff4296 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py @@ -11,11 +11,14 @@ def get_data(): }, "internal_links": { "Sales Order": ["items", "against_sales_order"], + "Material Request": ["items", "material_request"], + "Purchase Order": ["items", "purchase_order"], }, "transactions": [ {"label": _("Related"), "items": ["Sales Invoice", "Packing Slip", "Delivery Trip"]}, {"label": _("Reference"), "items": ["Sales Order", "Shipment", "Quality Inspection"]}, {"label": _("Returns"), "items": ["Stock Entry"]}, {"label": _("Subscription"), "items": ["Auto Repeat"]}, + {"label": _("Internal Transfer"), "items": ["Material Request", "Purchase Order"]}, ], } diff --git a/erpnext/stock/doctype/delivery_note/delivery_note_list.js b/erpnext/stock/doctype/delivery_note/delivery_note_list.js index 9e6f3bc932..6ff3ed3e8e 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note_list.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note_list.js @@ -14,7 +14,7 @@ frappe.listview_settings['Delivery Note'] = { return [__("Completed"), "green", "per_billed,=,100"]; } }, - onload: function (listview) { + onload: function (doclist) { const action = () => { const selected_docs = doclist.get_checked_items(); const docnames = doclist.get_checked_items(true); @@ -56,14 +56,14 @@ frappe.listview_settings['Delivery Note'] = { // doclist.page.add_actions_menu_item(__('Create Delivery Trip'), action, false); - listview.page.add_action_item(__('Create Delivery Trip'), action); + doclist.page.add_action_item(__('Create Delivery Trip'), action); - listview.page.add_action_item(__("Sales Invoice"), ()=>{ - erpnext.bulk_transaction_processing.create(listview, "Delivery Note", "Sales Invoice"); + doclist.page.add_action_item(__("Sales Invoice"), ()=>{ + erpnext.bulk_transaction_processing.create(doclist, "Delivery Note", "Sales Invoice"); }); - listview.page.add_action_item(__("Packaging Slip From Delivery Note"), ()=>{ - erpnext.bulk_transaction_processing.create(listview, "Delivery Note", "Packing Slip"); + doclist.page.add_action_item(__("Packaging Slip From Delivery Note"), ()=>{ + erpnext.bulk_transaction_processing.create(doclist, "Delivery Note", "Packing Slip"); }); } }; diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 6bcab737b3..903e2af3cb 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -6,7 +6,7 @@ import json import frappe from frappe.tests.utils import FrappeTestCase -from frappe.utils import cstr, flt, nowdate, nowtime +from frappe.utils import add_days, cstr, flt, nowdate, nowtime, today from erpnext.accounts.doctype.account.test_account import get_inventory_account from erpnext.accounts.utils import get_balance_on @@ -490,6 +490,46 @@ class TestDeliveryNote(FrappeTestCase): self.assertEqual(gle_warehouse_amount, 1400) + def test_bin_details_of_packed_item(self): + from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle + from erpnext.stock.doctype.item.test_item import make_item + + # test Update Items with product bundle + if not frappe.db.exists("Item", "_Test Product Bundle Item New"): + bundle_item = make_item("_Test Product Bundle Item New", {"is_stock_item": 0}) + bundle_item.append( + "item_defaults", {"company": "_Test Company", "default_warehouse": "_Test Warehouse - _TC"} + ) + bundle_item.save(ignore_permissions=True) + + make_item("_Packed Item New 1", {"is_stock_item": 1}) + make_product_bundle("_Test Product Bundle Item New", ["_Packed Item New 1"], 2) + + si = create_delivery_note( + item_code="_Test Product Bundle Item New", + update_stock=1, + warehouse="_Test Warehouse - _TC", + transaction_date=add_days(nowdate(), -1), + do_not_submit=1, + ) + + make_stock_entry(item="_Packed Item New 1", target="_Test Warehouse - _TC", qty=120, rate=100) + + bin_details = frappe.db.get_value( + "Bin", + {"item_code": "_Packed Item New 1", "warehouse": "_Test Warehouse - _TC"}, + ["actual_qty", "projected_qty", "ordered_qty"], + as_dict=1, + ) + + si.transaction_date = nowdate() + si.save() + + packed_item = si.packed_items[0] + self.assertEqual(flt(bin_details.actual_qty), flt(packed_item.actual_qty)) + self.assertEqual(flt(bin_details.projected_qty), flt(packed_item.projected_qty)) + self.assertEqual(flt(bin_details.ordered_qty), flt(packed_item.ordered_qty)) + def test_return_for_serialized_items(self): se = make_serialized_item() serial_no = get_serial_nos(se.get("items")[0].serial_no)[0] @@ -650,6 +690,11 @@ class TestDeliveryNote(FrappeTestCase): update_delivery_note_status(dn.name, "Closed") self.assertEqual(frappe.db.get_value("Delivery Note", dn.name, "Status"), "Closed") + # Check cancelling closed delivery note + dn.load_from_db() + dn.cancel() + self.assertEqual(dn.status, "Cancelled") + def test_dn_billing_status_case1(self): # SO -> DN -> SI so = make_sales_order() @@ -1050,9 +1095,22 @@ class TestDeliveryNote(FrappeTestCase): do_not_submit=True, ) + dn.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": "_Test Account Service Tax - _TC", + "description": "Tax 1", + "rate": 14, + "cost_center": "_Test Cost Center - _TC", + "included_in_print_rate": 1, + }, + ) + self.assertEqual(dn.items[0].rate, 500) # haven't saved yet dn.save() self.assertEqual(dn.ignore_pricing_rule, 1) + self.assertEqual(dn.taxes[0].included_in_print_rate, 0) # rate should reset to incoming rate self.assertEqual(dn.items[0].rate, rate) @@ -1063,6 +1121,7 @@ class TestDeliveryNote(FrappeTestCase): dn.save() self.assertEqual(dn.items[0].rate, rate) + self.assertEqual(dn.items[0].net_rate, rate) def test_internal_transfer_precision_gle(self): from erpnext.selling.doctype.customer.test_customer import create_internal_customer @@ -1091,6 +1150,36 @@ class TestDeliveryNote(FrappeTestCase): frappe.db.exists("GL Entry", {"voucher_no": dn.name, "voucher_type": dn.doctype}) ) + def test_batch_expiry_for_delivery_note(self): + from erpnext.controllers.sales_and_purchase_return import make_return_doc + from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt + + item = make_item( + "_Test Batch Item For Return Check", + { + "is_purchase_item": 1, + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "TBIRC.#####", + }, + ) + + pi = make_purchase_receipt(qty=1, item_code=item.name) + + dn = create_delivery_note(qty=1, item_code=item.name, batch_no=pi.items[0].batch_no) + + dn.load_from_db() + batch_no = dn.items[0].batch_no + self.assertTrue(batch_no) + + frappe.db.set_value("Batch", batch_no, "expiry_date", add_days(today(), -1)) + + return_dn = make_return_doc(dn.doctype, dn.name) + return_dn.save().submit() + + self.assertTrue(return_dn.docstatus == 1) + def create_delivery_note(**args): dn = frappe.new_doc("Delivery Note") @@ -1117,6 +1206,7 @@ def create_delivery_note(**args): "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, + "batch_no": args.batch_no or None, "target_warehouse": args.target_warehouse, }, ) 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 2de4842ebe..916ab2a05b 100644 --- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json +++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json @@ -8,6 +8,7 @@ "engine": "InnoDB", "field_order": [ "barcode", + "has_item_scanned", "item_code", "item_name", "col_break1", @@ -86,6 +87,12 @@ "expense_account", "allow_zero_valuation_rate", "column_break_71", + "internal_transfer_section", + "material_request", + "purchase_order", + "column_break_82", + "purchase_order_item", + "material_request_item", "accounting_dimensions_section", "cost_center", "dimension_col_break", @@ -257,6 +264,7 @@ }, { "collapsible": 1, + "collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount", "fieldname": "discount_and_margin", "fieldtype": "Section Break", "label": "Discount and Margin" @@ -749,6 +757,7 @@ "fieldtype": "Currency", "label": "Incoming Rate", "no_copy": 1, + "precision": "6", "print_hide": 1, "read_only": 1 }, @@ -777,13 +786,58 @@ "no_copy": 1, "print_hide": 1, "read_only": 1 + }, + { + "collapsible": 1, + "depends_on": "eval:parent.is_internal_customer == 1", + "fieldname": "internal_transfer_section", + "fieldtype": "Section Break", + "label": "Internal Transfer" + }, + { + "fieldname": "purchase_order", + "fieldtype": "Link", + "label": "Purchase Order", + "options": "Purchase Order", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "column_break_82", + "fieldtype": "Column Break" + }, + { + "fieldname": "purchase_order_item", + "fieldtype": "Data", + "label": "Purchase Order Item", + "print_hide": 1, + "read_only": 1 + }, + { + "default": "0", + "depends_on": "barcode", + "fieldname": "has_item_scanned", + "fieldtype": "Check", + "label": "Has Item Scanned", + "read_only": 1 + }, + { + "fieldname": "material_request", + "fieldtype": "Link", + "label": "Material Request", + "options": "Material Request" + }, + { + "fieldname": "material_request_item", + "fieldtype": "Data", + "label": "Material Request Item" } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-06-17 05:25:47.711177", + "modified": "2022-11-09 12:17:50.850142", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note Item", diff --git a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.js b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.js index 91a21f4e72..0310682a2c 100644 --- a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.js +++ b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.js @@ -30,19 +30,71 @@ frappe.ui.form.on('Inventory Dimension', { onload(frm) { frm.trigger('render_traget_field'); + frm.trigger("set_parent_fields"); }, refresh(frm) { if (frm.doc.__onload && frm.doc.__onload.has_stock_ledger && frm.doc.__onload.has_stock_ledger.length) { - let msg = __('Stock transactions exists against this dimension, user can not update document.'); - frm.dashboard.add_comment(msg, 'blue', true); + let allow_to_edit_fields = ['disabled', 'fetch_from_parent', + 'type_of_transaction', 'condition', 'mandatory_depends_on']; frm.fields.forEach((field) => { - if (field.df.fieldname !== 'disabled') { + if (!in_list(allow_to_edit_fields, field.df.fieldname)) { frm.set_df_property(field.df.fieldname, "read_only", "1"); } }); } + + if (!frm.is_new()) { + frm.add_custom_button(__('Delete Dimension'), () => { + frm.trigger('delete_dimension'); + }); + } + }, + + document_type(frm) { + frm.trigger("set_parent_fields"); + }, + + set_parent_fields(frm) { + if (frm.doc.apply_to_all_doctypes) { + frm.set_df_property("fetch_from_parent", "options", frm.doc.reference_document); + } else if (frm.doc.document_type && frm.doc.istable) { + frappe.call({ + method: 'erpnext.stock.doctype.inventory_dimension.inventory_dimension.get_parent_fields', + args: { + child_doctype: frm.doc.document_type, + dimension_name: frm.doc.reference_document + }, + callback: (r) => { + if (r.message && r.message.length) { + frm.set_df_property("fetch_from_parent", "options", + [""].concat(r.message)); + } else { + frm.set_df_property("fetch_from_parent", "hidden", 1); + } + } + }); + } + }, + + delete_dimension(frm) { + let msg = (` + Custom fields related to this dimension will be deleted on deletion of dimension. +
Do you want to delete {0} dimension? + `); + + frappe.confirm(__(msg, [frm.doc.name.bold()]), () => { + frappe.call({ + method: 'erpnext.stock.doctype.inventory_dimension.inventory_dimension.delete_dimension', + args: { + dimension: frm.doc.name + }, + callback: function() { + frappe.set_route('List', 'Inventory Dimension'); + } + }); + }); } }); diff --git a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.json b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.json index 8b334d13d7..eb6102a436 100644 --- a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.json +++ b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.json @@ -1,6 +1,5 @@ { "actions": [], - "allow_rename": 1, "autoname": "field:dimension_name", "creation": "2022-06-17 13:04:16.554051", "doctype": "DocType", @@ -12,19 +11,23 @@ "reference_document", "column_break_4", "disabled", - "section_break_7", "field_mapping_section", "source_fieldname", "column_break_9", "target_fieldname", "applicable_for_documents_tab", "apply_to_all_doctypes", + "column_break_13", "document_type", - "istable", "type_of_transaction", - "column_break_16", - "condition", + "fetch_from_parent", + "istable", "applicable_condition_example_section", + "condition", + "conditional_mandatory_section", + "reqd", + "mandatory_depends_on", + "conditional_rule_examples_section", "html_19" ], "fields": [ @@ -52,13 +55,13 @@ { "fieldname": "applicable_for_documents_tab", "fieldtype": "Tab Break", - "label": "Applicable For Documents" + "label": "Applicable For" }, { "depends_on": "eval:!doc.apply_to_all_doctypes", "fieldname": "document_type", "fieldtype": "Link", - "label": "Applicable to Document", + "label": "Apply to Document", "mandatory_depends_on": "eval:!doc.apply_to_all_doctypes", "options": "DocType" }, @@ -72,6 +75,7 @@ "fetch_from": "document_type.istable", "fieldname": "istable", "fieldtype": "Check", + "hidden": 1, "label": " Is Child Table", "read_only": 1 }, @@ -79,13 +83,13 @@ "depends_on": "eval:!doc.apply_to_all_doctypes", "fieldname": "condition", "fieldtype": "Code", - "label": "Applicable Condition" + "label": "Conditional Rule" }, { - "default": "0", + "default": "1", "fieldname": "apply_to_all_doctypes", "fieldtype": "Check", - "label": "Apply to All Inventory Document Types" + "label": "Apply to All Inventory Documents" }, { "default": "0", @@ -93,37 +97,33 @@ "fieldtype": "Check", "label": "Disabled" }, - { - "fieldname": "section_break_7", - "fieldtype": "Section Break" - }, { "fieldname": "target_fieldname", "fieldtype": "Data", "label": "Target Fieldname (Stock Ledger Entry)", + "no_copy": 1, "read_only": 1 }, { "fieldname": "source_fieldname", "fieldtype": "Data", "label": "Source Fieldname", + "no_copy": 1, "read_only": 1 }, { "collapsible": 1, "fieldname": "field_mapping_section", "fieldtype": "Section Break", + "hidden": 1, "label": "Field Mapping" }, { - "fieldname": "column_break_16", - "fieldtype": "Column Break" - }, - { + "depends_on": "eval:!doc.apply_to_all_doctypes", "fieldname": "type_of_transaction", "fieldtype": "Select", "label": "Type of Transaction", - "options": "\nInward\nOutward" + "options": "\nInward\nOutward\nBoth" }, { "fieldname": "html_19", @@ -134,17 +134,50 @@ "collapsible": 1, "depends_on": "eval:!doc.apply_to_all_doctypes", "fieldname": "applicable_condition_example_section", - "fieldtype": "Section Break", - "label": "Applicable Condition Examples" + "fieldtype": "Column Break" }, { "fieldname": "column_break_4", "fieldtype": "Column Break" + }, + { + "depends_on": "eval:!doc.apply_to_all_doctypes", + "description": "Set fieldname from which you want to fetch the data from the parent form.", + "fieldname": "fetch_from_parent", + "fieldtype": "Select", + "label": "Fetch Value From" + }, + { + "fieldname": "column_break_13", + "fieldtype": "Section Break" + }, + { + "depends_on": "eval:!doc.apply_to_all_doctypes", + "fieldname": "conditional_rule_examples_section", + "fieldtype": "Section Break", + "label": "Conditional Rule Examples" + }, + { + "description": "To apply condition on parent field use parent.field_name and to apply condition on child table use doc.field_name. Here field_name could be based on the actual column name of the respective field.", + "fieldname": "mandatory_depends_on", + "fieldtype": "Small Text", + "label": "Mandatory Depends On" + }, + { + "fieldname": "conditional_mandatory_section", + "fieldtype": "Section Break", + "label": "Mandatory Section" + }, + { + "default": "0", + "fieldname": "reqd", + "fieldtype": "Check", + "label": "Mandatory" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2022-07-19 21:06:11.824976", + "modified": "2023-01-31 13:44:38.507698", "modified_by": "Administrator", "module": "Stock", "name": "Inventory Dimension", diff --git a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py index 5a9541f060..db2b5d0a6b 100644 --- a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py +++ b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py @@ -33,23 +33,59 @@ class InventoryDimension(Document): ) def validate(self): + self.validate_reference_document() + + def before_save(self): self.do_not_update_document() self.reset_value() - self.validate_reference_document() self.set_source_and_target_fieldname() + self.set_type_of_transaction() + self.set_fetch_value_from() + + def set_type_of_transaction(self): + if self.apply_to_all_doctypes: + self.type_of_transaction = "Both" + + def set_fetch_value_from(self): + if self.apply_to_all_doctypes: + self.fetch_from_parent = self.reference_document def do_not_update_document(self): if self.is_new() or not self.has_stock_ledger(): return old_doc = self._doc_before_save + allow_to_edit_fields = [ + "disabled", + "fetch_from_parent", + "type_of_transaction", + "condition", + ] + for field in frappe.get_meta("Inventory Dimension").fields: - if field.fieldname != "disabled" and old_doc.get(field.fieldname) != self.get(field.fieldname): + if field.fieldname not in allow_to_edit_fields and old_doc.get(field.fieldname) != self.get( + field.fieldname + ): msg = f"""The user can not change value of the field {bold(field.label)} because stock transactions exists against the dimension {bold(self.name)}.""" frappe.throw(_(msg), DoNotChangeError) + def on_trash(self): + self.delete_custom_fields() + + def delete_custom_fields(self): + filters = {"fieldname": self.source_fieldname} + + if self.document_type: + filters["dt"] = self.document_type + + for field in frappe.get_all("Custom Field", filters=filters): + frappe.delete_doc("Custom Field", field.name) + + msg = f"Deleted custom fields related to the dimension {self.name}" + frappe.msgprint(_(msg)) + def reset_value(self): if self.apply_to_all_doctypes: self.istable = 0 @@ -76,34 +112,49 @@ class InventoryDimension(Document): self.add_custom_fields() def add_custom_fields(self): - dimension_field = dict( - fieldname=self.source_fieldname, - fieldtype="Link", - insert_after="warehouse", - options=self.reference_document, - label=self.dimension_name, - ) + dimension_fields = [ + dict( + fieldname="inventory_dimension", + fieldtype="Section Break", + insert_after="warehouse", + label="Inventory Dimension", + collapsible=1, + ), + dict( + fieldname=self.source_fieldname, + fieldtype="Link", + insert_after="inventory_dimension", + options=self.reference_document, + label=self.dimension_name, + reqd=self.reqd, + mandatory_depends_on=self.mandatory_depends_on, + ), + ] custom_fields = {} if self.apply_to_all_doctypes: for doctype in get_inventory_documents(): - if not frappe.db.get_value( - "Custom Field", {"dt": doctype[0], "fieldname": self.source_fieldname} - ): - custom_fields.setdefault(doctype[0], dimension_field) - elif not frappe.db.get_value( - "Custom Field", {"dt": self.document_type, "fieldname": self.source_fieldname} - ): - custom_fields.setdefault(self.document_type, dimension_field) + if not field_exists(doctype[0], self.source_fieldname): + custom_fields.setdefault(doctype[0], dimension_fields) + elif not field_exists(self.document_type, self.source_fieldname): + custom_fields.setdefault(self.document_type, dimension_fields) if not frappe.db.get_value( "Custom Field", {"dt": "Stock Ledger Entry", "fieldname": self.target_fieldname} - ): + ) and not field_exists("Stock Ledger Entry", self.target_fieldname): + dimension_field = dimension_fields[1] + dimension_field["mandatory_depends_on"] = "" + dimension_field["reqd"] = 0 dimension_field["fieldname"] = self.target_fieldname custom_fields["Stock Ledger Entry"] = dimension_field - create_custom_fields(custom_fields) + if custom_fields: + create_custom_fields(custom_fields) + + +def field_exists(doctype, fieldname) -> str or None: + return frappe.db.get_value("DocField", {"parent": doctype, "fieldname": fieldname}, "name") @frappe.whitelist() @@ -143,7 +194,7 @@ def get_evaluated_inventory_dimension(doc, sl_dict, parent_doc=None): elif ( row.type_of_transaction == "Outward" if doc.docstatus == 1 - else row.type_of_transaction != "Inward" + else row.type_of_transaction != "Outward" ) and sl_dict.actual_qty > 0: continue @@ -166,7 +217,14 @@ def get_document_wise_inventory_dimensions(doctype) -> dict: if not frappe.local.document_wise_inventory_dimensions.get(doctype): dimensions = frappe.get_all( "Inventory Dimension", - fields=["name", "source_fieldname", "condition", "target_fieldname", "type_of_transaction"], + fields=[ + "name", + "source_fieldname", + "condition", + "target_fieldname", + "type_of_transaction", + "fetch_from_parent", + ], filters={"disabled": 0}, or_filters={"document_type": doctype, "apply_to_all_doctypes": 1}, ) @@ -194,3 +252,36 @@ def get_inventory_dimensions(): frappe.local.inventory_dimensions = dimensions return frappe.local.inventory_dimensions + + +@frappe.whitelist() +def delete_dimension(dimension): + doc = frappe.get_doc("Inventory Dimension", dimension) + doc.delete() + + +@frappe.whitelist() +def get_parent_fields(child_doctype, dimension_name): + parent_doctypes = frappe.get_all( + "DocField", fields=["parent"], filters={"options": child_doctype} + ) + + fields = [] + + fields.extend( + frappe.get_all( + "DocField", + fields=["fieldname as value", "label"], + filters={"options": dimension_name, "parent": ("in", [d.parent for d in parent_doctypes])}, + ) + ) + + fields.extend( + frappe.get_all( + "Custom Field", + fields=["fieldname as value", "label"], + filters={"options": dimension_name, "dt": ("in", [d.parent for d in parent_doctypes])}, + ) + ) + + return fields diff --git a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py index 998a0e9d6a..28b1ed96f0 100644 --- a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py +++ b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py @@ -2,13 +2,17 @@ # See license.txt import frappe +from frappe.custom.doctype.custom_field.custom_field import create_custom_field from frappe.tests.utils import FrappeTestCase +from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note from erpnext.stock.doctype.inventory_dimension.inventory_dimension import ( CanNotBeChildDoc, CanNotBeDefaultDimension, DoNotChangeError, + delete_dimension, ) +from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse @@ -42,6 +46,32 @@ class TestInventoryDimension(FrappeTestCase): self.assertRaises(CanNotBeDefaultDimension, inv_dim1.insert) + def test_delete_inventory_dimension(self): + inv_dim1 = create_inventory_dimension( + reference_document="Shelf", + type_of_transaction="Outward", + dimension_name="From Shelf", + apply_to_all_doctypes=0, + document_type="Stock Entry Detail", + condition="parent.purpose == 'Material Issue'", + ) + + inv_dim1.save() + + custom_field = frappe.db.get_value( + "Custom Field", {"fieldname": "from_shelf", "dt": "Stock Entry Detail"}, "name" + ) + + self.assertTrue(custom_field) + + delete_dimension(inv_dim1.name) + + custom_field = frappe.db.get_value( + "Custom Field", {"fieldname": "from_shelf", "dt": "Stock Entry Detail"}, "name" + ) + + self.assertFalse(custom_field) + def test_inventory_dimension(self): warehouse = "Shelf Warehouse - _TC" item_code = "_Test Item" @@ -55,6 +85,9 @@ class TestInventoryDimension(FrappeTestCase): condition="parent.purpose == 'Material Issue'", ) + inv_dim1.reqd = 0 + inv_dim1.save() + create_inventory_dimension( reference_document="Shelf", type_of_transaction="Inward", @@ -109,6 +142,114 @@ class TestInventoryDimension(FrappeTestCase): self.assertTrue(inv_dim1.has_stock_ledger()) self.assertRaises(DoNotChangeError, inv_dim1.save) + def test_inventory_dimension_for_purchase_receipt_and_delivery_note(self): + inv_dimension = create_inventory_dimension( + reference_document="Rack", dimension_name="Rack", apply_to_all_doctypes=1 + ) + + self.assertEqual(inv_dimension.type_of_transaction, "Both") + self.assertEqual(inv_dimension.fetch_from_parent, "Rack") + + create_custom_field( + "Purchase Receipt", dict(fieldname="rack", label="Rack", fieldtype="Link", options="Rack") + ) + + create_custom_field( + "Delivery Note", dict(fieldname="rack", label="Rack", fieldtype="Link", options="Rack") + ) + + frappe.reload_doc("stock", "doctype", "purchase_receipt_item") + frappe.reload_doc("stock", "doctype", "delivery_note_item") + + pr_doc = make_purchase_receipt(qty=2, do_not_submit=True) + pr_doc.rack = "Rack 1" + pr_doc.save() + pr_doc.submit() + + pr_doc.load_from_db() + + self.assertEqual(pr_doc.items[0].rack, "Rack 1") + sle_rack = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_detail_no": pr_doc.items[0].name, "voucher_type": pr_doc.doctype}, + "rack", + ) + + self.assertEqual(sle_rack, "Rack 1") + + dn_doc = create_delivery_note(qty=2, do_not_submit=True) + dn_doc.rack = "Rack 1" + dn_doc.save() + dn_doc.submit() + + dn_doc.load_from_db() + + self.assertEqual(dn_doc.items[0].rack, "Rack 1") + sle_rack = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_detail_no": dn_doc.items[0].name, "voucher_type": dn_doc.doctype}, + "rack", + ) + + self.assertEqual(sle_rack, "Rack 1") + + def test_check_standard_dimensions(self): + create_inventory_dimension( + reference_document="Project", + type_of_transaction="Outward", + dimension_name="Project", + apply_to_all_doctypes=0, + document_type="Stock Ledger Entry", + ) + + self.assertFalse( + frappe.db.get_value( + "Custom Field", {"fieldname": "project", "dt": "Stock Ledger Entry"}, "name" + ) + ) + + def test_check_mandatory_dimensions(self): + doc = create_inventory_dimension( + reference_document="Pallet", + type_of_transaction="Outward", + dimension_name="Pallet", + apply_to_all_doctypes=0, + document_type="Stock Entry Detail", + ) + + doc.reqd = 1 + doc.save() + + self.assertTrue( + frappe.db.get_value( + "Custom Field", {"fieldname": "pallet", "dt": "Stock Entry Detail", "reqd": 1}, "name" + ) + ) + + doc.load_from_db + doc.reqd = 0 + doc.save() + + def test_check_mandatory_depends_on_dimensions(self): + doc = create_inventory_dimension( + reference_document="Pallet", + type_of_transaction="Outward", + dimension_name="Pallet", + apply_to_all_doctypes=0, + document_type="Stock Entry Detail", + ) + + doc.mandatory_depends_on = "t_warehouse" + doc.save() + + self.assertTrue( + frappe.db.get_value( + "Custom Field", + {"fieldname": "pallet", "dt": "Stock Entry Detail", "mandatory_depends_on": "t_warehouse"}, + "name", + ) + ) + def prepare_test_data(): if not frappe.db.exists("DocType", "Shelf"): @@ -133,6 +274,44 @@ def prepare_test_data(): create_warehouse("Shelf Warehouse") + if not frappe.db.exists("DocType", "Rack"): + frappe.get_doc( + { + "doctype": "DocType", + "name": "Rack", + "module": "Stock", + "custom": 1, + "naming_rule": "By fieldname", + "autoname": "field:rack_name", + "fields": [{"label": "Rack Name", "fieldname": "rack_name", "fieldtype": "Data"}], + "permissions": [ + {"role": "System Manager", "permlevel": 0, "read": 1, "write": 1, "create": 1, "delete": 1} + ], + } + ).insert(ignore_permissions=True) + + for rack in ["Rack 1"]: + if not frappe.db.exists("Rack", rack): + frappe.get_doc({"doctype": "Rack", "rack_name": rack}).insert(ignore_permissions=True) + + create_warehouse("Rack Warehouse") + + if not frappe.db.exists("DocType", "Pallet"): + frappe.get_doc( + { + "doctype": "DocType", + "name": "Pallet", + "module": "Stock", + "custom": 1, + "naming_rule": "By fieldname", + "autoname": "field:pallet_name", + "fields": [{"label": "Pallet Name", "fieldname": "pallet_name", "fieldtype": "Data"}], + "permissions": [ + {"role": "System Manager", "permlevel": 0, "read": 1, "write": 1, "create": 1, "delete": 1} + ], + } + ).insert(ignore_permissions=True) + def create_inventory_dimension(**args): args = frappe._dict(args) diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index 29b001fdcb..9a9ddf4404 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -10,6 +10,34 @@ frappe.ui.form.on("Item", { frm.add_fetch('attribute', 'to_range', 'to_range'); frm.add_fetch('attribute', 'increment', 'increment'); frm.add_fetch('tax_type', 'tax_rate', 'tax_rate'); + + frm.make_methods = { + 'Sales Order': () => { + open_form(frm, "Sales Order", "Sales Order Item", "items"); + }, + 'Delivery Note': () => { + open_form(frm, "Delivery Note", "Delivery Note Item", "items"); + }, + 'Sales Invoice': () => { + open_form(frm, "Sales Invoice", "Sales Invoice Item", "items"); + }, + 'Purchase Order': () => { + open_form(frm, "Purchase Order", "Purchase Order Item", "items"); + }, + 'Purchase Receipt': () => { + open_form(frm, "Purchase Receipt", "Purchase Receipt Item", "items"); + }, + 'Purchase Invoice': () => { + open_form(frm, "Purchase Invoice", "Purchase Invoice Item", "items"); + }, + 'Material Request': () => { + open_form(frm, "Material Request", "Material Request Item", "items"); + }, + 'Stock Entry': () => { + open_form(frm, "Stock Entry", "Stock Entry Detail", "items"); + }, + }; + }, onload: function(frm) { erpnext.item.setup_queries(frm); @@ -562,7 +590,7 @@ $.extend(erpnext.item, { let selected_attributes = {}; me.multiple_variant_dialog.$wrapper.find('.form-column').each((i, col) => { if(i===0) return; - let attribute_name = $(col).find('label').html().trim(); + let attribute_name = $(col).find('.control-label').html().trim(); selected_attributes[attribute_name] = []; let checked_opts = $(col).find('.checkbox input'); checked_opts.each((i, opt) => { @@ -858,3 +886,26 @@ frappe.tour['Item'] = [ ]; + +function open_form(frm, doctype, child_doctype, parentfield) { + frappe.model.with_doctype(doctype, () => { + let new_doc = frappe.model.get_new_doc(doctype); + + let new_child_doc = frappe.model.add_child(new_doc, child_doctype, parentfield); + new_child_doc.item_code = frm.doc.name; + new_child_doc.item_name = frm.doc.item_name; + new_child_doc.uom = frm.doc.stock_uom; + new_child_doc.description = frm.doc.description; + if (!new_child_doc.qty) { + new_child_doc.qty = 1.0; + } + + frappe.run_serially([ + () => frappe.ui.form.make_quick_entry(doctype, null, null, new_doc), + () => { + frappe.flags.ignore_company_party_validation = true; + frappe.model.trigger("item_code", frm.doc.name, new_child_doc); + } + ]) + }); +} diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index 76cb31dc42..34adbebc07 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -22,7 +22,6 @@ "allow_alternative_item", "is_stock_item", "has_variants", - "include_item_in_manufacturing", "opening_stock", "valuation_rate", "standard_rate", @@ -112,6 +111,7 @@ "quality_inspection_template", "inspection_required_before_delivery", "manufacturing", + "include_item_in_manufacturing", "is_sub_contracted_item", "default_bom", "column_break_74", @@ -706,7 +706,7 @@ "depends_on": "enable_deferred_expense", "fieldname": "no_of_months_exp", "fieldtype": "Int", - "label": "No of Months" + "label": "No of Months (Expense)" }, { "collapsible": 1, @@ -795,7 +795,7 @@ }, { "fieldname": "customer_code", - "fieldtype": "Data", + "fieldtype": "Small Text", "hidden": 1, "label": "Customer Code", "no_copy": 1, @@ -910,7 +910,8 @@ "image_field": "image", "index_web_pages_for_search": 1, "links": [], - "modified": "2022-06-15 09:02:06.177691", + "make_attachments_public": 1, + "modified": "2023-02-14 04:48:26.343620", "modified_by": "Administrator", "module": "Stock", "name": "Item", diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 87fa72d74f..c06700a99a 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -8,6 +8,8 @@ from typing import Dict, List, Optional import frappe from frappe import _ from frappe.model.document import Document +from frappe.query_builder import Interval +from frappe.query_builder.functions import Count, CurDate, UnixTimestamp from frappe.utils import ( cint, cstr, @@ -164,10 +166,7 @@ class Item(Document): if not self.is_stock_item or self.has_serial_no or self.has_batch_no: return - if not self.valuation_rate and self.standard_rate: - self.valuation_rate = self.standard_rate - - if not self.valuation_rate and not self.is_customer_provided_item: + if not self.valuation_rate and not self.standard_rate and not self.is_customer_provided_item: frappe.throw(_("Valuation Rate is mandatory if Opening Stock entered")) from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry @@ -192,7 +191,7 @@ class Item(Document): item_code=self.name, target=default_warehouse, qty=self.opening_stock, - rate=self.valuation_rate, + rate=self.valuation_rate or self.standard_rate, company=default.company, posting_date=getdate(), posting_time=nowtime(), @@ -232,10 +231,10 @@ class Item(Document): def clear_retain_sample(self): if not self.has_batch_no: - self.retain_sample = None + self.retain_sample = False if not self.retain_sample: - self.sample_quantity = None + self.sample_quantity = 0 def add_default_uom_in_conversion_factor_table(self): if not self.is_new() and self.has_value_changed("stock_uom"): @@ -279,7 +278,7 @@ class Item(Document): frappe.throw(_("Row #{0}: Maximum Net Rate cannot be greater than Minimum Net Rate")) def update_template_tables(self): - template = frappe.get_doc("Item", self.variant_of) + template = frappe.get_cached_doc("Item", self.variant_of) # add item taxes from template for d in template.get("taxes"): @@ -359,7 +358,7 @@ class Item(Document): check_list.append(d.item_tax_template) def validate_barcode(self): - from stdnum import ean + import barcodenumber if len(self.barcodes) > 0: for item_barcode in self.barcodes: @@ -377,19 +376,16 @@ class Item(Document): item_barcode.barcode_type = ( "" if item_barcode.barcode_type not in options else item_barcode.barcode_type ) - if item_barcode.barcode_type and item_barcode.barcode_type.upper() in ( - "EAN", - "UPC-A", - "EAN-13", - "EAN-8", - ): - if not ean.is_valid(item_barcode.barcode): - frappe.throw( - _("Barcode {0} is not a valid {1} code").format( - item_barcode.barcode, item_barcode.barcode_type - ), - InvalidBarcode, - ) + if item_barcode.barcode_type: + barcode_type = convert_erpnext_to_barcodenumber(item_barcode.barcode_type.upper()) + if barcode_type in barcodenumber.barcodes(): + if not barcodenumber.check_code(barcode_type, item_barcode.barcode): + frappe.throw( + _("Barcode {0} is not a valid {1} code").format( + item_barcode.barcode, item_barcode.barcode_type + ), + InvalidBarcode, + ) def validate_warehouse_for_reorder(self): """Validate Reorder level table for duplicate and conditional mandatory""" @@ -937,15 +933,24 @@ class Item(Document): "Purchase Order Item", "Material Request Item", "Product Bundle", + "BOM", ] for doctype in linked_doctypes: filters = {"item_code": self.name, "docstatus": 1} - if doctype == "Product Bundle": - filters = {"new_item_code": self.name} + if doctype in ("Product Bundle", "BOM"): + if doctype == "Product Bundle": + filters = {"new_item_code": self.name} + fieldname = "new_item_code as docname" + else: + filters = {"item": self.name, "docstatus": 1} + fieldname = "name as docname" - if doctype in ( + if linked_doc := frappe.db.get_value(doctype, filters, fieldname, as_dict=True): + return linked_doc.update({"doctype": doctype}) + + elif doctype in ( "Purchase Invoice Item", "Sales Invoice Item", ): @@ -977,6 +982,22 @@ class Item(Document): ) +def convert_erpnext_to_barcodenumber(erpnext_number): + convert = { + "UPC-A": "UPCA", + "CODE-39": "CODE39", + "EAN": "EAN13", + "EAN-12": "EAN", + "EAN-8": "EAN8", + "ISBN-10": "ISBN10", + "ISBN-13": "ISBN13", + } + if erpnext_number in convert: + return convert[erpnext_number] + else: + return erpnext_number + + def make_item_price(item, price_list_name, item_price): frappe.get_doc( { @@ -988,18 +1009,19 @@ def make_item_price(item, price_list_name, item_price): ).insert() -def get_timeline_data(doctype, name): +def get_timeline_data(doctype: str, name: str) -> dict[int, int]: """get timeline data based on Stock Ledger Entry. This is displayed as heatmap on the item page.""" - items = frappe.db.sql( - """select unix_timestamp(posting_date), count(*) - from `tabStock Ledger Entry` - where item_code=%s and posting_date > date_sub(curdate(), interval 1 year) - group by posting_date""", - name, - ) + sle = frappe.qb.DocType("Stock Ledger Entry") - return dict(items) + return dict( + frappe.qb.from_(sle) + .select(UnixTimestamp(sle.posting_date), Count("*")) + .where(sle.item_code == name) + .where(sle.posting_date > CurDate() - Interval(years=1)) + .groupby(sle.posting_date) + .run() + ) def validate_end_of_life(item_code, end_of_life=None, disabled=None): diff --git a/erpnext/stock/doctype/item/item_dashboard.py b/erpnext/stock/doctype/item/item_dashboard.py index 897acb7448..34bb4d1225 100644 --- a/erpnext/stock/doctype/item/item_dashboard.py +++ b/erpnext/stock/doctype/item/item_dashboard.py @@ -5,7 +5,7 @@ def get_data(): return { "heatmap": True, "heatmap_message": _("This is based on stock movement. See {0} for details").format( - '' + _("Stock Ledger") + "" + '' + _("Stock Ledger") + "" ), "fieldname": "item_code", "non_standard_fieldnames": { diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py index 3366c737cb..67ed90d4e7 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -5,6 +5,7 @@ import json import frappe +from frappe.custom.doctype.property_setter.property_setter import make_property_setter from frappe.test_runner import make_test_objects from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import add_days, today @@ -31,7 +32,7 @@ test_ignore = ["BOM"] test_dependencies = ["Warehouse", "Item Group", "Item Tax Template", "Brand", "Item Attribute"] -def make_item(item_code=None, properties=None): +def make_item(item_code=None, properties=None, uoms=None): if not item_code: item_code = frappe.generate_hash(length=16) @@ -55,6 +56,11 @@ def make_item(item_code=None, properties=None): for item_default in [doc for doc in item.get("item_defaults") if not doc.default_warehouse]: item_default.default_warehouse = "_Test Warehouse - _TC" item_default.company = "_Test Company" + + if uoms: + for uom in uoms: + item.append("uoms", uom) + item.insert() return item @@ -77,6 +83,7 @@ class TestItem(FrappeTestCase): def test_get_item_details(self): # delete modified item price record and make as per test_records frappe.db.sql("""delete from `tabItem Price`""") + frappe.db.sql("""delete from `tabBin`""") to_check = { "item_code": "_Test Item", @@ -97,9 +104,25 @@ class TestItem(FrappeTestCase): "batch_no": None, "uom": "_Test UOM", "conversion_factor": 1.0, + "reserved_qty": 1, + "actual_qty": 5, + "projected_qty": 14, } make_test_objects("Item Price") + make_test_objects( + "Bin", + [ + { + "item_code": "_Test Item", + "warehouse": "_Test Warehouse - _TC", + "reserved_qty": 1, + "actual_qty": 5, + "ordered_qty": 10, + "projected_qty": 14, + } + ], + ) company = "_Test Company" currency = frappe.get_cached_value("Company", company, "default_currency") @@ -123,7 +146,7 @@ class TestItem(FrappeTestCase): ) for key, value in to_check.items(): - self.assertEqual(value, details.get(key)) + self.assertEqual(value, details.get(key), key) def test_item_tax_template(self): expected_item_tax_template = [ @@ -556,6 +579,19 @@ class TestItem(FrappeTestCase): { "barcode": "ARBITRARY_TEXT", }, + {"barcode": "72527273070", "barcode_type": "UPC-A"}, + {"barcode": "123456", "barcode_type": "CODE-39"}, + {"barcode": "401268452363", "barcode_type": "EAN-12"}, + {"barcode": "90311017", "barcode_type": "EAN-8"}, + {"barcode": "0123456789012", "barcode_type": "GS1"}, + {"barcode": "2211564566668", "barcode_type": "GTIN"}, + {"barcode": "0256480249", "barcode_type": "ISBN"}, + {"barcode": "0192552570", "barcode_type": "ISBN-10"}, + {"barcode": "9781234567897", "barcode_type": "ISBN-13"}, + {"barcode": "9771234567898", "barcode_type": "ISSN"}, + {"barcode": "4581171967072", "barcode_type": "JAN"}, + {"barcode": "12345678", "barcode_type": "PZN"}, + {"barcode": "725272730706", "barcode_type": "UPC"}, ] create_item(item_code) for barcode_properties in barcode_properties_list: @@ -716,8 +752,8 @@ class TestItem(FrappeTestCase): item.has_batch_no = None item.save() - self.assertEqual(item.retain_sample, None) - self.assertEqual(item.sample_quantity, None) + self.assertEqual(item.retain_sample, False) + self.assertEqual(item.sample_quantity, 0) item.delete() def consume_item_code_with_differet_stock_transactions( @@ -778,6 +814,68 @@ class TestItem(FrappeTestCase): item.has_batch_no = 1 item.save() + def test_customer_codes_length(self): + """Check if item code with special characters are allowed.""" + item = make_item(properties={"item_code": "Test Item Code With Special Characters"}) + for row in range(3): + item.append("customer_items", {"ref_code": frappe.generate_hash("", 120)}) + item.save() + self.assertTrue(len(item.customer_code) > 140) + + def test_update_is_stock_item(self): + # Step - 1: Create an Item with Maintain Stock enabled + item = make_item(properties={"is_stock_item": 1}) + + # Step - 2: Disable Maintain Stock + item.is_stock_item = 0 + item.save() + item.reload() + self.assertEqual(item.is_stock_item, 0) + + # Step - 3: Create Product Bundle + pb = frappe.new_doc("Product Bundle") + pb.new_item_code = item.name + pb.flags.ignore_mandatory = True + pb.save() + + # Step - 4: Try to enable Maintain Stock, should throw a validation error + item.is_stock_item = 1 + self.assertRaises(frappe.ValidationError, item.save) + item.reload() + + # Step - 5: Delete Product Bundle + pb.delete() + + # Step - 6: Again try to enable Maintain Stock + item.is_stock_item = 1 + item.save() + item.reload() + self.assertEqual(item.is_stock_item, 1) + + def test_serach_fields_for_item(self): + from erpnext.controllers.queries import item_query + + make_property_setter("Item", None, "search_fields", "item_name", "Data", for_doctype="Doctype") + + item = make_item(properties={"item_name": "Test Item", "description": "Test Description"}) + data = item_query( + "Item", "Test Item", "", 0, 20, filters={"item_name": "Test Item"}, as_dict=True + ) + self.assertEqual(data[0].name, item.name) + self.assertEqual(data[0].item_name, item.item_name) + self.assertTrue("description" not in data[0]) + + make_property_setter( + "Item", None, "search_fields", "item_name, description", "Data", for_doctype="Doctype" + ) + data = item_query( + "Item", "Test Item", "", 0, 20, filters={"item_name": "Test Item"}, as_dict=True + ) + self.assertEqual(data[0].name, item.name) + self.assertEqual(data[0].item_name, item.item_name) + self.assertEqual(data[0].description, item.description) + self.assertTrue("description" in data[0]) + def set_item_variant_settings(fields): doc = frappe.get_doc("Item Variant Settings") diff --git a/erpnext/stock/doctype/item_attribute/item_attribute.py b/erpnext/stock/doctype/item_attribute/item_attribute.py index 391ff06918..ac4c313e28 100644 --- a/erpnext/stock/doctype/item_attribute/item_attribute.py +++ b/erpnext/stock/doctype/item_attribute/item_attribute.py @@ -74,11 +74,10 @@ class ItemAttribute(Document): def validate_duplication(self): values, abbrs = [], [] for d in self.item_attribute_values: - d.abbr = d.abbr.upper() - if d.attribute_value in values: - frappe.throw(_("{0} must appear only once").format(d.attribute_value)) + if d.attribute_value.lower() in map(str.lower, values): + frappe.throw(_("Attribute value: {0} must appear only once").format(d.attribute_value.title())) values.append(d.attribute_value) - if d.abbr in abbrs: - frappe.throw(_("{0} must appear only once").format(d.abbr)) + if d.abbr.lower() in map(str.lower, abbrs): + frappe.throw(_("Abbreviation: {0} must appear only once").format(d.abbr.title())) abbrs.append(d.abbr) diff --git a/erpnext/stock/doctype/item_barcode/item_barcode.json b/erpnext/stock/doctype/item_barcode/item_barcode.json index 56832f32d3..d9a8347ca0 100644 --- a/erpnext/stock/doctype/item_barcode/item_barcode.json +++ b/erpnext/stock/doctype/item_barcode/item_barcode.json @@ -17,6 +17,7 @@ "in_list_view": 1, "label": "Barcode", "no_copy": 1, + "reqd": 1, "unique": 1 }, { @@ -24,7 +25,7 @@ "fieldtype": "Select", "in_list_view": 1, "label": "Barcode Type", - "options": "\nEAN\nUPC-A" + "options": "\nEAN\nUPC-A\nCODE-39\nEAN-12\nEAN-8\nGS1\nGTIN\nISBN\nISBN-10\nISBN-13\nISSN\nJAN\nPZN\nUPC" }, { "fieldname": "uom", @@ -36,7 +37,7 @@ ], "istable": 1, "links": [], - "modified": "2022-06-01 06:24:33.969534", + "modified": "2022-08-24 19:59:47.871677", "modified_by": "Administrator", "module": "Stock", "name": "Item Barcode", diff --git a/erpnext/stock/doctype/item_price/item_price.js b/erpnext/stock/doctype/item_price/item_price.js index 12cf6cf84d..ce489ff52b 100644 --- a/erpnext/stock/doctype/item_price/item_price.js +++ b/erpnext/stock/doctype/item_price/item_price.js @@ -2,7 +2,18 @@ // License: GNU General Public License v3. See license.txt frappe.ui.form.on("Item Price", { - onload: function (frm) { + setup(frm) { + frm.set_query("item_code", function() { + return { + filters: { + "disabled": 0, + "has_variants": 0 + } + }; + }); + }, + + onload(frm) { // Fetch price list details frm.add_fetch("price_list", "buying", "buying"); frm.add_fetch("price_list", "selling", "selling"); diff --git a/erpnext/stock/doctype/item_price/item_price.json b/erpnext/stock/doctype/item_price/item_price.json index 83177b372a..f4d9bb0742 100644 --- a/erpnext/stock/doctype/item_price/item_price.json +++ b/erpnext/stock/doctype/item_price/item_price.json @@ -1,6 +1,7 @@ { "actions": [], "allow_import": 1, + "autoname": "hash", "creation": "2013-05-02 16:29:48", "description": "Multiple Item prices.", "doctype": "DocType", @@ -48,78 +49,62 @@ "oldfieldtype": "Select", "options": "Item", "reqd": 1, - "search_index": 1, - "show_days": 1, - "show_seconds": 1 + "search_index": 1 }, { "fieldname": "uom", "fieldtype": "Link", "label": "UOM", - "options": "UOM", - "show_days": 1, - "show_seconds": 1 + "options": "UOM" }, { "default": "0", "description": "Quantity that must be bought or sold per UOM", "fieldname": "packing_unit", "fieldtype": "Int", - "label": "Packing Unit", - "show_days": 1, - "show_seconds": 1 + "label": "Packing Unit" }, { "fieldname": "column_break_17", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "fieldname": "item_name", "fieldtype": "Data", "in_list_view": 1, "label": "Item Name", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fetch_from": "item_code.brand", "fieldname": "brand", - "fieldtype": "Read Only", + "fieldtype": "Link", "in_list_view": 1, "label": "Brand", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "options": "Brand", + "read_only": 1 }, { "fieldname": "item_description", "fieldtype": "Text", "label": "Item Description", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "price_list_details", "fieldtype": "Section Break", "label": "Price List", - "options": "fa fa-tags", - "show_days": 1, - "show_seconds": 1 + "options": "fa fa-tags" }, { "fieldname": "price_list", "fieldtype": "Link", "in_global_search": 1, + "in_list_view": 1, "in_standard_filter": 1, "label": "Price List", "options": "Price List", - "reqd": 1, - "show_days": 1, - "show_seconds": 1 + "reqd": 1 }, { "bold": 1, @@ -127,49 +112,37 @@ "fieldname": "customer", "fieldtype": "Link", "label": "Customer", - "options": "Customer", - "show_days": 1, - "show_seconds": 1 + "options": "Customer" }, { "depends_on": "eval:doc.buying == 1", "fieldname": "supplier", "fieldtype": "Link", "label": "Supplier", - "options": "Supplier", - "show_days": 1, - "show_seconds": 1 + "options": "Supplier" }, { "fieldname": "column_break_3", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "default": "0", "fieldname": "buying", "fieldtype": "Check", "label": "Buying", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "default": "0", "fieldname": "selling", "fieldtype": "Check", "label": "Selling", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "item_details", "fieldtype": "Section Break", - "options": "fa fa-tag", - "show_days": 1, - "show_seconds": 1 + "options": "fa fa-tag" }, { "bold": 1, @@ -177,15 +150,11 @@ "fieldtype": "Link", "label": "Currency", "options": "Currency", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "col_br_1", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "fieldname": "price_list_rate", @@ -197,84 +166,65 @@ "oldfieldname": "ref_rate", "oldfieldtype": "Currency", "options": "currency", - "reqd": 1, - "show_days": 1, - "show_seconds": 1 + "reqd": 1 }, { "fieldname": "section_break_15", - "fieldtype": "Section Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Section Break" }, { "default": "Today", "fieldname": "valid_from", "fieldtype": "Date", - "label": "Valid From", - "show_days": 1, - "show_seconds": 1 + "label": "Valid From" }, { "default": "0", "fieldname": "lead_time_days", "fieldtype": "Int", - "label": "Lead Time in days", - "show_days": 1, - "show_seconds": 1 + "label": "Lead Time in days" }, { "fieldname": "column_break_18", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "fieldname": "valid_upto", "fieldtype": "Date", - "label": "Valid Upto", - "show_days": 1, - "show_seconds": 1 + "label": "Valid Upto" }, { "fieldname": "section_break_24", - "fieldtype": "Section Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Section Break" }, { "fieldname": "note", "fieldtype": "Text", - "label": "Note", - "show_days": 1, - "show_seconds": 1 + "label": "Note" }, { "fieldname": "reference", "fieldtype": "Data", "in_list_view": 1, - "label": "Reference", - "show_days": 1, - "show_seconds": 1 + "in_standard_filter": 1, + "label": "Reference" }, { "fieldname": "batch_no", "fieldtype": "Link", "label": "Batch No", - "options": "Batch", - "show_days": 1, - "show_seconds": 1 + "options": "Batch" } ], "icon": "fa fa-flag", "idx": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2020-12-08 18:12:15.395772", + "modified": "2022-11-15 08:26:04.041861", "modified_by": "Administrator", "module": "Stock", "name": "Item Price", - "name_case": "Title Case", + "naming_rule": "Random", "owner": "Administrator", "permissions": [ { @@ -307,6 +257,7 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "ASC", + "states": [], "title_field": "item_name", "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/stock/doctype/item_price/item_price.py b/erpnext/stock/doctype/item_price/item_price.py index bcd31ada83..54d1ae634f 100644 --- a/erpnext/stock/doctype/item_price/item_price.py +++ b/erpnext/stock/doctype/item_price/item_price.py @@ -3,7 +3,7 @@ import frappe -from frappe import _ +from frappe import _, bold from frappe.model.document import Document from frappe.query_builder import Criterion from frappe.query_builder.functions import Cast_ @@ -21,6 +21,7 @@ class ItemPrice(Document): self.update_price_list_details() self.update_item_details() self.check_duplicates() + self.validate_item_template() def validate_item(self): if not frappe.db.exists("Item", self.item_code): @@ -49,6 +50,12 @@ class ItemPrice(Document): "Item", self.item_code, ["item_name", "description"] ) + def validate_item_template(self): + if frappe.get_cached_value("Item", self.item_code, "has_variants"): + msg = f"Item Price cannot be created for the template item {bold(self.item_code)}" + + frappe.throw(_(msg)) + def check_duplicates(self): item_price = frappe.qb.DocType("Item Price") diff --git a/erpnext/stock/doctype/item_price/item_price_list.js b/erpnext/stock/doctype/item_price/item_price_list.js new file mode 100644 index 0000000000..48158393f6 --- /dev/null +++ b/erpnext/stock/doctype/item_price/item_price_list.js @@ -0,0 +1,3 @@ +frappe.listview_settings['Item Price'] = { + hide_name_column: true, +}; diff --git a/erpnext/stock/doctype/item_price/test_item_price.py b/erpnext/stock/doctype/item_price/test_item_price.py index 30d933e247..8fd4938fa3 100644 --- a/erpnext/stock/doctype/item_price/test_item_price.py +++ b/erpnext/stock/doctype/item_price/test_item_price.py @@ -16,6 +16,28 @@ class TestItemPrice(FrappeTestCase): frappe.db.sql("delete from `tabItem Price`") make_test_records_for_doctype("Item Price", force=True) + def test_template_item_price(self): + from erpnext.stock.doctype.item.test_item import make_item + + item = make_item( + "Test Template Item 1", + { + "has_variants": 1, + "variant_based_on": "Manufacturer", + }, + ) + + doc = frappe.get_doc( + { + "doctype": "Item Price", + "price_list": "_Test Price List", + "item_code": item.name, + "price_list_rate": 100, + } + ) + + self.assertRaises(frappe.ValidationError, doc.save) + def test_duplicate_item(self): doc = frappe.copy_doc(test_records[0]) self.assertRaises(ItemPriceDuplicateItem, doc.save) diff --git a/erpnext/stock/doctype/item_price/test_records.json b/erpnext/stock/doctype/item_price/test_records.json index 0a3d7e8198..afe5ad65b7 100644 --- a/erpnext/stock/doctype/item_price/test_records.json +++ b/erpnext/stock/doctype/item_price/test_records.json @@ -38,5 +38,19 @@ "price_list_rate": 1000, "valid_from": "2017-04-10", "valid_upto": "2017-04-17" + }, + { + "doctype": "Item Price", + "item_code": "_Test Item", + "price_list": "_Test Buying Price List", + "price_list_rate": 100, + "supplier": "_Test Supplier" + }, + { + "doctype": "Item Price", + "item_code": "_Test Item", + "price_list": "_Test Selling Price List", + "price_list_rate": 200, + "customer": "_Test Customer" } ] diff --git a/erpnext/stock/doctype/item_supplier/item_supplier.json b/erpnext/stock/doctype/item_supplier/item_supplier.json index 6cff8e0892..84649a67d0 100644 --- a/erpnext/stock/doctype/item_supplier/item_supplier.json +++ b/erpnext/stock/doctype/item_supplier/item_supplier.json @@ -1,95 +1,43 @@ { - "allow_copy": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2013-02-22 01:28:01", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "creation": "2013-02-22 01:28:01", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "supplier", + "supplier_part_no" + ], "fields": [ { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "supplier", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Supplier", - "length": 0, - "no_copy": 0, - "options": "Supplier", - "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": "supplier", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Supplier", + "options": "Supplier", + "reqd": 1 + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "supplier_part_no", - "fieldtype": "Data", - "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": "Supplier Part Number", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "200px", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0, + "fieldname": "supplier_part_no", + "fieldtype": "Data", + "in_global_search": 1, + "in_list_view": 1, + "label": "Supplier Part Number", + "print_width": "200px", "width": "200px" } - ], - "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": "2017-02-20 13:29:32.569715", - "modified_by": "Administrator", - "module": "Stock", - "name": "Item Supplier", - "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": "2022-09-07 12:33:55.780062", + "modified_by": "Administrator", + "module": "Stock", + "name": "Item Supplier", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py index b3af309359..111a0861b7 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py @@ -55,7 +55,6 @@ class LandedCostVoucher(Document): self.get_items_from_purchase_receipts() self.set_applicable_charges_on_item() - self.validate_applicable_charges_for_item() def check_mandatory(self): if not self.get("purchase_receipts"): @@ -115,6 +114,13 @@ class LandedCostVoucher(Document): total_item_cost += item.get(based_on_field) for item in self.get("items"): + if not total_item_cost and not item.get(based_on_field): + frappe.throw( + _( + "It's not possible to distribute charges equally when total amount is zero, please set 'Distribute Charges Based On' as 'Quantity'" + ) + ) + item.applicable_charges = flt( flt(item.get(based_on_field)) * (flt(self.total_taxes_and_charges) / flt(total_item_cost)), item.precision("applicable_charges"), @@ -162,6 +168,7 @@ class LandedCostVoucher(Document): ) def on_submit(self): + self.validate_applicable_charges_for_item() self.update_landed_cost() def on_cancel(self): diff --git a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py index 979b5c4f83..00fa1686c0 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py @@ -175,6 +175,59 @@ class TestLandedCostVoucher(FrappeTestCase): ) self.assertEqual(last_sle_after_landed_cost.stock_value - last_sle.stock_value, 50.0) + def test_landed_cost_voucher_for_zero_purchase_rate(self): + "Test impact of LCV on future stock balances." + from erpnext.stock.doctype.item.test_item import make_item + + item = make_item("LCV Stock Item", {"is_stock_item": 1}) + warehouse = "Stores - _TC" + + pr = make_purchase_receipt( + item_code=item.name, + warehouse=warehouse, + qty=10, + rate=0, + posting_date=add_days(frappe.utils.nowdate(), -2), + ) + + self.assertEqual( + frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Purchase Receipt", "voucher_no": pr.name, "is_cancelled": 0}, + "stock_value_difference", + ), + 0, + ) + + lcv = make_landed_cost_voucher( + company=pr.company, + receipt_document_type="Purchase Receipt", + receipt_document=pr.name, + charges=100, + distribute_charges_based_on="Distribute Manually", + do_not_save=True, + ) + + lcv.get_items_from_purchase_receipts() + lcv.items[0].applicable_charges = 100 + lcv.save() + lcv.submit() + + self.assertTrue( + frappe.db.exists( + "Stock Ledger Entry", + {"voucher_type": "Purchase Receipt", "voucher_no": pr.name, "is_cancelled": 0}, + ) + ) + self.assertEqual( + frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Purchase Receipt", "voucher_no": pr.name, "is_cancelled": 0}, + "stock_value_difference", + ), + 100, + ) + def test_landed_cost_voucher_against_purchase_invoice(self): pi = make_purchase_invoice( @@ -516,7 +569,7 @@ def make_landed_cost_voucher(**args): lcv = frappe.new_doc("Landed Cost Voucher") lcv.company = args.company or "_Test Company" - lcv.distribute_charges_based_on = "Amount" + lcv.distribute_charges_based_on = args.distribute_charges_based_on or "Amount" lcv.set( "purchase_receipts", diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js index e68b0abfb9..c1f1b0d135 100644 --- a/erpnext/stock/doctype/material_request/material_request.js +++ b/erpnext/stock/doctype/material_request/material_request.js @@ -110,8 +110,11 @@ frappe.ui.form.on('Material Request', { if (frm.doc.material_request_type === "Material Transfer") { add_create_pick_list_button(); - frm.add_custom_button(__("Transfer Material"), + frm.add_custom_button(__("Material Transfer"), () => frm.events.make_stock_entry(frm), __('Create')); + + frm.add_custom_button(__("Material Transfer (In Transit)"), + () => frm.events.make_in_transit_stock_entry(frm), __('Create')); } if (frm.doc.material_request_type === "Material Issue") { @@ -333,6 +336,46 @@ frappe.ui.form.on('Material Request', { }); }, + make_in_transit_stock_entry(frm) { + frappe.prompt( + [ + { + label: __('In Transit Warehouse'), + fieldname: 'in_transit_warehouse', + fieldtype: 'Link', + options: 'Warehouse', + reqd: 1, + get_query: () => { + return{ + filters: { + 'company': frm.doc.company, + 'is_group': 0, + 'warehouse_type': 'Transit' + } + } + } + } + ], + (values) => { + frappe.call({ + method: "erpnext.stock.doctype.material_request.material_request.make_in_transit_stock_entry", + args: { + source_name: frm.doc.name, + in_transit_warehouse: values.in_transit_warehouse + }, + callback: function(r) { + if (r.message) { + let doc = frappe.model.sync(r.message); + frappe.set_route('Form', doc[0].doctype, doc[0].name); + } + } + }) + }, + __('In Transit Transfer'), + __("Create Stock Entry") + ) + }, + create_pick_list: (frm) => { frappe.model.open_mapped_doc({ method: "erpnext.stock.doctype.material_request.material_request.create_pick_list", @@ -366,12 +409,10 @@ frappe.ui.form.on('Material Request', { frappe.ui.form.on("Material Request Item", { qty: function (frm, doctype, name) { - var d = locals[doctype][name]; - if (flt(d.qty) < flt(d.min_order_qty)) { + const item = locals[doctype][name]; + if (flt(item.qty) < flt(item.min_order_qty)) { frappe.msgprint(__("Warning: Material Requested Qty is less than Minimum Order Qty")); } - - const item = locals[doctype][name]; frm.events.get_item_data(frm, item, false); }, diff --git a/erpnext/stock/doctype/material_request/material_request.json b/erpnext/stock/doctype/material_request/material_request.json index cb46a6c368..413c373e0e 100644 --- a/erpnext/stock/doctype/material_request/material_request.json +++ b/erpnext/stock/doctype/material_request/material_request.json @@ -11,33 +11,40 @@ "naming_series", "title", "material_request_type", - "transfer_status", "customer", - "status", + "company", "column_break_2", "transaction_date", "schedule_date", - "company", "amended_from", "warehouse_section", + "scan_barcode", + "column_break_13", "set_from_warehouse", "column_break5", "set_warehouse", "items_section", - "scan_barcode", "items", - "more_info", - "per_ordered", - "column_break2", - "per_received", - "printing_details", - "letter_head", - "select_print_heading", + "terms_tab", "terms_section_break", "tc_name", "terms", + "more_info_tab", + "status_section", + "status", + "per_ordered", + "column_break2", + "transfer_status", + "per_received", + "printing_details", + "letter_head", + "column_break_31", + "select_print_heading", "reference", - "job_card" + "job_card", + "column_break_35", + "work_order", + "connections_tab" ], "fields": [ { @@ -146,14 +153,6 @@ "options": "Material Request Item", "reqd": 1 }, - { - "collapsible": 1, - "fieldname": "more_info", - "fieldtype": "Section Break", - "label": "More Information", - "oldfieldtype": "Section Break", - "options": "fa fa-file-text" - }, { "default": "Today", "fieldname": "transaction_date", @@ -238,7 +237,6 @@ "collapsible_depends_on": "terms", "fieldname": "terms_section_break", "fieldtype": "Section Break", - "label": "Terms and Conditions", "oldfieldtype": "Section Break", "options": "fa fa-legal" }, @@ -276,10 +274,10 @@ { "fieldname": "warehouse_section", "fieldtype": "Section Break", - "label": "Warehouse" + "hide_border": 1, + "label": "Items" }, { - "description": "Sets 'Target Warehouse' in each row of the Items table.", "fieldname": "set_warehouse", "fieldtype": "Link", "in_list_view": 1, @@ -295,7 +293,6 @@ }, { "depends_on": "eval:doc.material_request_type == 'Material Transfer'", - "description": "Sets 'Source Warehouse' in each row of the Items table.", "fieldname": "set_from_warehouse", "fieldtype": "Link", "label": "Set Source Warehouse", @@ -309,16 +306,60 @@ "label": "Transfer Status", "options": "\nNot Started\nIn Transit\nCompleted", "read_only": 1 + }, + { + "fieldname": "work_order", + "fieldtype": "Link", + "label": "Work Order", + "options": "Work Order", + "read_only": 1 + }, + { + "fieldname": "terms_tab", + "fieldtype": "Tab Break", + "label": "Terms" + }, + { + "fieldname": "more_info_tab", + "fieldtype": "Tab Break", + "label": "More Info" + }, + { + "fieldname": "connections_tab", + "fieldtype": "Tab Break", + "label": "Connections", + "show_dashboard": 1 + }, + { + "collapsible": 1, + "fieldname": "status_section", + "fieldtype": "Section Break", + "label": "Status", + "oldfieldtype": "Section Break", + "options": "fa fa-file-text" + }, + { + "fieldname": "column_break_31", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_35", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_13", + "fieldtype": "Column Break" } ], "icon": "fa fa-ticket", "idx": 70, "is_submittable": 1, "links": [], - "modified": "2021-08-17 20:16:12.737743", + "modified": "2022-09-27 17:58:26.366469", "modified_by": "Administrator", "module": "Stock", "name": "Material Request", + "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ { @@ -386,5 +427,6 @@ "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "title_field": "title" } \ No newline at end of file diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index 2614a7f1f4..6426fe8015 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -10,7 +10,7 @@ import json import frappe from frappe import _, msgprint from frappe.model.mapper import get_mapped_doc -from frappe.utils import cstr, flt, get_link_to_form, getdate, new_line_sep, nowdate +from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, new_line_sep, nowdate from erpnext.buying.utils import check_on_hold_or_closed_status, validate_for_items from erpnext.controllers.buying_controller import BuyingController @@ -22,9 +22,6 @@ form_grid_templates = {"items": "templates/form_grid/material_request_grid.html" class MaterialRequest(BuyingController): - def get_feed(self): - return - def check_if_already_pulled(self): pass @@ -120,7 +117,6 @@ class MaterialRequest(BuyingController): self.title = _("{0} Request for {1}").format(self.material_request_type, items)[:100] def on_submit(self): - # frappe.db.set(self, 'status', 'Submitted') self.update_requested_qty() self.update_requested_qty_in_production_plan() if self.material_request_type == "Purchase": @@ -501,13 +497,13 @@ def get_material_requests_based_on_supplier(doctype, txt, searchfield, start, pa and mr.per_ordered < 99.99 and mr.docstatus = 1 and mr.status != 'Stopped' - and mr.company = '{1}' - {2} + and mr.company = %s + {1} order by mr_item.item_code ASC - limit {3} offset {4} """.format( - ", ".join(["%s"] * len(supplier_items)), filters.get("company"), conditions, page_len, start + limit {2} offset {3} """.format( + ", ".join(["%s"] * len(supplier_items)), conditions, cint(page_len), cint(start) ), - tuple(supplier_items), + tuple(supplier_items) + (filters.get("company"),), as_dict=1, ) @@ -597,7 +593,9 @@ def make_stock_entry(source_name, target_doc=None): if source.material_request_type == "Customer Provided": target.purpose = "Material Receipt" - target.set_missing_values() + target.set_transfer_qty() + target.set_actual_qty() + target.calculate_rate_and_amount(raise_error_if_no_rate=False) target.set_stock_entry_type() target.set_job_card_data() @@ -718,3 +716,14 @@ def create_pick_list(source_name, target_doc=None): doc.set_item_locations() return doc + + +@frappe.whitelist() +def make_in_transit_stock_entry(source_name, in_transit_warehouse): + ste_doc = make_stock_entry(source_name) + ste_doc.add_to_transit = 1 + + for row in ste_doc.items: + row.t_warehouse = in_transit_warehouse + + return ste_doc diff --git a/erpnext/stock/doctype/material_request/material_request_dashboard.py b/erpnext/stock/doctype/material_request/material_request_dashboard.py index b073e6a22e..2bba52a4e2 100644 --- a/erpnext/stock/doctype/material_request/material_request_dashboard.py +++ b/erpnext/stock/doctype/material_request/material_request_dashboard.py @@ -4,12 +4,16 @@ from frappe import _ def get_data(): return { "fieldname": "material_request", + "internal_links": { + "Sales Order": ["items", "sales_order"], + }, "transactions": [ { "label": _("Reference"), - "items": ["Request for Quotation", "Supplier Quotation", "Purchase Order"], + "items": ["Sales Order", "Request for Quotation", "Supplier Quotation", "Purchase Order"], }, {"label": _("Stock"), "items": ["Stock Entry", "Purchase Receipt", "Pick List"]}, {"label": _("Manufacturing"), "items": ["Work Order"]}, + {"label": _("Internal Transfer"), "items": ["Sales Order"]}, ], } diff --git a/erpnext/stock/doctype/material_request/test_material_request.py b/erpnext/stock/doctype/material_request/test_material_request.py index 78af1532ea..a707c74c7d 100644 --- a/erpnext/stock/doctype/material_request/test_material_request.py +++ b/erpnext/stock/doctype/material_request/test_material_request.py @@ -11,6 +11,7 @@ from frappe.utils import flt, today from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.material_request.material_request import ( + make_in_transit_stock_entry, make_purchase_order, make_stock_entry, make_supplier_quotation, @@ -56,6 +57,22 @@ class TestMaterialRequest(FrappeTestCase): self.assertEqual(se.doctype, "Stock Entry") self.assertEqual(len(se.get("items")), len(mr.get("items"))) + def test_in_transit_make_stock_entry(self): + mr = frappe.copy_doc(test_records[0]).insert() + + self.assertRaises(frappe.ValidationError, make_stock_entry, mr.name) + + mr = frappe.get_doc("Material Request", mr.name) + mr.material_request_type = "Material Transfer" + mr.submit() + + in_transit_warehouse = get_in_transit_warehouse(mr.company) + se = make_in_transit_stock_entry(mr.name, in_transit_warehouse) + + self.assertEqual(se.doctype, "Stock Entry") + for row in se.get("items"): + self.assertEqual(row.t_warehouse, in_transit_warehouse) + def _insert_stock_entry(self, qty1, qty2, warehouse=None): se = frappe.get_doc( { @@ -216,7 +233,7 @@ class TestMaterialRequest(FrappeTestCase): po.load_from_db() mr.update_status("Stopped") self.assertRaises(frappe.InvalidStatusError, po.submit) - frappe.db.set(po, "docstatus", 1) + po.db_set("docstatus", 1) self.assertRaises(frappe.InvalidStatusError, po.cancel) # resubmit and check for per complete @@ -590,6 +607,7 @@ class TestMaterialRequest(FrappeTestCase): mr = frappe.copy_doc(test_records[0]) mr.material_request_type = "Material Issue" mr.submit() + frappe.db.value_cache = {} # testing bin value after material request is submitted self.assertEqual(_get_requested_qty(), existing_requested_qty - 54.0) @@ -741,6 +759,36 @@ class TestMaterialRequest(FrappeTestCase): self.assertEqual(existing_requested_qty, current_requested_qty) +def get_in_transit_warehouse(company): + if not frappe.db.exists("Warehouse Type", "Transit"): + frappe.get_doc( + { + "doctype": "Warehouse Type", + "name": "Transit", + } + ).insert() + + in_transit_warehouse = frappe.db.exists( + "Warehouse", {"warehouse_type": "Transit", "company": company} + ) + + if not in_transit_warehouse: + in_transit_warehouse = ( + frappe.get_doc( + { + "doctype": "Warehouse", + "warehouse_name": "Transit", + "warehouse_type": "Transit", + "company": company, + } + ) + .insert() + .name + ) + + return in_transit_warehouse + + def make_material_request(**args): args = frappe._dict(args) mr = frappe.new_doc("Material Request") diff --git a/erpnext/stock/doctype/packed_item/packed_item.py b/erpnext/stock/doctype/packed_item/packed_item.py index 4d05d7a345..dbd8de4fcb 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.py +++ b/erpnext/stock/doctype/packed_item/packed_item.py @@ -48,7 +48,7 @@ def make_packing_list(doc): update_packed_item_from_cancelled_doc(item_row, bundle_item, pi_row, doc) if set_price_from_children: # create/update bundle item wise price dict - update_product_bundle_rate(parent_items_price, pi_row) + update_product_bundle_rate(parent_items_price, pi_row, item_row) if parent_items_price: set_product_bundle_rate_amount(doc, parent_items_price) # set price in bundle item @@ -83,8 +83,8 @@ def reset_packing_list(doc): # 1. items were deleted # 2. if bundle item replaced by another item (same no. of items but different items) # we maintain list to track recurring item rows as well - items_before_save = [item.item_code for item in doc_before_save.get("items")] - items_after_save = [item.item_code for item in doc.get("items")] + items_before_save = [(item.name, item.item_code) for item in doc_before_save.get("items")] + items_after_save = [(item.name, item.item_code) for item in doc.get("items")] reset_table = items_before_save != items_after_save else: # reset: if via Update Items OR @@ -247,7 +247,7 @@ def get_cancelled_doc_packed_item_details(old_packed_items): return prev_doc_packed_items_map -def update_product_bundle_rate(parent_items_price, pi_row): +def update_product_bundle_rate(parent_items_price, pi_row, item_row): """ Update the price dict of Product Bundles based on the rates of the Items in the bundle. @@ -259,7 +259,7 @@ def update_product_bundle_rate(parent_items_price, pi_row): if not rate: parent_items_price[key] = 0.0 - parent_items_price[key] += flt(pi_row.rate) + parent_items_price[key] += flt((pi_row.rate * pi_row.qty) / item_row.stock_qty) def set_product_bundle_rate_amount(doc, parent_items_price): diff --git a/erpnext/stock/doctype/packed_item/test_packed_item.py b/erpnext/stock/doctype/packed_item/test_packed_item.py index ad7fd9a697..ad06732bc3 100644 --- a/erpnext/stock/doctype/packed_item/test_packed_item.py +++ b/erpnext/stock/doctype/packed_item/test_packed_item.py @@ -126,8 +126,8 @@ class TestPackedItem(FrappeTestCase): so.packed_items[1].rate = 200 so.save() - self.assertEqual(so.items[0].rate, 350) - self.assertEqual(so.items[0].amount, 700) + self.assertEqual(so.items[0].rate, 700) + self.assertEqual(so.items[0].amount, 1400) def test_newly_mapped_doc_packed_items(self): "Test impact on packed items in newly mapped DN from SO." diff --git a/erpnext/stock/doctype/pick_list/pick_list.js b/erpnext/stock/doctype/pick_list/pick_list.js index 799406cd79..8213adb89b 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.js +++ b/erpnext/stock/doctype/pick_list/pick_list.js @@ -51,7 +51,15 @@ frappe.ui.form.on('Pick List', { if (!(frm.doc.locations && frm.doc.locations.length)) { frappe.msgprint(__('Add items in the Item Locations table')); } else { - frm.call('set_item_locations', {save: save}); + frappe.call({ + method: "set_item_locations", + doc: frm.doc, + args: { + "save": save, + }, + freeze: 1, + freeze_message: __("Setting Item Locations..."), + }); } }, get_item_locations: (frm) => { diff --git a/erpnext/stock/doctype/pick_list/pick_list.json b/erpnext/stock/doctype/pick_list/pick_list.json index e1c3f0f506..7259dc00a8 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.json +++ b/erpnext/stock/doctype/pick_list/pick_list.json @@ -26,7 +26,8 @@ "locations", "amended_from", "print_settings_section", - "group_same_items" + "group_same_items", + "status" ], "fields": [ { @@ -168,11 +169,26 @@ "fieldtype": "Data", "label": "Customer Name", "read_only": 1 + }, + { + "default": "Draft", + "fieldname": "status", + "fieldtype": "Select", + "hidden": 1, + "in_standard_filter": 1, + "label": "Status", + "no_copy": 1, + "options": "Draft\nOpen\nCompleted\nCancelled", + "print_hide": 1, + "read_only": 1, + "report_hide": 1, + "reqd": 1, + "search_index": 1 } ], "is_submittable": 1, "links": [], - "modified": "2022-07-19 11:03:04.442174", + "modified": "2023-01-24 10:33:43.244476", "modified_by": "Administrator", "module": "Stock", "name": "Pick List", @@ -244,4 +260,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} +} \ No newline at end of file diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index d31d695c80..bf3b5ddc54 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -4,12 +4,15 @@ import json from collections import OrderedDict, defaultdict from itertools import groupby -from typing import Dict, List, Set +from typing import Dict, List import frappe from frappe import _ from frappe.model.document import Document from frappe.model.mapper import map_child_doc +from frappe.query_builder import Case +from frappe.query_builder.custom import GROUP_CONCAT +from frappe.query_builder.functions import Coalesce, IfNull, Locate, Replace, Sum from frappe.utils import cint, floor, flt, today from frappe.utils.nestedset import get_descendants_of @@ -39,7 +42,9 @@ class PickList(Document): ) def before_submit(self): - update_sales_orders = set() + self.validate_picked_items() + + def validate_picked_items(self): for item in self.locations: if self.scan_mode and item.picked_qty < item.stock_qty: frappe.throw( @@ -48,17 +53,14 @@ class PickList(Document): ).format(item.idx, item.stock_qty - item.picked_qty, item.stock_uom), title=_("Pick List Incomplete"), ) - elif not self.scan_mode and item.picked_qty == 0: + + if not self.scan_mode and item.picked_qty == 0: # if the user has not entered any picked qty, set it to stock_qty, before submit item.picked_qty = item.stock_qty - if item.sales_order_item: - # update the picked_qty in SO Item - self.update_sales_order_item(item, item.picked_qty, item.item_code) - update_sales_orders.add(item.sales_order) - if not frappe.get_cached_value("Item", item.item_code, "has_serial_no"): continue + if not item.serial_no: frappe.throw( _("Row #{0}: {1} does not have any available serial numbers in {2}").format( @@ -66,62 +68,119 @@ class PickList(Document): ), title=_("Serial Nos Required"), ) - if len(item.serial_no.split("\n")) == item.picked_qty: - continue - frappe.throw( - _( - "For item {0} at row {1}, count of serial numbers does not match with the picked quantity" - ).format(frappe.bold(item.item_code), frappe.bold(item.idx)), - title=_("Quantity Mismatch"), - ) - self.update_bundle_picked_qty() - self.update_sales_order_picking_status(update_sales_orders) - - def before_cancel(self): - """Deduct picked qty on cancelling pick list""" - updated_sales_orders = set() - - for item in self.get("locations"): - if item.sales_order_item: - self.update_sales_order_item(item, -1 * item.picked_qty, item.item_code) - updated_sales_orders.add(item.sales_order) - - self.update_bundle_picked_qty() - self.update_sales_order_picking_status(updated_sales_orders) - - def update_sales_order_item(self, item, picked_qty, item_code): - item_table = "Sales Order Item" if not item.product_bundle_item else "Packed Item" - stock_qty_field = "stock_qty" if not item.product_bundle_item else "qty" - - already_picked, actual_qty = frappe.db.get_value( - item_table, - item.sales_order_item, - ["picked_qty", stock_qty_field], - ) - - if self.docstatus == 1: - if (((already_picked + picked_qty) / actual_qty) * 100) > ( - 100 + flt(frappe.db.get_single_value("Stock Settings", "over_delivery_receipt_allowance")) - ): + if len(item.serial_no.split("\n")) != item.picked_qty: frappe.throw( _( - "You are picking more than required quantity for {}. Check if there is any other pick list created for {}" - ).format(item_code, item.sales_order) + "For item {0} at row {1}, count of serial numbers does not match with the picked quantity" + ).format(frappe.bold(item.item_code), frappe.bold(item.idx)), + title=_("Quantity Mismatch"), ) - frappe.db.set_value(item_table, item.sales_order_item, "picked_qty", already_picked + picked_qty) + def on_submit(self): + self.update_status() + self.update_bundle_picked_qty() + self.update_reference_qty() + self.update_sales_order_picking_status() + + def on_cancel(self): + self.update_status() + self.update_bundle_picked_qty() + self.update_reference_qty() + self.update_sales_order_picking_status() + + def update_status(self, status=None, update_modified=True): + if not status: + if self.docstatus == 0: + status = "Draft" + elif self.docstatus == 1: + if self.status == "Draft": + status = "Open" + elif target_document_exists(self.name, self.purpose): + status = "Completed" + elif self.docstatus == 2: + status = "Cancelled" + + if status: + frappe.db.set_value("Pick List", self.name, "status", status, update_modified=update_modified) + + def update_reference_qty(self): + packed_items = [] + so_items = [] + + for item in self.locations: + if item.product_bundle_item: + packed_items.append(item.sales_order_item) + elif item.sales_order_item: + so_items.append(item.sales_order_item) + + if packed_items: + self.update_packed_items_qty(packed_items) + + if so_items: + self.update_sales_order_item_qty(so_items) + + def update_packed_items_qty(self, packed_items): + picked_items = get_picked_items_qty(packed_items) + self.validate_picked_qty(picked_items) + + picked_qty = frappe._dict() + for d in picked_items: + picked_qty[d.sales_order_item] = d.picked_qty + + for packed_item in packed_items: + frappe.db.set_value( + "Packed Item", + packed_item, + "picked_qty", + flt(picked_qty.get(packed_item)), + update_modified=False, + ) + + def update_sales_order_item_qty(self, so_items): + picked_items = get_picked_items_qty(so_items) + self.validate_picked_qty(picked_items) + + picked_qty = frappe._dict() + for d in picked_items: + picked_qty[d.sales_order_item] = d.picked_qty + + for so_item in so_items: + frappe.db.set_value( + "Sales Order Item", + so_item, + "picked_qty", + flt(picked_qty.get(so_item)), + update_modified=False, + ) + + def update_sales_order_picking_status(self) -> None: + sales_orders = [] + for row in self.locations: + if row.sales_order and row.sales_order not in sales_orders: + sales_orders.append(row.sales_order) - @staticmethod - def update_sales_order_picking_status(sales_orders: Set[str]) -> None: for sales_order in sales_orders: - if sales_order: - frappe.get_doc("Sales Order", sales_order).update_picking_status() + frappe.get_doc("Sales Order", sales_order, for_update=True).update_picking_status() + + def validate_picked_qty(self, data): + over_delivery_receipt_allowance = 100 + flt( + frappe.db.get_single_value("Stock Settings", "over_delivery_receipt_allowance") + ) + + for row in data: + if (row.picked_qty / row.stock_qty) * 100 > over_delivery_receipt_allowance: + frappe.throw( + _( + f"You are picking more than required quantity for the item {row.item_code}. Check if there is any other pick list created for the sales order {row.sales_order}." + ) + ) @frappe.whitelist() def set_item_locations(self, save=False): self.validate_for_qty() items = self.aggregate_item_qty() + picked_items_details = self.get_picked_items_details(items) self.item_location_map = frappe._dict() from_warehouses = None @@ -133,13 +192,18 @@ class PickList(Document): # reset self.delete_key("locations") + updated_locations = frappe._dict() for item_doc in items: item_code = item_doc.item_code self.item_location_map.setdefault( item_code, get_available_item_locations( - item_code, from_warehouses, self.item_count_map.get(item_code), self.company + item_code, + from_warehouses, + self.item_count_map.get(item_code), + self.company, + picked_item_details=picked_items_details.get(item_code), ), ) @@ -153,7 +217,26 @@ class PickList(Document): for row in locations: location = item_doc.as_dict() location.update(row) - self.append("locations", location) + key = ( + location.item_code, + location.warehouse, + location.uom, + location.batch_no, + location.serial_no, + location.sales_order_item or location.material_request_item, + ) + + if key not in updated_locations: + updated_locations.setdefault(key, location) + else: + updated_locations[key].qty += location.qty + updated_locations[key].stock_qty += location.stock_qty + + for location in updated_locations.values(): + if location.picked_qty > location.stock_qty: + location.picked_qty = location.stock_qty + + self.append("locations", location) # If table is empty on update after submit, set stock_qty, picked_qty to 0 so that indicator is red # and give feedback to the user. This is to avoid empty Pick Lists. @@ -183,20 +266,20 @@ class PickList(Document): frappe.throw("Row #{0}: Item Code is Mandatory".format(item.idx)) item_code = item.item_code reference = item.sales_order_item or item.material_request_item - key = (item_code, item.uom, reference) + key = (item_code, item.uom, item.warehouse, item.batch_no, reference) item.idx = None item.name = None if item_map.get(key): item_map[key].qty += item.qty - item_map[key].stock_qty += item.stock_qty + item_map[key].stock_qty += flt(item.stock_qty, item.precision("stock_qty")) else: item_map[key] = item # maintain count of each item (useful to limit get query) self.item_count_map.setdefault(item_code, 0) - self.item_count_map[item_code] += item.stock_qty + self.item_count_map[item_code] += flt(item.stock_qty, item.precision("stock_qty")) return item_map.values() @@ -207,7 +290,8 @@ class PickList(Document): frappe.throw(_("Qty of Finished Goods Item should be greater than 0.")) def before_print(self, settings=None): - self.group_similar_items() + if self.group_same_items: + self.group_similar_items() def group_similar_items(self): group_item_qty = defaultdict(float) @@ -240,7 +324,7 @@ class PickList(Document): for so_row, item_code in product_bundles.items(): picked_qty = self._compute_picked_qty_for_bundle(so_row, product_bundle_qty_map[item_code]) item_table = "Sales Order Item" - already_picked = frappe.db.get_value(item_table, so_row, "picked_qty") + already_picked = frappe.db.get_value(item_table, so_row, "picked_qty", for_update=True) frappe.db.set_value( item_table, so_row, @@ -248,6 +332,56 @@ class PickList(Document): already_picked + (picked_qty * (1 if self.docstatus == 1 else -1)), ) + def get_picked_items_details(self, items): + picked_items = frappe._dict() + + if items: + pi = frappe.qb.DocType("Pick List") + pi_item = frappe.qb.DocType("Pick List Item") + query = ( + frappe.qb.from_(pi) + .inner_join(pi_item) + .on(pi.name == pi_item.parent) + .select( + pi_item.item_code, + pi_item.warehouse, + pi_item.batch_no, + Sum(Case().when(pi_item.picked_qty > 0, pi_item.picked_qty).else_(pi_item.stock_qty)).as_( + "picked_qty" + ), + Replace(GROUP_CONCAT(pi_item.serial_no), ",", "\n").as_("serial_no"), + ) + .where( + (pi_item.item_code.isin([x.item_code for x in items])) + & ((pi_item.picked_qty > 0) | (pi_item.stock_qty > 0)) + & (pi.status != "Completed") + & (pi_item.docstatus != 2) + ) + .groupby( + pi_item.item_code, + pi_item.warehouse, + pi_item.batch_no, + ) + ) + + if self.name: + query = query.where(pi_item.parent != self.name) + + items_data = query.run(as_dict=True) + + for item_data in items_data: + key = (item_data.warehouse, item_data.batch_no) if item_data.batch_no else item_data.warehouse + serial_no = [x for x in item_data.serial_no.split("\n") if x] if item_data.serial_no else None + data = {"picked_qty": item_data.picked_qty} + if serial_no: + data["serial_no"] = serial_no + if item_data.item_code not in picked_items: + picked_items[item_data.item_code] = {key: data} + else: + picked_items[item_data.item_code][key] = data + + return picked_items + def _get_product_bundles(self) -> Dict[str, str]: # Dict[so_item_row: item_code] product_bundles = {} @@ -285,6 +419,32 @@ class PickList(Document): return int(flt(min(possible_bundles), precision or 6)) +def update_pick_list_status(pick_list): + if pick_list: + doc = frappe.get_doc("Pick List", pick_list) + doc.run_method("update_status") + + +def get_picked_items_qty(items) -> List[Dict]: + pi_item = frappe.qb.DocType("Pick List Item") + return ( + frappe.qb.from_(pi_item) + .select( + pi_item.sales_order_item, + pi_item.item_code, + pi_item.sales_order, + Sum(pi_item.stock_qty).as_("stock_qty"), + Sum(pi_item.picked_qty).as_("picked_qty"), + ) + .where((pi_item.docstatus == 1) & (pi_item.sales_order_item.isin(items))) + .groupby( + pi_item.sales_order_item, + pi_item.sales_order, + ) + .for_update() + ).run(as_dict=True) + + def validate_item_locations(pick_list): if not pick_list.locations: frappe.throw(_("Add items in the Item Locations table")) @@ -348,31 +508,38 @@ def get_items_with_location_and_quantity(item_doc, item_location_map, docstatus) def get_available_item_locations( - item_code, from_warehouses, required_qty, company, ignore_validation=False + item_code, + from_warehouses, + required_qty, + company, + ignore_validation=False, + picked_item_details=None, ): locations = [] + total_picked_qty = ( + sum([v.get("picked_qty") for k, v in picked_item_details.items()]) if picked_item_details else 0 + ) has_serial_no = frappe.get_cached_value("Item", item_code, "has_serial_no") has_batch_no = frappe.get_cached_value("Item", item_code, "has_batch_no") if has_batch_no and has_serial_no: locations = get_available_item_locations_for_serial_and_batched_item( - item_code, from_warehouses, required_qty, company + item_code, from_warehouses, required_qty, company, total_picked_qty ) elif has_serial_no: locations = get_available_item_locations_for_serialized_item( - item_code, from_warehouses, required_qty, company + item_code, from_warehouses, required_qty, company, total_picked_qty ) elif has_batch_no: locations = get_available_item_locations_for_batched_item( - item_code, from_warehouses, required_qty, company + item_code, from_warehouses, required_qty, company, total_picked_qty ) else: locations = get_available_item_locations_for_other_item( - item_code, from_warehouses, required_qty, company + item_code, from_warehouses, required_qty, company, total_picked_qty ) total_qty_available = sum(location.get("qty") for location in locations) - remaining_qty = required_qty - total_qty_available if remaining_qty > 0 and not ignore_validation: @@ -383,25 +550,60 @@ def get_available_item_locations( title=_("Insufficient Stock"), ) + if picked_item_details: + for location in list(locations): + key = ( + (location["warehouse"], location["batch_no"]) + if location.get("batch_no") + else location["warehouse"] + ) + + if key in picked_item_details: + picked_detail = picked_item_details[key] + + if picked_detail.get("serial_no") and location.get("serial_no"): + location["serial_no"] = list( + set(location["serial_no"]).difference(set(picked_detail["serial_no"])) + ) + location["qty"] = len(location["serial_no"]) + else: + location["qty"] -= picked_detail.get("picked_qty") + + if location["qty"] < 1: + locations.remove(location) + + total_qty_available = sum(location.get("qty") for location in locations) + remaining_qty = required_qty - total_qty_available + + if remaining_qty > 0 and not ignore_validation: + frappe.msgprint( + _("{0} units of Item {1} is picked in another Pick List.").format( + remaining_qty, frappe.get_desk_link("Item", item_code) + ), + title=_("Already Picked"), + ) + return locations def get_available_item_locations_for_serialized_item( - item_code, from_warehouses, required_qty, company + item_code, from_warehouses, required_qty, company, total_picked_qty=0 ): - filters = frappe._dict({"item_code": item_code, "company": company, "warehouse": ["!=", ""]}) + sn = frappe.qb.DocType("Serial No") + query = ( + frappe.qb.from_(sn) + .select(sn.name, sn.warehouse) + .where((sn.item_code == item_code) & (sn.company == company)) + .orderby(sn.purchase_date) + .limit(cint(required_qty + total_picked_qty)) + ) if from_warehouses: - filters.warehouse = ["in", from_warehouses] + query = query.where(sn.warehouse.isin(from_warehouses)) + else: + query = query.where(Coalesce(sn.warehouse, "") != "") - serial_nos = frappe.get_all( - "Serial No", - fields=["name", "warehouse"], - filters=filters, - limit=required_qty, - order_by="purchase_date", - as_list=1, - ) + serial_nos = query.run(as_list=True) warehouse_serial_nos_map = frappe._dict() for serial_no, warehouse in serial_nos: @@ -415,94 +617,88 @@ def get_available_item_locations_for_serialized_item( def get_available_item_locations_for_batched_item( - item_code, from_warehouses, required_qty, company + item_code, from_warehouses, required_qty, company, total_picked_qty=0 ): - warehouse_condition = "and warehouse in %(warehouses)s" if from_warehouses else "" - batch_locations = frappe.db.sql( - """ - SELECT - sle.`warehouse`, - sle.`batch_no`, - SUM(sle.`actual_qty`) AS `qty` - FROM - `tabStock Ledger Entry` sle, `tabBatch` batch - WHERE - sle.batch_no = batch.name - and sle.`item_code`=%(item_code)s - and sle.`company` = %(company)s - and batch.disabled = 0 - and sle.is_cancelled=0 - and IFNULL(batch.`expiry_date`, '2200-01-01') > %(today)s - {warehouse_condition} - GROUP BY - sle.`warehouse`, - sle.`batch_no`, - sle.`item_code` - HAVING `qty` > 0 - ORDER BY IFNULL(batch.`expiry_date`, '2200-01-01'), batch.`creation` - """.format( - warehouse_condition=warehouse_condition - ), - { # nosec - "item_code": item_code, - "company": company, - "today": today(), - "warehouses": from_warehouses, - }, - as_dict=1, + sle = frappe.qb.DocType("Stock Ledger Entry") + batch = frappe.qb.DocType("Batch") + + query = ( + frappe.qb.from_(sle) + .from_(batch) + .select(sle.warehouse, sle.batch_no, Sum(sle.actual_qty).as_("qty")) + .where( + (sle.batch_no == batch.name) + & (sle.item_code == item_code) + & (sle.company == company) + & (batch.disabled == 0) + & (sle.is_cancelled == 0) + & (IfNull(batch.expiry_date, "2200-01-01") > today()) + ) + .groupby(sle.warehouse, sle.batch_no, sle.item_code) + .having(Sum(sle.actual_qty) > 0) + .orderby(IfNull(batch.expiry_date, "2200-01-01"), batch.creation, sle.batch_no, sle.warehouse) + .limit(cint(required_qty + total_picked_qty)) ) - return batch_locations + if from_warehouses: + query = query.where(sle.warehouse.isin(from_warehouses)) + + return query.run(as_dict=True) def get_available_item_locations_for_serial_and_batched_item( - item_code, from_warehouses, required_qty, company + item_code, from_warehouses, required_qty, company, total_picked_qty=0 ): # Get batch nos by FIFO locations = get_available_item_locations_for_batched_item( item_code, from_warehouses, required_qty, company ) - filters = frappe._dict( - {"item_code": item_code, "company": company, "warehouse": ["!=", ""], "batch_no": ""} - ) + if locations: + sn = frappe.qb.DocType("Serial No") + conditions = (sn.item_code == item_code) & (sn.company == company) - # Get Serial Nos by FIFO for Batch No - for location in locations: - filters.batch_no = location.batch_no - filters.warehouse = location.warehouse - location.qty = ( - required_qty if location.qty > required_qty else location.qty - ) # if extra qty in batch + for location in locations: + location.qty = ( + required_qty if location.qty > required_qty else location.qty + ) # if extra qty in batch - serial_nos = frappe.get_list( - "Serial No", fields=["name"], filters=filters, limit=location.qty, order_by="purchase_date" - ) + serial_nos = ( + frappe.qb.from_(sn) + .select(sn.name) + .where( + (conditions) & (sn.batch_no == location.batch_no) & (sn.warehouse == location.warehouse) + ) + .orderby(sn.purchase_date) + .limit(cint(location.qty + total_picked_qty)) + ).run(as_dict=True) - serial_nos = [sn.name for sn in serial_nos] - location.serial_no = serial_nos + serial_nos = [sn.name for sn in serial_nos] + location.serial_no = serial_nos + location.qty = len(serial_nos) return locations -def get_available_item_locations_for_other_item(item_code, from_warehouses, required_qty, company): - # gets all items available in different warehouses - warehouses = [x.get("name") for x in frappe.get_list("Warehouse", {"company": company}, "name")] - - filters = frappe._dict( - {"item_code": item_code, "warehouse": ["in", warehouses], "actual_qty": [">", 0]} +def get_available_item_locations_for_other_item( + item_code, from_warehouses, required_qty, company, total_picked_qty=0 +): + bin = frappe.qb.DocType("Bin") + query = ( + frappe.qb.from_(bin) + .select(bin.warehouse, bin.actual_qty.as_("qty")) + .where((bin.item_code == item_code) & (bin.actual_qty > 0)) + .orderby(bin.creation) + .limit(cint(required_qty + total_picked_qty)) ) if from_warehouses: - filters.warehouse = ["in", from_warehouses] + query = query.where(bin.warehouse.isin(from_warehouses)) + else: + wh = frappe.qb.DocType("Warehouse") + query = query.from_(wh).where((bin.warehouse == wh.name) & (wh.company == company)) - item_locations = frappe.get_all( - "Bin", - fields=["warehouse", "actual_qty as qty"], - filters=filters, - limit=required_qty, - order_by="creation", - ) + item_locations = query.run(as_dict=True) return item_locations @@ -686,31 +882,22 @@ def create_stock_entry(pick_list): @frappe.whitelist() def get_pending_work_orders(doctype, txt, searchfield, start, page_length, filters, as_dict): - return frappe.db.sql( - """ - SELECT - `name`, `company`, `planned_start_date` - FROM - `tabWork Order` - WHERE - `status` not in ('Completed', 'Stopped') - AND `qty` > `material_transferred_for_manufacturing` - AND `docstatus` = 1 - AND `company` = %(company)s - AND `name` like %(txt)s - ORDER BY - (case when locate(%(_txt)s, name) > 0 then locate(%(_txt)s, name) else 99999 end) name - LIMIT - %(start)s, %(page_length)s""", - { - "txt": "%%%s%%" % txt, - "_txt": txt.replace("%", ""), - "start": start, - "page_length": frappe.utils.cint(page_length), - "company": filters.get("company"), - }, - as_dict=as_dict, - ) + wo = frappe.qb.DocType("Work Order") + return ( + frappe.qb.from_(wo) + .select(wo.name, wo.company, wo.planned_start_date) + .where( + (wo.status.notin(["Completed", "Stopped"])) + & (wo.qty > wo.material_transferred_for_manufacturing) + & (wo.docstatus == 1) + & (wo.company == filters.get("company")) + & (wo.name.like("%{0}%".format(txt))) + ) + .orderby(Case().when(Locate(txt, wo.name) > 0, Locate(txt, wo.name)).else_(99999)) + .orderby(wo.name) + .limit(cint(page_length)) + .offset(start) + ).run(as_dict=as_dict) @frappe.whitelist() diff --git a/erpnext/stock/doctype/pick_list/pick_list_dashboard.py b/erpnext/stock/doctype/pick_list/pick_list_dashboard.py index 92e57bed22..7fbcbafbac 100644 --- a/erpnext/stock/doctype/pick_list/pick_list_dashboard.py +++ b/erpnext/stock/doctype/pick_list/pick_list_dashboard.py @@ -1,7 +1,10 @@ def get_data(): return { "fieldname": "pick_list", + "internal_links": { + "Sales Order": ["locations", "sales_order"], + }, "transactions": [ - {"items": ["Stock Entry", "Delivery Note"]}, + {"items": ["Stock Entry", "Sales Order", "Delivery Note"]}, ], } diff --git a/erpnext/stock/doctype/pick_list/pick_list_list.js b/erpnext/stock/doctype/pick_list/pick_list_list.js new file mode 100644 index 0000000000..ad88b0a682 --- /dev/null +++ b/erpnext/stock/doctype/pick_list/pick_list_list.js @@ -0,0 +1,14 @@ +// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.listview_settings['Pick List'] = { + get_indicator: function (doc) { + const status_colors = { + "Draft": "grey", + "Open": "orange", + "Completed": "green", + "Cancelled": "red", + }; + return [__(doc.status), status_colors[doc.status], "status,=," + doc.status]; + }, +}; \ No newline at end of file diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py index f552299806..1254fe3927 100644 --- a/erpnext/stock/doctype/pick_list/test_pick_list.py +++ b/erpnext/stock/doctype/pick_list/test_pick_list.py @@ -414,6 +414,7 @@ class TestPickList(FrappeTestCase): pick_list.submit() delivery_note = create_delivery_note(pick_list.name) + pick_list.load_from_db() self.assertEqual(pick_list.locations[0].qty, delivery_note.items[0].qty) self.assertEqual(pick_list.locations[1].qty, delivery_note.items[1].qty) @@ -445,10 +446,10 @@ class TestPickList(FrappeTestCase): pl.before_print() self.assertEqual(len(pl.locations), 4) - # grouping should halve the number of items + # grouping should not happen if group_same_items is False pl = frappe.get_doc( doctype="Pick List", - group_same_items=True, + group_same_items=False, locations=[ _dict(item_code="A", warehouse="X", qty=5, picked_qty=1), _dict(item_code="B", warehouse="Y", qty=4, picked_qty=2), @@ -457,6 +458,11 @@ class TestPickList(FrappeTestCase): ], ) pl.before_print() + self.assertEqual(len(pl.locations), 4) + + # grouping should halve the number of items + pl.group_same_items = True + pl.before_print() self.assertEqual(len(pl.locations), 2) expected_items = [ @@ -658,3 +664,147 @@ class TestPickList(FrappeTestCase): self.assertEqual(dn.items[0].rate, 42) so.reload() self.assertEqual(so.per_delivered, 100) + + def test_pick_list_status(self): + warehouse = "_Test Warehouse - _TC" + item = make_item(properties={"maintain_stock": 1}).name + make_stock_entry(item=item, to_warehouse=warehouse, qty=10) + + so = make_sales_order(item_code=item, qty=10, rate=100) + + pl = create_pick_list(so.name) + pl.save() + pl.reload() + self.assertEqual(pl.status, "Draft") + + pl.submit() + pl.reload() + self.assertEqual(pl.status, "Open") + + dn = create_delivery_note(pl.name) + dn.save() + pl.reload() + self.assertEqual(pl.status, "Open") + + dn.submit() + pl.reload() + self.assertEqual(pl.status, "Completed") + + dn.cancel() + pl.reload() + self.assertEqual(pl.status, "Completed") + + pl.cancel() + pl.reload() + self.assertEqual(pl.status, "Cancelled") + + def test_consider_existing_pick_list(self): + def create_items(items_properties): + items = [] + + for properties in items_properties: + properties.update({"maintain_stock": 1}) + item_code = make_item(properties=properties).name + properties.update({"item_code": item_code}) + items.append(properties) + + return items + + def create_stock_entries(items): + warehouses = ["Stores - _TC", "Finished Goods - _TC"] + + for item in items: + for warehouse in warehouses: + se = make_stock_entry( + item=item.get("item_code"), + to_warehouse=warehouse, + qty=5, + ) + + def get_item_list(items, qty, warehouse="All Warehouses - _TC"): + return [ + { + "item_code": item.get("item_code"), + "qty": qty, + "warehouse": warehouse, + } + for item in items + ] + + def get_picked_items_details(pick_list_doc): + items_data = {} + + for location in pick_list_doc.locations: + key = (location.warehouse, location.batch_no) if location.batch_no else location.warehouse + serial_no = [x for x in location.serial_no.split("\n") if x] if location.serial_no else None + data = {"picked_qty": location.picked_qty} + if serial_no: + data["serial_no"] = serial_no + if location.item_code not in items_data: + items_data[location.item_code] = {key: data} + else: + items_data[location.item_code][key] = data + + return items_data + + # Step - 1: Setup - Create Items and Stock Entries + items_properties = [ + { + "valuation_rate": 100, + }, + { + "valuation_rate": 200, + "has_batch_no": 1, + "create_new_batch": 1, + }, + { + "valuation_rate": 300, + "has_serial_no": 1, + "serial_no_series": "SNO.###", + }, + { + "valuation_rate": 400, + "has_batch_no": 1, + "create_new_batch": 1, + "has_serial_no": 1, + "serial_no_series": "SNO.###", + }, + ] + + items = create_items(items_properties) + create_stock_entries(items) + + # Step - 2: Create Sales Order [1] + so1 = make_sales_order(item_list=get_item_list(items, qty=6)) + + # Step - 3: Create and Submit Pick List [1] for Sales Order [1] + pl1 = create_pick_list(so1.name) + pl1.submit() + + # Step - 4: Create Sales Order [2] with same Item(s) as Sales Order [1] + so2 = make_sales_order(item_list=get_item_list(items, qty=4)) + + # Step - 5: Create Pick List [2] for Sales Order [2] + pl2 = create_pick_list(so2.name) + pl2.save() + + # Step - 6: Assert + picked_items_details = get_picked_items_details(pl1) + + for location in pl2.locations: + key = (location.warehouse, location.batch_no) if location.batch_no else location.warehouse + item_data = picked_items_details.get(location.item_code, {}).get(key, {}) + picked_qty = item_data.get("picked_qty", 0) + picked_serial_no = picked_items_details.get("serial_no", []) + bin_actual_qty = frappe.db.get_value( + "Bin", {"item_code": location.item_code, "warehouse": location.warehouse}, "actual_qty" + ) + + # Available Qty to pick should be equal to [Actual Qty - Picked Qty] + self.assertEqual(location.stock_qty, bin_actual_qty - picked_qty) + + # Serial No should not be in the Picked Serial No list + if location.serial_no: + a = set(picked_serial_no) + b = set([x for x in location.serial_no.split("\n") if x]) + self.assertSetEqual(b, b.difference(a)) diff --git a/erpnext/stock/doctype/price_list/test_records.json b/erpnext/stock/doctype/price_list/test_records.json index 7ca949c402..e02a7adbd8 100644 --- a/erpnext/stock/doctype/price_list/test_records.json +++ b/erpnext/stock/doctype/price_list/test_records.json @@ -31,5 +31,21 @@ "enabled": 1, "price_list_name": "_Test Price List Rest of the World", "selling": 1 + }, + { + "buying": 0, + "currency": "USD", + "doctype": "Price List", + "enabled": 1, + "price_list_name": "_Test Selling Price List", + "selling": 1 + }, + { + "buying": 1, + "currency": "USD", + "doctype": "Price List", + "enabled": 1, + "price_list_name": "_Test Buying Price List", + "selling": 0 } ] diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json index a70415dfc3..8f043585b8 100755 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json @@ -17,10 +17,11 @@ "supplier_name", "supplier_delivery_note", "column_break1", - "company", "posting_date", "posting_time", "set_posting_time", + "column_break_12", + "company", "apply_putaway_rule", "is_return", "return_against", @@ -28,18 +29,6 @@ "cost_center", "dimension_col_break", "project", - "section_addresses", - "supplier_address", - "contact_person", - "address_display", - "contact_display", - "contact_mobile", - "contact_email", - "col_break_address", - "shipping_address", - "shipping_address_display", - "billing_address", - "billing_address_display", "currency_and_price_list", "currency", "conversion_rate", @@ -49,37 +38,35 @@ "plc_conversion_rate", "ignore_pricing_rule", "sec_warehouse", + "scan_barcode", + "column_break_31", "set_warehouse", - "rejected_warehouse", - "col_break_warehouse", "set_from_warehouse", + "col_break_warehouse", + "rejected_warehouse", "is_subcontracted", "supplier_warehouse", "items_section", - "scan_barcode", "items", "section_break0", "total_qty", + "total_net_weight", + "column_break_43", "base_total", "base_net_total", "column_break_27", - "total_net_weight", "total", "net_total", - "pricing_rule_details", - "pricing_rules", - "raw_material_details", - "get_current_stock", - "supplied_items", "taxes_charges_section", "tax_category", + "taxes_and_charges", "shipping_col", "shipping_rule", + "column_break_53", + "incoterm", + "named_place", "taxes_section", - "taxes_and_charges", "taxes", - "sec_tax_breakup", - "other_charges_calculation", "totals", "base_taxes_and_charges_added", "base_taxes_and_charges_deducted", @@ -88,53 +75,81 @@ "taxes_and_charges_added", "taxes_and_charges_deducted", "total_taxes_and_charges", - "section_break_42", - "apply_discount_on", - "base_discount_amount", - "column_break_44", - "additional_discount_percentage", - "discount_amount", "section_break_46", "base_grand_total", "base_rounding_adjustment", - "base_in_words", "base_rounded_total", + "base_in_words", "column_break_50", "grand_total", "rounding_adjustment", "rounded_total", "in_words", "disable_rounded_total", - "terms_section_break", + "section_break_42", + "apply_discount_on", + "base_discount_amount", + "column_break_44", + "additional_discount_percentage", + "discount_amount", + "sec_tax_breakup", + "other_charges_calculation", + "pricing_rule_details", + "pricing_rules", + "raw_material_details", + "get_current_stock", + "supplied_items", + "address_and_contact_tab", + "section_addresses", + "supplier_address", + "address_display", + "col_break_address", + "contact_person", + "contact_display", + "contact_mobile", + "contact_email", + "section_break_98", + "shipping_address", + "column_break_100", + "shipping_address_display", + "billing_address_section", + "billing_address", + "column_break_104", + "billing_address_display", + "terms_tab", "tc_name", "terms", - "more_info", + "more_info_tab", + "status_section", "status", - "amended_from", - "range", "column_break4", "per_billed", "per_returned", - "is_internal_supplier", - "inter_company_reference", - "represents_company", "subscription_detail", "auto_repeat", "printing_settings", "letter_head", - "language", - "instructions", + "group_same_items", "column_break_97", "select_print_heading", - "other_details", - "remarks", - "group_same_items", + "language", "transporter_info", "transporter_name", "column_break5", "lr_no", "lr_date", - "is_old_subcontracting_flow" + "additional_info_section", + "instructions", + "is_internal_supplier", + "represents_company", + "inter_company_reference", + "column_break_131", + "remarks", + "range", + "amended_from", + "is_old_subcontracting_flow", + "other_details", + "connections_tab" ], "fields": [ { @@ -223,7 +238,6 @@ "width": "100px" }, { - "description": "Time at which materials were received", "fieldname": "posting_time", "fieldtype": "Time", "label": "Posting Time", @@ -277,15 +291,14 @@ "read_only": 1 }, { - "collapsible": 1, "fieldname": "section_addresses", "fieldtype": "Section Break", - "label": "Address and Contact" + "label": "Supplier Address" }, { "fieldname": "supplier_address", "fieldtype": "Link", - "label": "Select Supplier Address", + "label": "Supplier Address", "options": "Address", "print_hide": 1 }, @@ -330,7 +343,7 @@ { "fieldname": "shipping_address", "fieldtype": "Link", - "label": "Select Shipping Address", + "label": "Shipping Address Template", "options": "Address", "print_hide": 1 }, @@ -405,16 +418,16 @@ "fieldname": "ignore_pricing_rule", "fieldtype": "Check", "label": "Ignore Pricing Rule", - "no_copy": 1, "permlevel": 1, "print_hide": 1 }, { "fieldname": "sec_warehouse", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "hide_border": 1, + "label": "Items" }, { - "description": "Sets 'Accepted Warehouse' in each row of the items table.", "fieldname": "set_warehouse", "fieldtype": "Link", "label": "Accepted Warehouse", @@ -422,7 +435,6 @@ "print_hide": 1 }, { - "description": "Sets 'Rejected Warehouse' in each row of the items table.", "fieldname": "rejected_warehouse", "fieldtype": "Link", "label": "Rejected Warehouse", @@ -462,6 +474,7 @@ { "fieldname": "items_section", "fieldtype": "Section Break", + "hide_border": 1, "oldfieldtype": "Section Break", "options": "fa fa-shopping-cart" }, @@ -572,6 +585,7 @@ "read_only": 1 }, { + "depends_on": "total_net_weight", "fieldname": "total_net_weight", "fieldtype": "Float", "label": "Total Net Weight", @@ -579,11 +593,11 @@ "read_only": 1 }, { - "description": "Add / Edit Taxes and Charges", "fieldname": "taxes_charges_section", "fieldtype": "Section Break", - "oldfieldtype": "Section Break", - "options": "fa fa-money" + "hide_border": 1, + "label": "Taxes and Charges", + "oldfieldtype": "Section Break" }, { "fieldname": "tax_category", @@ -604,7 +618,8 @@ }, { "fieldname": "taxes_section", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "hide_border": 1 }, { "fieldname": "taxes_and_charges", @@ -710,7 +725,6 @@ }, { "collapsible": 1, - "collapsible_depends_on": "discount_amount", "fieldname": "section_break_42", "fieldtype": "Section Break", "label": "Additional Discount" @@ -750,7 +764,8 @@ }, { "fieldname": "section_break_46", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "label": "Totals" }, { "fieldname": "base_grand_total", @@ -842,15 +857,6 @@ "fieldtype": "Check", "label": "Disable Rounded Total" }, - { - "collapsible": 1, - "collapsible_depends_on": "terms", - "fieldname": "terms_section_break", - "fieldtype": "Section Break", - "label": "Terms and Conditions", - "oldfieldtype": "Section Break", - "options": "fa fa-legal" - }, { "fieldname": "tc_name", "fieldtype": "Link", @@ -867,14 +873,6 @@ "oldfieldname": "terms", "oldfieldtype": "Text Editor" }, - { - "collapsible": 1, - "fieldname": "more_info", - "fieldtype": "Section Break", - "label": "More Information", - "oldfieldtype": "Section Break", - "options": "fa fa-file-text" - }, { "default": "Draft", "fieldname": "status", @@ -925,7 +923,6 @@ "width": "50%" }, { - "description": "Track this Purchase Receipt against any Project", "fieldname": "project", "fieldtype": "Link", "label": "Project", @@ -942,7 +939,7 @@ { "fieldname": "subscription_detail", "fieldtype": "Section Break", - "label": "Auto Repeat Detail" + "label": "Auto Repeat" }, { "fieldname": "auto_repeat", @@ -1026,7 +1023,7 @@ "collapsible_depends_on": "transporter_name", "fieldname": "transporter_info", "fieldtype": "Section Break", - "label": "Transporter Details", + "label": "Transporter", "options": "fa fa-truck" }, { @@ -1088,7 +1085,7 @@ { "fieldname": "billing_address", "fieldtype": "Link", - "label": "Select Billing Address", + "label": "Billing Address", "options": "Address" }, { @@ -1114,7 +1111,6 @@ }, { "depends_on": "eval: doc.is_internal_supplier", - "description": "Sets 'From Warehouse' in each row of the items table.", "fieldname": "set_from_warehouse", "fieldtype": "Link", "label": "Set From Warehouse", @@ -1152,13 +1148,97 @@ "hidden": 1, "label": "Is Old Subcontracting Flow", "read_only": 1 + }, + { + "fieldname": "address_and_contact_tab", + "fieldtype": "Tab Break", + "label": "Address & Contact" + }, + { + "fieldname": "terms_tab", + "fieldtype": "Tab Break", + "label": "Terms" + }, + { + "fieldname": "more_info_tab", + "fieldtype": "Tab Break", + "label": "More Info" + }, + { + "fieldname": "connections_tab", + "fieldtype": "Tab Break", + "label": "Connections", + "show_dashboard": 1 + }, + { + "fieldname": "column_break_12", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_31", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_43", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_53", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_98", + "fieldtype": "Section Break", + "label": "Company Shipping Address" + }, + { + "fieldname": "billing_address_section", + "fieldtype": "Section Break", + "label": "Company Billing Address" + }, + { + "collapsible": 1, + "fieldname": "status_section", + "fieldtype": "Section Break", + "label": "Status", + "oldfieldtype": "Section Break", + "options": "fa fa-file-text" + }, + { + "fieldname": "additional_info_section", + "fieldtype": "Section Break", + "label": "Additional Info" + }, + { + "fieldname": "column_break_131", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_100", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_104", + "fieldtype": "Column Break" + }, + { + "fieldname": "incoterm", + "fieldtype": "Link", + "label": "Incoterm", + "options": "Incoterm" + }, + { + "depends_on": "incoterm", + "fieldname": "named_place", + "fieldtype": "Data", + "label": "Named Place" } ], "icon": "fa fa-truck", "idx": 261, "is_submittable": 1, "links": [], - "modified": "2022-06-15 15:43:40.664382", + "modified": "2022-12-12 18:40:32.447752", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt", diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 84da3cc41d..c8a4bd3d27 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -6,7 +6,9 @@ import frappe from frappe import _, throw from frappe.desk.notifications import clear_doctype_notifications from frappe.model.mapper import get_mapped_doc +from frappe.query_builder.functions import CombineDatetime from frappe.utils import cint, flt, getdate, nowdate +from pypika import functions as fn import erpnext from erpnext.accounts.utils import get_account_currency @@ -171,7 +173,9 @@ class PurchaseReceipt(BuyingController): ) if ( - cint(frappe.db.get_single_value("Buying Settings", "maintain_same_rate")) and not self.is_return + cint(frappe.db.get_single_value("Buying Settings", "maintain_same_rate")) + and not self.is_return + and not self.is_internal_supplier ): self.validate_rate_with_reference_doc( [["Purchase Order", "purchase_order", "purchase_order_item"]] @@ -362,6 +366,12 @@ class PurchaseReceipt(BuyingController): if credit_currency == self.company_currency else flt(d.net_amount, d.precision("net_amount")) ) + + outgoing_amount = d.base_net_amount + if self.is_internal_supplier and d.valuation_rate: + outgoing_amount = d.valuation_rate * d.stock_qty + credit_amount = outgoing_amount + if credit_amount: account = warehouse_account[d.from_warehouse]["account"] if d.from_warehouse else stock_rbnb @@ -369,7 +379,7 @@ class PurchaseReceipt(BuyingController): gl_entries=gl_entries, account=account, cost_center=d.cost_center, - debit=-1 * flt(d.base_net_amount, d.precision("base_net_amount")), + debit=-1 * flt(outgoing_amount, d.precision("base_net_amount")), credit=0.0, remarks=remarks, against_account=warehouse_account_name, @@ -456,14 +466,14 @@ class PurchaseReceipt(BuyingController): # divisional loss adjustment valuation_amount_as_per_doc = ( - flt(d.base_net_amount, d.precision("base_net_amount")) + flt(outgoing_amount, d.precision("base_net_amount")) + flt(d.landed_cost_voucher_amount) + flt(d.rm_supp_cost) + flt(d.item_tax_amount) ) divisional_loss = flt( - valuation_amount_as_per_doc - stock_value_diff, d.precision("base_net_amount") + valuation_amount_as_per_doc - flt(stock_value_diff), d.precision("base_net_amount") ) if divisional_loss: @@ -631,47 +641,6 @@ class PurchaseReceipt(BuyingController): i += 1 - def add_gl_entry( - self, - gl_entries, - account, - cost_center, - debit, - credit, - remarks, - against_account, - debit_in_account_currency=None, - credit_in_account_currency=None, - account_currency=None, - project=None, - voucher_detail_no=None, - item=None, - posting_date=None, - ): - - gl_entry = { - "account": account, - "cost_center": cost_center, - "debit": debit, - "credit": credit, - "against": against_account, - "remarks": remarks, - } - - if voucher_detail_no: - gl_entry.update({"voucher_detail_no": voucher_detail_no}) - - if debit_in_account_currency: - gl_entry.update({"debit_in_account_currency": debit_in_account_currency}) - - if credit_in_account_currency: - gl_entry.update({"credit_in_account_currency": credit_in_account_currency}) - - if posting_date: - gl_entry.update({"posting_date": posting_date}) - - gl_entries.append(self.get_gl_dict(gl_entry, item=item)) - def get_asset_gl_entry(self, gl_entries): for item in self.get("items"): if item.is_fixed_asset: @@ -785,48 +754,38 @@ class PurchaseReceipt(BuyingController): def update_billing_status(self, update_modified=True): updated_pr = [self.name] + po_details = [] for d in self.get("items"): if d.get("purchase_invoice") and d.get("purchase_invoice_item"): d.db_set("billed_amt", d.amount, update_modified=update_modified) elif d.purchase_order_item: - updated_pr += update_billed_amount_based_on_po(d.purchase_order_item, update_modified) + po_details.append(d.purchase_order_item) + + if po_details: + updated_pr += update_billed_amount_based_on_po(po_details, update_modified) for pr in set(updated_pr): - pr_doc = self if (pr == self.name) else frappe.get_doc("Purchase Receipt", pr) + pr_doc = self if (pr == self.name) else frappe.get_cached_doc("Purchase Receipt", pr) update_billing_percentage(pr_doc, update_modified=update_modified) self.load_from_db() -def update_billed_amount_based_on_po(po_detail, update_modified=True): - # Billed against Sales Order directly - billed_against_po = frappe.db.sql( - """select sum(amount) from `tabPurchase Invoice Item` - where po_detail=%s and (pr_detail is null or pr_detail = '') and docstatus=1""", - po_detail, - ) - billed_against_po = billed_against_po and billed_against_po[0][0] or 0 +def update_billed_amount_based_on_po(po_details, update_modified=True): + po_billed_amt_details = get_billed_amount_against_po(po_details) - # Get all Purchase Receipt Item rows against the Purchase Order Item row - pr_details = frappe.db.sql( - """select pr_item.name, pr_item.amount, pr_item.parent - from `tabPurchase Receipt Item` pr_item, `tabPurchase Receipt` pr - where pr.name=pr_item.parent and pr_item.purchase_order_item=%s - and pr.docstatus=1 and pr.is_return = 0 - order by pr.posting_date asc, pr.posting_time asc, pr.name asc""", - po_detail, - as_dict=1, - ) + # Get all Purchase Receipt Item rows against the Purchase Order Items + pr_details = get_purchase_receipts_against_po_details(po_details) + + pr_items = [pr_detail.name for pr_detail in pr_details] + pr_items_billed_amount = get_billed_amount_against_pr(pr_items) updated_pr = [] for pr_item in pr_details: + billed_against_po = flt(po_billed_amt_details.get(pr_item.purchase_order_item)) + # Get billed amount directly against Purchase Receipt - billed_amt_agianst_pr = frappe.db.sql( - """select sum(amount) from `tabPurchase Invoice Item` - where pr_detail=%s and docstatus=1""", - pr_item.name, - ) - billed_amt_agianst_pr = billed_amt_agianst_pr and billed_amt_agianst_pr[0][0] or 0 + billed_amt_agianst_pr = flt(pr_items_billed_amount.get(pr_item.name, 0)) # Distribute billed amount directly against PO between PRs based on FIFO if billed_against_po and billed_amt_agianst_pr < pr_item.amount: @@ -838,37 +797,100 @@ def update_billed_amount_based_on_po(po_detail, update_modified=True): billed_amt_agianst_pr += billed_against_po billed_against_po = 0 - frappe.db.set_value( - "Purchase Receipt Item", - pr_item.name, - "billed_amt", - billed_amt_agianst_pr, - update_modified=update_modified, - ) + po_billed_amt_details[pr_item.purchase_order_item] = billed_against_po - updated_pr.append(pr_item.parent) + if pr_item.billed_amt != billed_amt_agianst_pr: + frappe.db.set_value( + "Purchase Receipt Item", + pr_item.name, + "billed_amt", + billed_amt_agianst_pr, + update_modified=update_modified, + ) + + updated_pr.append(pr_item.parent) return updated_pr +def get_purchase_receipts_against_po_details(po_details): + # Get Purchase Receipts against Purchase Order Items + + purchase_receipt = frappe.qb.DocType("Purchase Receipt") + purchase_receipt_item = frappe.qb.DocType("Purchase Receipt Item") + + query = ( + frappe.qb.from_(purchase_receipt) + .inner_join(purchase_receipt_item) + .on(purchase_receipt.name == purchase_receipt_item.parent) + .select( + purchase_receipt_item.name, + purchase_receipt_item.parent, + purchase_receipt_item.amount, + purchase_receipt_item.billed_amt, + purchase_receipt_item.purchase_order_item, + ) + .where( + (purchase_receipt_item.purchase_order_item.isin(po_details)) + & (purchase_receipt.docstatus == 1) + & (purchase_receipt.is_return == 0) + ) + .orderby(CombineDatetime(purchase_receipt.posting_date, purchase_receipt.posting_time)) + .orderby(purchase_receipt.name) + ) + + return query.run(as_dict=True) + + +def get_billed_amount_against_pr(pr_items): + # Get billed amount directly against Purchase Receipt + + if not pr_items: + return {} + + purchase_invoice_item = frappe.qb.DocType("Purchase Invoice Item") + + query = ( + frappe.qb.from_(purchase_invoice_item) + .select(fn.Sum(purchase_invoice_item.amount).as_("billed_amt"), purchase_invoice_item.pr_detail) + .where((purchase_invoice_item.pr_detail.isin(pr_items)) & (purchase_invoice_item.docstatus == 1)) + .groupby(purchase_invoice_item.pr_detail) + ).run(as_dict=1) + + return {d.pr_detail: flt(d.billed_amt) for d in query} + + +def get_billed_amount_against_po(po_items): + # Get billed amount directly against Purchase Order + if not po_items: + return {} + + purchase_invoice_item = frappe.qb.DocType("Purchase Invoice Item") + + query = ( + frappe.qb.from_(purchase_invoice_item) + .select(fn.Sum(purchase_invoice_item.amount).as_("billed_amt"), purchase_invoice_item.po_detail) + .where( + (purchase_invoice_item.po_detail.isin(po_items)) + & (purchase_invoice_item.docstatus == 1) + & (purchase_invoice_item.pr_detail.isnull()) + ) + .groupby(purchase_invoice_item.po_detail) + ).run(as_dict=1) + + return {d.po_detail: flt(d.billed_amt) for d in query} + + def update_billing_percentage(pr_doc, update_modified=True): # Reload as billed amount was set in db directly pr_doc.load_from_db() # Update Billing % based on pending accepted qty total_amount, total_billed_amount = 0, 0 - for item in pr_doc.items: - return_data = frappe.db.get_list( - "Purchase Receipt", - fields=["sum(abs(`tabPurchase Receipt Item`.qty)) as qty"], - filters=[ - ["Purchase Receipt", "docstatus", "=", 1], - ["Purchase Receipt", "is_return", "=", 1], - ["Purchase Receipt Item", "purchase_receipt_item", "=", item.name], - ], - ) + item_wise_returned_qty = get_item_wise_returned_qty(pr_doc) - returned_qty = return_data[0].qty if return_data else 0 + for item in pr_doc.items: + returned_qty = flt(item_wise_returned_qty.get(item.name)) returned_amount = flt(returned_qty) * flt(item.rate) pending_amount = flt(item.amount) - returned_amount total_billable_amount = pending_amount if item.billed_amt <= pending_amount else item.billed_amt @@ -885,6 +907,27 @@ def update_billing_percentage(pr_doc, update_modified=True): pr_doc.notify_update() +def get_item_wise_returned_qty(pr_doc): + items = [d.name for d in pr_doc.items] + + return frappe._dict( + frappe.get_all( + "Purchase Receipt", + fields=[ + "`tabPurchase Receipt Item`.purchase_receipt_item", + "sum(abs(`tabPurchase Receipt Item`.qty)) as qty", + ], + filters=[ + ["Purchase Receipt", "docstatus", "=", 1], + ["Purchase Receipt", "is_return", "=", 1], + ["Purchase Receipt Item", "purchase_receipt_item", "in", items], + ], + group_by="`tabPurchase Receipt Item`.purchase_receipt_item", + as_list=1, + ) + ) + + @frappe.whitelist() def make_purchase_invoice(source_name, target_doc=None): from erpnext.accounts.party import get_payment_terms_template @@ -1091,13 +1134,25 @@ def get_item_account_wise_additional_cost(purchase_document): account.expense_account, {"amount": 0.0, "base_amount": 0.0} ) - item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][account.expense_account][ - "amount" - ] += (account.amount * item.get(based_on_field) / total_item_cost) + if total_item_cost > 0: + item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][ + account.expense_account + ]["amount"] += ( + account.amount * item.get(based_on_field) / total_item_cost + ) - item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][account.expense_account][ - "base_amount" - ] += (account.base_amount * item.get(based_on_field) / total_item_cost) + item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][ + account.expense_account + ]["base_amount"] += ( + account.base_amount * item.get(based_on_field) / total_item_cost + ) + else: + item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][ + account.expense_account + ]["amount"] += item.applicable_charges + item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][ + account.expense_account + ]["base_amount"] += item.applicable_charges return item_account_wise_cost diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt_dashboard.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt_dashboard.py index 06ba936556..b3ae7b58b4 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt_dashboard.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt_dashboard.py @@ -12,13 +12,17 @@ def get_data(): "Purchase Receipt": "return_against", }, "internal_links": { + "Material Request": ["items", "material_request"], "Purchase Order": ["items", "purchase_order"], "Project": ["items", "project"], "Quality Inspection": ["items", "quality_inspection"], }, "transactions": [ {"label": _("Related"), "items": ["Purchase Invoice", "Landed Cost Voucher", "Asset"]}, - {"label": _("Reference"), "items": ["Purchase Order", "Quality Inspection", "Project"]}, + { + "label": _("Reference"), + "items": ["Material Request", "Purchase Order", "Quality Inspection", "Project"], + }, {"label": _("Returns"), "items": ["Purchase Receipt"]}, {"label": _("Subscription"), "items": ["Auto Repeat"]}, ], diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index d0d115d96a..b6341466f8 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -1,10 +1,10 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt - import frappe from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import add_days, cint, cstr, flt, today +from pypika import functions as fn import erpnext from erpnext.accounts.doctype.account.test_account import get_inventory_account @@ -1156,6 +1156,429 @@ class TestPurchaseReceipt(FrappeTestCase): if gle.account == account: self.assertEqual(gle.credit, 50) + def test_backdated_transaction_for_internal_transfer(self): + from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt + from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note + + prepare_data_for_internal_transfer() + customer = "_Test Internal Customer 2" + company = "_Test Company with perpetual inventory" + + from_warehouse = create_warehouse("_Test Internal From Warehouse New", company=company) + to_warehouse = create_warehouse("_Test Internal To Warehouse New", company=company) + item_doc = create_item("Test Internal Transfer Item") + + target_warehouse = create_warehouse("_Test Internal GIT Warehouse New", company=company) + + make_purchase_receipt( + item_code=item_doc.name, + company=company, + posting_date=add_days(today(), -1), + warehouse=from_warehouse, + qty=1, + rate=100, + ) + + dn1 = create_delivery_note( + item_code=item_doc.name, + company=company, + customer=customer, + cost_center="Main - TCP1", + expense_account="Cost of Goods Sold - TCP1", + qty=1, + rate=500, + warehouse=from_warehouse, + target_warehouse=target_warehouse, + ) + + self.assertEqual(dn1.items[0].rate, 100) + + pr1 = make_inter_company_purchase_receipt(dn1.name) + pr1.items[0].warehouse = to_warehouse + self.assertEqual(pr1.items[0].rate, 100) + pr1.submit() + + self.assertEqual(pr1.is_internal_supplier, 1) + + # Backdated purchase receipt entry, the valuation rate should be updated for DN1 and PR1 + make_purchase_receipt( + item_code=item_doc.name, + company=company, + posting_date=add_days(today(), -2), + warehouse=from_warehouse, + qty=1, + rate=200, + ) + + dn_value = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Delivery Note", "voucher_no": dn1.name, "warehouse": target_warehouse}, + "stock_value_difference", + ) + + self.assertEqual(abs(dn_value), 200.00) + + pr_value = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Purchase Receipt", "voucher_no": pr1.name, "warehouse": to_warehouse}, + "stock_value_difference", + ) + + self.assertEqual(abs(pr_value), 200.00) + pr1.load_from_db() + + self.assertEqual(pr1.items[0].valuation_rate, 200) + self.assertEqual(pr1.items[0].rate, 100) + + Gl = frappe.qb.DocType("GL Entry") + + query = ( + frappe.qb.from_(Gl) + .select( + (fn.Sum(Gl.debit) - fn.Sum(Gl.credit)).as_("value"), + ) + .where((Gl.voucher_type == pr1.doctype) & (Gl.voucher_no == pr1.name)) + ).run(as_dict=True) + + self.assertEqual(query[0].value, 0) + + def test_backdated_transaction_for_internal_transfer_in_trasit_warehouse_for_purchase_receipt( + self, + ): + from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt + from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note + + prepare_data_for_internal_transfer() + customer = "_Test Internal Customer 2" + company = "_Test Company with perpetual inventory" + + from_warehouse = create_warehouse("_Test Internal From Warehouse New", company=company) + to_warehouse = create_warehouse("_Test Internal To Warehouse New", company=company) + item_doc = create_item("Test Internal Transfer Item") + + target_warehouse = create_warehouse("_Test Internal GIT Warehouse New", company=company) + + make_purchase_receipt( + item_code=item_doc.name, + company=company, + posting_date=add_days(today(), -1), + warehouse=from_warehouse, + qty=1, + rate=100, + ) + + # Keep stock in advance and make sure that systen won't pick this stock while reposting backdated transaction + for i in range(1, 4): + make_purchase_receipt( + item_code=item_doc.name, + company=company, + posting_date=add_days(today(), -1 * i), + warehouse=target_warehouse, + qty=1, + rate=320 * i, + ) + + dn1 = create_delivery_note( + item_code=item_doc.name, + company=company, + customer=customer, + cost_center="Main - TCP1", + expense_account="Cost of Goods Sold - TCP1", + qty=1, + rate=500, + warehouse=from_warehouse, + target_warehouse=target_warehouse, + ) + + self.assertEqual(dn1.items[0].rate, 100) + + pr1 = make_inter_company_purchase_receipt(dn1.name) + pr1.items[0].warehouse = to_warehouse + self.assertEqual(pr1.items[0].rate, 100) + pr1.submit() + + stk_ledger = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Purchase Receipt", "voucher_no": pr1.name, "warehouse": target_warehouse}, + ["stock_value_difference", "outgoing_rate"], + as_dict=True, + ) + + self.assertEqual(abs(stk_ledger.stock_value_difference), 100) + self.assertEqual(stk_ledger.outgoing_rate, 100) + + # Backdated purchase receipt entry, the valuation rate should be updated for DN1 and PR1 + make_purchase_receipt( + item_code=item_doc.name, + company=company, + posting_date=add_days(today(), -2), + warehouse=from_warehouse, + qty=1, + rate=200, + ) + + dn_value = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Delivery Note", "voucher_no": dn1.name, "warehouse": target_warehouse}, + "stock_value_difference", + ) + + self.assertEqual(abs(dn_value), 200.00) + + pr_value = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Purchase Receipt", "voucher_no": pr1.name, "warehouse": to_warehouse}, + "stock_value_difference", + ) + + self.assertEqual(abs(pr_value), 200.00) + pr1.load_from_db() + + self.assertEqual(pr1.items[0].valuation_rate, 200) + self.assertEqual(pr1.items[0].rate, 100) + + Gl = frappe.qb.DocType("GL Entry") + + query = ( + frappe.qb.from_(Gl) + .select( + (fn.Sum(Gl.debit) - fn.Sum(Gl.credit)).as_("value"), + ) + .where((Gl.voucher_type == pr1.doctype) & (Gl.voucher_no == pr1.name)) + ).run(as_dict=True) + + self.assertEqual(query[0].value, 0) + + def test_backdated_transaction_for_internal_transfer_in_trasit_warehouse_for_purchase_invoice( + self, + ): + from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import ( + make_purchase_invoice as make_purchase_invoice_for_si, + ) + from erpnext.accounts.doctype.sales_invoice.sales_invoice import ( + make_inter_company_purchase_invoice, + ) + from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice + + prepare_data_for_internal_transfer() + customer = "_Test Internal Customer 2" + company = "_Test Company with perpetual inventory" + + from_warehouse = create_warehouse("_Test Internal From Warehouse New", company=company) + to_warehouse = create_warehouse("_Test Internal To Warehouse New", company=company) + item_doc = create_item("Test Internal Transfer Item") + + target_warehouse = create_warehouse("_Test Internal GIT Warehouse New", company=company) + + make_purchase_invoice_for_si( + item_code=item_doc.name, + company=company, + posting_date=add_days(today(), -1), + warehouse=from_warehouse, + qty=1, + update_stock=1, + expense_account="Cost of Goods Sold - TCP1", + cost_center="Main - TCP1", + rate=100, + ) + + # Keep stock in advance and make sure that systen won't pick this stock while reposting backdated transaction + for i in range(1, 4): + make_purchase_invoice_for_si( + item_code=item_doc.name, + company=company, + posting_date=add_days(today(), -1 * i), + warehouse=target_warehouse, + update_stock=1, + qty=1, + expense_account="Cost of Goods Sold - TCP1", + cost_center="Main - TCP1", + rate=320 * i, + ) + + si1 = create_sales_invoice( + item_code=item_doc.name, + company=company, + customer=customer, + cost_center="Main - TCP1", + income_account="Sales - TCP1", + qty=1, + rate=500, + update_stock=1, + warehouse=from_warehouse, + target_warehouse=target_warehouse, + ) + + self.assertEqual(si1.items[0].rate, 100) + + pi1 = make_inter_company_purchase_invoice(si1.name) + pi1.items[0].warehouse = to_warehouse + self.assertEqual(pi1.items[0].rate, 100) + pi1.update_stock = 1 + pi1.save() + pi1.submit() + + stk_ledger = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": pi1.doctype, "voucher_no": pi1.name, "warehouse": target_warehouse}, + ["stock_value_difference", "outgoing_rate"], + as_dict=True, + ) + + self.assertEqual(abs(stk_ledger.stock_value_difference), 100) + self.assertEqual(stk_ledger.outgoing_rate, 100) + + # Backdated purchase receipt entry, the valuation rate should be updated for si1 and pi1 + make_purchase_receipt( + item_code=item_doc.name, + company=company, + posting_date=add_days(today(), -2), + warehouse=from_warehouse, + qty=1, + rate=200, + ) + + si_value = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": si1.doctype, "voucher_no": si1.name, "warehouse": target_warehouse}, + "stock_value_difference", + ) + + self.assertEqual(abs(si_value), 200.00) + + pi_value = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": pi1.doctype, "voucher_no": pi1.name, "warehouse": to_warehouse}, + "stock_value_difference", + ) + + self.assertEqual(abs(pi_value), 200.00) + pi1.load_from_db() + + self.assertEqual(pi1.items[0].valuation_rate, 200) + self.assertEqual(pi1.items[0].rate, 100) + + Gl = frappe.qb.DocType("GL Entry") + + query = ( + frappe.qb.from_(Gl) + .select( + (fn.Sum(Gl.debit) - fn.Sum(Gl.credit)).as_("value"), + ) + .where((Gl.voucher_type == pi1.doctype) & (Gl.voucher_no == pi1.name)) + ).run(as_dict=True) + + self.assertEqual(query[0].value, 0) + + def test_batch_expiry_for_purchase_receipt(self): + from erpnext.controllers.sales_and_purchase_return import make_return_doc + + item = make_item( + "_Test Batch Item For Return Check", + { + "is_purchase_item": 1, + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "TBIRC.#####", + }, + ) + + pi = make_purchase_receipt( + qty=1, + item_code=item.name, + update_stock=True, + ) + + pi.load_from_db() + batch_no = pi.items[0].batch_no + self.assertTrue(batch_no) + + frappe.db.set_value("Batch", batch_no, "expiry_date", add_days(today(), -1)) + + return_pi = make_return_doc(pi.doctype, pi.name) + return_pi.save().submit() + + self.assertTrue(return_pi.docstatus == 1) + + def test_disable_last_purchase_rate(self): + from erpnext.stock.get_item_details import get_item_details + + item = make_item( + "_Test Disable Last Purchase Rate", + {"is_purchase_item": 1, "is_stock_item": 1}, + ) + + frappe.db.set_single_value("Buying Settings", "disable_last_purchase_rate", 1) + + pr = make_purchase_receipt( + qty=1, + rate=100, + item_code=item.name, + ) + + args = pr.items[0].as_dict() + args.update( + { + "supplier": pr.supplier, + "doctype": pr.doctype, + "conversion_rate": pr.conversion_rate, + "currency": pr.currency, + "company": pr.company, + "posting_date": pr.posting_date, + "posting_time": pr.posting_time, + } + ) + + res = get_item_details(args) + self.assertEqual(res.get("last_purchase_rate"), 0) + + frappe.db.set_single_value("Buying Settings", "disable_last_purchase_rate", 0) + + pr = make_purchase_receipt( + qty=1, + rate=100, + item_code=item.name, + ) + + res = get_item_details(args) + self.assertEqual(res.get("last_purchase_rate"), 100) + + +def prepare_data_for_internal_transfer(): + from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier + from erpnext.selling.doctype.customer.test_customer import create_internal_customer + + company = "_Test Company with perpetual inventory" + + create_internal_customer( + "_Test Internal Customer 2", + company, + company, + ) + + create_internal_supplier( + "_Test Internal Supplier 2", + company, + company, + ) + + if not frappe.db.get_value("Company", company, "unrealized_profit_loss_account"): + account = "Unrealized Profit and Loss - TCP1" + if not frappe.db.exists("Account", account): + frappe.get_doc( + { + "doctype": "Account", + "account_name": "Unrealized Profit and Loss", + "parent_account": "Direct Income - TCP1", + "company": company, + "is_group": 0, + "account_type": "Income Account", + } + ).insert() + + frappe.db.set_value("Company", company, "unrealized_profit_loss_account", account) + def get_sl_entries(voucher_type, voucher_no): return frappe.db.sql( 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 c97dbee911..7a350b9e44 100644 --- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json +++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -8,6 +8,7 @@ "engine": "InnoDB", "field_order": [ "barcode", + "has_item_scanned", "section_break_2", "item_code", "product_bundle", @@ -744,6 +745,7 @@ "oldfieldname": "valuation_rate", "oldfieldtype": "Currency", "options": "Company:company:default_currency", + "precision": "6", "print_hide": 1, "print_width": "80px", "read_only": 1, @@ -792,10 +794,8 @@ { "fieldname": "expense_account", "fieldtype": "Link", - "hidden": 1, "label": "Expense Account", - "options": "Account", - "read_only": 1 + "options": "Account" }, { "fieldname": "accounting_dimensions_section", @@ -859,7 +859,8 @@ "label": "Purchase Receipt Item", "no_copy": 1, "print_hide": 1, - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "collapsible": 1, @@ -920,6 +921,7 @@ }, { "collapsible": 1, + "collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount", "fieldname": "discount_and_margin_section", "fieldtype": "Section Break", "label": "Discount and Margin" @@ -973,7 +975,8 @@ "label": "Purchase Invoice Item", "no_copy": 1, "print_hide": 1, - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "fieldname": "product_bundle", @@ -996,12 +999,20 @@ { "fieldname": "column_break_102", "fieldtype": "Column Break" + }, + { + "default": "0", + "depends_on": "barcode", + "fieldname": "has_item_scanned", + "fieldtype": "Check", + "label": "Has Item Scanned", + "read_only": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2022-06-17 05:32:16.483178", + "modified": "2023-01-18 15:48:58.114923", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt Item", diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.json b/erpnext/stock/doctype/quality_inspection/quality_inspection.json index edfe7e98b2..db9322f326 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.json +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.json @@ -10,6 +10,7 @@ "naming_series", "report_date", "status", + "manual_inspection", "column_break_4", "inspection_type", "reference_type", @@ -231,6 +232,12 @@ "label": "Status", "options": "\nAccepted\nRejected", "reqd": 1 + }, + { + "default": "0", + "fieldname": "manual_inspection", + "fieldtype": "Check", + "label": "Manual Inspection" } ], "icon": "fa fa-search", @@ -238,10 +245,11 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-12-18 19:59:55.710300", + "modified": "2022-10-04 22:00:13.995221", "modified_by": "Administrator", "module": "Stock", "name": "Quality Inspection", + "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ { @@ -262,5 +270,6 @@ "search_fields": "item_code, report_date, reference_name", "show_name_in_global_search": 1, "sort_field": "modified", - "sort_order": "ASC" + "sort_order": "ASC", + "states": [] } \ No newline at end of file diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.py b/erpnext/stock/doctype/quality_inspection/quality_inspection.py index 13abfad455..2a9f091bd0 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.py @@ -6,7 +6,7 @@ import frappe from frappe import _ from frappe.model.document import Document from frappe.model.mapper import get_mapped_doc -from frappe.utils import cint, flt +from frappe.utils import cint, cstr, flt from erpnext.stock.doctype.quality_inspection_template.quality_inspection_template import ( get_template_details, @@ -30,6 +30,9 @@ class QualityInspection(Document): if self.readings: self.inspect_and_set_status() + def before_submit(self): + self.validate_readings_status_mandatory() + @frappe.whitelist() def get_item_specification_details(self): if not self.quality_inspection_template: @@ -65,6 +68,11 @@ class QualityInspection(Document): def on_cancel(self): self.update_qc_reference() + def validate_readings_status_mandatory(self): + for reading in self.readings: + if not reading.status: + frappe.throw(_("Row #{0}: Status is mandatory").format(reading.idx)) + def update_qc_reference(self): quality_inspection = self.name if self.docstatus == 1 else "" @@ -124,6 +132,16 @@ class QualityInspection(Document): # if not formula based check acceptance values set self.set_status_based_on_acceptance_values(reading) + if not self.manual_inspection: + self.status = "Accepted" + for reading in self.readings: + if reading.status == "Rejected": + self.status = "Rejected" + frappe.msgprint( + _("Status set to rejected as there are one or more rejected readings."), alert=True + ) + break + def set_status_based_on_acceptance_values(self, reading): if not cint(reading.numeric): result = reading.get("reading_value") == reading.get("value") @@ -201,68 +219,71 @@ class QualityInspection(Document): @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def item_query(doctype, txt, searchfield, start, page_len, filters): - if filters.get("from"): - from frappe.desk.reportview import get_match_cond + from frappe.desk.reportview import get_match_cond - mcond = get_match_cond(filters["from"]) - cond, qi_condition = "", "and (quality_inspection is null or quality_inspection = '')" + from_doctype = cstr(filters.get("from")) + if not from_doctype or not frappe.db.exists("DocType", from_doctype): + return [] - if filters.get("parent"): - if ( - filters.get("from") in ["Purchase Invoice Item", "Purchase Receipt Item"] - and filters.get("inspection_type") != "In Process" - ): - cond = """and item_code in (select name from `tabItem` where - inspection_required_before_purchase = 1)""" - elif ( - filters.get("from") in ["Sales Invoice Item", "Delivery Note Item"] - and filters.get("inspection_type") != "In Process" - ): - cond = """and item_code in (select name from `tabItem` where - inspection_required_before_delivery = 1)""" - elif filters.get("from") == "Stock Entry Detail": - cond = """and s_warehouse is null""" + mcond = get_match_cond(from_doctype) + cond, qi_condition = "", "and (quality_inspection is null or quality_inspection = '')" - if filters.get("from") in ["Supplier Quotation Item"]: - qi_condition = "" + if filters.get("parent"): + if ( + from_doctype in ["Purchase Invoice Item", "Purchase Receipt Item"] + and filters.get("inspection_type") != "In Process" + ): + cond = """and item_code in (select name from `tabItem` where + inspection_required_before_purchase = 1)""" + elif ( + from_doctype in ["Sales Invoice Item", "Delivery Note Item"] + and filters.get("inspection_type") != "In Process" + ): + cond = """and item_code in (select name from `tabItem` where + inspection_required_before_delivery = 1)""" + elif from_doctype == "Stock Entry Detail": + cond = """and s_warehouse is null""" - return frappe.db.sql( - """ - SELECT item_code - FROM `tab{doc}` - WHERE parent=%(parent)s and docstatus < 2 and item_code like %(txt)s - {qi_condition} {cond} {mcond} - ORDER BY item_code limit {page_len} offset {start} - """.format( - doc=filters.get("from"), - cond=cond, - mcond=mcond, - start=start, - page_len=page_len, - qi_condition=qi_condition, - ), - {"parent": filters.get("parent"), "txt": "%%%s%%" % txt}, - ) + if from_doctype in ["Supplier Quotation Item"]: + qi_condition = "" - elif filters.get("reference_name"): - return frappe.db.sql( - """ - SELECT production_item - FROM `tab{doc}` - WHERE name = %(reference_name)s and docstatus < 2 and production_item like %(txt)s - {qi_condition} {cond} {mcond} - ORDER BY production_item - limit {page_len} offset {start} - """.format( - doc=filters.get("from"), - cond=cond, - mcond=mcond, - start=start, - page_len=page_len, - qi_condition=qi_condition, - ), - {"reference_name": filters.get("reference_name"), "txt": "%%%s%%" % txt}, - ) + return frappe.db.sql( + """ + SELECT item_code + FROM `tab{doc}` + WHERE parent=%(parent)s and docstatus < 2 and item_code like %(txt)s + {qi_condition} {cond} {mcond} + ORDER BY item_code limit {page_len} offset {start} + """.format( + doc=from_doctype, + cond=cond, + mcond=mcond, + start=cint(start), + page_len=cint(page_len), + qi_condition=qi_condition, + ), + {"parent": filters.get("parent"), "txt": "%%%s%%" % txt}, + ) + + elif filters.get("reference_name"): + return frappe.db.sql( + """ + SELECT production_item + FROM `tab{doc}` + WHERE name = %(reference_name)s and docstatus < 2 and production_item like %(txt)s + {qi_condition} {cond} {mcond} + ORDER BY production_item + limit {page_len} offset {start} + """.format( + doc=from_doctype, + cond=cond, + mcond=mcond, + start=cint(start), + page_len=cint(page_len), + qi_condition=qi_condition, + ), + {"reference_name": filters.get("reference_name"), "txt": "%%%s%%" % txt}, + ) @frappe.whitelist() diff --git a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py index 144f13880b..4f19643ad5 100644 --- a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py @@ -160,7 +160,7 @@ class TestQualityInspection(FrappeTestCase): ) readings = [ - {"specification": "Iron Content", "min_value": 0.1, "max_value": 0.9, "reading_1": "0.4"} + {"specification": "Iron Content", "min_value": 0.1, "max_value": 0.9, "reading_1": "1.0"} ] qa = create_quality_inspection( @@ -184,6 +184,38 @@ class TestQualityInspection(FrappeTestCase): se.cancel() frappe.db.set_value("Stock Settings", None, "action_if_quality_inspection_is_rejected", "Stop") + def test_qi_status(self): + make_stock_entry( + item_code="_Test Item with QA", target="_Test Warehouse - _TC", qty=1, basic_rate=100 + ) + dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True) + qa = create_quality_inspection( + reference_type="Delivery Note", reference_name=dn.name, status="Accepted", do_not_save=True + ) + qa.readings[0].manual_inspection = 1 + qa.save() + + # Case - 1: When there are one or more readings with rejected status and parent manual inspection is unchecked, then parent status should be set to rejected. + qa.status = "Accepted" + qa.manual_inspection = 0 + qa.readings[0].status = "Rejected" + qa.save() + self.assertEqual(qa.status, "Rejected") + + # Case - 2: When all readings have accepted status and parent manual inspection is unchecked, then parent status should be set to accepted. + qa.status = "Rejected" + qa.manual_inspection = 0 + qa.readings[0].status = "Accepted" + qa.save() + self.assertEqual(qa.status, "Accepted") + + # Case - 3: When parent manual inspection is checked, then parent status should not be changed. + qa.status = "Accepted" + qa.manual_inspection = 1 + qa.readings[0].status = "Rejected" + qa.save() + self.assertEqual(qa.status, "Accepted") + def create_quality_inspection(**args): args = frappe._dict(args) diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js index 4cd40bf38e..8aec532847 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js @@ -15,7 +15,7 @@ frappe.ui.form.on('Repost Item Valuation', { return { filters: { name: ['in', ['Purchase Receipt', 'Purchase Invoice', 'Delivery Note', - 'Sales Invoice', 'Stock Entry', 'Stock Reconciliation']] + 'Sales Invoice', 'Stock Entry', 'Stock Reconciliation', 'Subcontracting Receipt']] } }; }); @@ -24,7 +24,8 @@ frappe.ui.form.on('Repost Item Valuation', { frm.set_query("voucher_no", () => { return { filters: { - company: frm.doc.company + company: frm.doc.company, + docstatus: 1 } }; }); @@ -33,6 +34,22 @@ frappe.ui.form.on('Repost Item Valuation', { frm.trigger('setup_realtime_progress'); }, + based_on: function(frm) { + var fields_to_reset = []; + + if (frm.doc.based_on == 'Transaction') { + fields_to_reset = ['item_code', 'warehouse']; + } else if (frm.doc.based_on == 'Item and Warehouse') { + fields_to_reset = ['voucher_type', 'voucher_no']; + } + + if (fields_to_reset) { + fields_to_reset.forEach(field => { + frm.set_value(field, undefined); + }); + } + }, + setup_realtime_progress: function(frm) { frappe.realtime.on('item_reposting_progress', data => { if (frm.doc.name !== data.name) { @@ -57,6 +74,21 @@ frappe.ui.form.on('Repost Item Valuation', { } frm.trigger('show_reposting_progress'); + + if (frm.doc.status === 'Queued' && frm.doc.docstatus === 1) { + frm.trigger('execute_reposting'); + } + }, + + execute_reposting(frm) { + frm.add_custom_button(__("Start Reposting"), () => { + frappe.call({ + method: 'erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.execute_repost_item_valuation', + callback: function() { + frappe.msgprint(__('Reposting has been started in the background.')); + } + }); + }); }, show_reposting_progress: function(frm) { diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json index e093933829..8a5309c348 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json @@ -50,13 +50,15 @@ "fieldname": "posting_date", "fieldtype": "Date", "label": "Posting Date", + "read_only_depends_on": "eval: doc.based_on == \"Transaction\"", "reqd": 1 }, { "fetch_from": "voucher_no.posting_time", "fieldname": "posting_time", "fieldtype": "Time", - "label": "Posting Time" + "label": "Posting Time", + "read_only_depends_on": "eval: doc.based_on == \"Transaction\"" }, { "default": "Queued", @@ -195,7 +197,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2022-06-13 12:20:22.182322", + "modified": "2022-11-28 16:00:05.637440", "modified_by": "Administrator", "module": "Stock", "name": "Repost Item Valuation", diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py index 7c57ecd73e..bbed099da9 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -5,7 +5,9 @@ import frappe from frappe import _ from frappe.exceptions import QueryDeadlockError, QueryTimeoutError from frappe.model.document import Document -from frappe.utils import cint, get_link_to_form, get_weekday, now, nowtime +from frappe.query_builder import DocType, Interval +from frappe.query_builder.functions import Now +from frappe.utils import cint, get_link_to_form, get_weekday, getdate, now, nowtime from frappe.utils.user import get_users_with_role from rq.timeouts import JobTimeoutException @@ -21,10 +23,43 @@ RecoverableErrors = (JobTimeoutException, QueryDeadlockError, QueryTimeoutError) class RepostItemValuation(Document): + @staticmethod + def clear_old_logs(days=None): + days = days or 90 + table = DocType("Repost Item Valuation") + frappe.db.delete( + table, + filters=( + (table.modified < (Now() - Interval(days=days))) + & (table.status.isin(["Completed", "Skipped"])) + ), + ) + def validate(self): self.set_status(write=False) self.reset_field_values() self.set_company() + self.validate_accounts_freeze() + + def validate_accounts_freeze(self): + acc_settings = frappe.db.get_value( + "Accounts Settings", + "Accounts Settings", + ["acc_frozen_upto", "frozen_accounts_modifier"], + as_dict=1, + ) + if not acc_settings.acc_frozen_upto: + return + if getdate(self.posting_date) <= getdate(acc_settings.acc_frozen_upto): + if ( + acc_settings.frozen_accounts_modifier + and frappe.session.user in get_users_with_role(acc_settings.frozen_accounts_modifier) + ): + frappe.msgprint(_("Caution: This might alter frozen accounts.")) + return + frappe.throw( + _("You cannot repost item valuation before {}").format(acc_settings.acc_frozen_upto) + ) def reset_field_values(self): if self.based_on == "Transaction": @@ -128,6 +163,9 @@ def repost(doc): if not frappe.db.exists("Repost Item Valuation", doc.name): return + # This is to avoid TooManyWritesError in case of large reposts + frappe.db.MAX_WRITES_PER_TRANSACTION *= 4 + doc.set_status("In Progress") if not frappe.flags.in_test: frappe.db.commit() @@ -237,7 +275,7 @@ def _get_directly_dependent_vouchers(doc): def notify_error_to_stock_managers(doc, traceback): recipients = get_users_with_role("Stock Manager") if not recipients: - get_users_with_role("System Manager") + recipients = get_users_with_role("System Manager") subject = _("Error while reposting item valuation") message = ( @@ -307,3 +345,9 @@ def in_configured_timeslot(repost_settings=None, current_time=None): return end_time >= now_time >= start_time else: return now_time >= start_time or now_time <= end_time + + +@frappe.whitelist() +def execute_repost_item_valuation(): + """Execute repost item valuation via scheduler.""" + frappe.get_doc("Scheduled Job Type", "repost_item_valuation.repost_entries").enqueue(force=True) 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 edd2553d5d..96ac4352dc 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 @@ -6,9 +6,9 @@ from unittest.mock import MagicMock, call import frappe from frappe.tests.utils import FrappeTestCase -from frappe.utils import nowdate -from frappe.utils.data import add_to_date, today +from frappe.utils import add_days, add_to_date, now, nowdate, today +from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.utils import repost_gle_for_stock_vouchers from erpnext.controllers.stock_controller import create_item_wise_repost_entries from erpnext.stock.doctype.item.test_item import make_item @@ -85,6 +85,33 @@ class TestRepostItemValuation(FrappeTestCase, StockTestMixin): msg=f"Exepcted false from : {case}", ) + def test_clear_old_logs(self): + # create 10 logs + for i in range(1, 20): + repost_doc = frappe.get_doc( + doctype="Repost Item Valuation", + item_code="_Test Item", + warehouse="_Test Warehouse - _TC", + based_on="Item and Warehouse", + posting_date=nowdate(), + status="Skipped", + posting_time="00:01:00", + ).insert(ignore_permissions=True) + + repost_doc.load_from_db() + repost_doc.modified = add_days(now(), days=-i * 10) + repost_doc.db_update_all() + + logs = frappe.get_all("Repost Item Valuation", filters={"status": "Skipped"}) + self.assertTrue(len(logs) > 10) + + from erpnext.stock.doctype.repost_item_valuation.repost_item_valuation import RepostItemValuation + + RepostItemValuation.clear_old_logs(days=1) + + logs = frappe.get_all("Repost Item Valuation", filters={"status": "Skipped"}) + self.assertTrue(len(logs) == 0) + def test_create_item_wise_repost_item_valuation_entries(self): pr = make_purchase_receipt( company="_Test Company with perpetual inventory", @@ -272,3 +299,80 @@ class TestRepostItemValuation(FrappeTestCase, StockTestMixin): [{"credit": 50, "debit": 0}], gle_filters={"account": "Stock In Hand - TCP1"}, ) + + def test_duplicate_ple_on_repost(self): + from erpnext.accounts import utils + + # lower numbers to simplify test + orig_chunk_size = utils.GL_REPOSTING_CHUNK + utils.GL_REPOSTING_CHUNK = 2 + self.addCleanup(setattr, utils, "GL_REPOSTING_CHUNK", orig_chunk_size) + + rate = 100 + item = self.make_item() + item.valuation_rate = 90 + item.allow_negative_stock = 1 + item.save() + + company = "_Test Company with perpetual inventory" + + # consume non-existing stock + sinv = create_sales_invoice( + company=company, + posting_date=today(), + debit_to="Debtors - TCP1", + income_account="Sales - TCP1", + expense_account="Cost of Goods Sold - TCP1", + warehouse="Stores - TCP1", + update_stock=1, + currency="INR", + item_code=item.name, + cost_center="Main - TCP1", + qty=1, + rate=rate, + ) + + # backdated receipt triggers repost + make_stock_entry( + item=item.name, + company=company, + qty=5, + rate=rate, + target="Stores - TCP1", + posting_date=add_to_date(today(), days=-1), + ) + + ple_entries = frappe.db.get_list( + "Payment Ledger Entry", + filters={"voucher_type": sinv.doctype, "voucher_no": sinv.name, "delinked": 0}, + ) + + # assert successful deduplication on PLE + self.assertEqual(len(ple_entries), 1) + + # outstanding should not be affected + sinv.reload() + self.assertEqual(sinv.outstanding_amount, 100) + + def test_account_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 + + accounts_settings = frappe.get_doc("Accounts Settings") + accounts_settings.acc_frozen_upto = today + accounts_settings.frozen_accounts_modifier = "" + accounts_settings.save() + + self.assertRaises(frappe.ValidationError, riv.save) + + accounts_settings.acc_frozen_upto = "" + accounts_settings.save() diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index 6042ed4ac5..541d4d17e1 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -766,13 +766,13 @@ def get_delivery_note_serial_no(item_code, qty, delivery_note): @frappe.whitelist() def auto_fetch_serial_number( - qty: float, + qty: int, item_code: str, warehouse: str, posting_date: Optional[str] = None, batch_nos: Optional[Union[str, List[str]]] = None, for_doctype: Optional[str] = None, - exclude_sr_nos: Optional[List[str]] = None, + exclude_sr_nos=None, ) -> List[str]: filters = frappe._dict({"item_code": item_code, "warehouse": warehouse}) @@ -846,16 +846,15 @@ def get_pos_reserved_serial_nos(filters): pos_transacted_sr_nos = query.run(as_dict=True) - reserved_sr_nos = [] - returned_sr_nos = [] + reserved_sr_nos = set() + returned_sr_nos = set() for d in pos_transacted_sr_nos: if d.is_return == 0: - reserved_sr_nos += get_serial_nos(d.serial_no) + [reserved_sr_nos.add(x) for x in get_serial_nos(d.serial_no)] elif d.is_return == 1: - returned_sr_nos += get_serial_nos(d.serial_no) + [returned_sr_nos.add(x) for x in get_serial_nos(d.serial_no)] - for sr_no in returned_sr_nos: - reserved_sr_nos.remove(sr_no) + reserved_sr_nos = list(reserved_sr_nos - returned_sr_nos) return reserved_sr_nos diff --git a/erpnext/stock/doctype/shipment/shipment.json b/erpnext/stock/doctype/shipment/shipment.json index a33cbc288c..53b549deec 100644 --- a/erpnext/stock/doctype/shipment/shipment.json +++ b/erpnext/stock/doctype/shipment/shipment.json @@ -412,9 +412,9 @@ }, { "fieldname": "incoterm", - "fieldtype": "Select", + "fieldtype": "Link", "label": "Incoterm", - "options": "EXW (Ex Works)\nFCA (Free Carrier)\nCPT (Carriage Paid To)\nCIP (Carriage and Insurance Paid to)\nDPU (Delivered At Place Unloaded)\nDAP (Delivered At Place)\nDDP (Delivered Duty Paid)" + "options": "Incoterm" }, { "fieldname": "shipment_delivery_note", @@ -433,10 +433,11 @@ ], "is_submittable": 1, "links": [], - "modified": "2021-04-13 17:14:18.181818", + "modified": "2022-11-17 17:23:27.025802", "modified_by": "Administrator", "module": "Stock", "name": "Shipment", + "naming_rule": "Expression (old style)", "owner": "Administrator", "permissions": [ { @@ -470,5 +471,6 @@ ], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 -} +} \ No newline at end of file diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 1c514a90ee..fb1f77ad3b 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -112,6 +112,10 @@ frappe.ui.form.on('Stock Entry', { } }); attach_bom_items(frm.doc.bom_no); + + if(!check_should_not_attach_bom_items(frm.doc.bom_no)) { + erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); + } }, setup_quality_inspection: function(frm) { @@ -129,7 +133,7 @@ frappe.ui.form.on('Stock Entry', { let quality_inspection_field = frm.get_docfield("items", "quality_inspection"); quality_inspection_field.get_route_options_for_new_doc = function(row) { - if (frm.is_new()) return; + if (frm.is_new()) return {}; return { "inspection_type": "Incoming", "reference_type": frm.doc.doctype, @@ -165,6 +169,8 @@ frappe.ui.form.on('Stock Entry', { }, refresh: function(frm) { + frm.trigger("get_items_from_transit_entry"); + if(!frm.doc.docstatus) { frm.trigger('validate_purpose_consumption'); frm.add_custom_button(__('Material Request'), function() { @@ -174,6 +180,8 @@ frappe.ui.form.on('Stock Entry', { if(!items.length) { items = frm.doc.items; } + + mr.work_order = frm.doc.work_order; items.forEach(function(item) { var mr_item = frappe.model.add_child(mr, 'items'); mr_item.item_code = item.item_code; @@ -324,7 +332,33 @@ frappe.ui.form.on('Stock Entry', { } frm.trigger("setup_quality_inspection"); - attach_bom_items(frm.doc.bom_no) + attach_bom_items(frm.doc.bom_no); + + if(!check_should_not_attach_bom_items(frm.doc.bom_no)) { + erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); + } + }, + + get_items_from_transit_entry: function(frm) { + if (frm.doc.docstatus===0) { + frm.add_custom_button(__('Transit Entry'), function() { + erpnext.utils.map_current_doc({ + method: "erpnext.stock.doctype.stock_entry.stock_entry.make_stock_in_entry", + source_doctype: "Stock Entry", + target: frm, + date_field: "posting_date", + setters: { + stock_entry_type: "Material Transfer", + purpose: "Material Transfer", + }, + get_query_filters: { + docstatus: 1, + purpose: "Material Transfer", + add_to_transit: 1, + } + }) + }, __("Get Items From")); + } }, before_save: function(frm) { @@ -583,18 +617,23 @@ frappe.ui.form.on('Stock Entry', { }, add_to_transit: function(frm) { - if(frm.doc.add_to_transit && frm.doc.purpose=='Material Transfer') { - frm.set_value('to_warehouse', ''); + if(frm.doc.purpose=='Material Transfer') { + var filters = { + 'is_group': 0, + 'company': frm.doc.company + } + + if(frm.doc.add_to_transit){ + filters['warehouse_type'] = 'Transit'; + frm.set_value('to_warehouse', ''); + frm.trigger('set_transit_warehouse'); + } + frm.fields_dict.to_warehouse.get_query = function() { return { - filters:{ - 'warehouse_type' : 'Transit', - 'is_group': 0, - 'company': frm.doc.company - } + filters:filters }; }; - frm.trigger('set_transit_warehouse'); } }, @@ -618,6 +657,12 @@ frappe.ui.form.on('Stock Entry', { purchase_order: (frm) => { if (frm.doc.purchase_order) { frm.set_value("subcontracting_order", ""); + erpnext.utils.map_current_doc({ + method: 'erpnext.stock.doctype.stock_entry.stock_entry.get_items_from_subcontract_order', + source_name: frm.doc.purchase_order, + target_doc: frm, + freeze: true, + }); } }, @@ -625,7 +670,7 @@ frappe.ui.form.on('Stock Entry', { if (frm.doc.subcontracting_order) { frm.set_value("purchase_order", ""); erpnext.utils.map_current_doc({ - method: 'erpnext.stock.doctype.stock_entry.stock_entry.get_items_from_subcontracting_order', + method: 'erpnext.stock.doctype.stock_entry.stock_entry.get_items_from_subcontract_order', source_name: frm.doc.subcontracting_order, target_doc: frm, freeze: true, @@ -808,7 +853,8 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle return { "filters": { "docstatus": 1, - "company": me.frm.doc.company + "company": me.frm.doc.company, + "status": ["not in", ["Completed", "Closed"]] } }; }); @@ -925,7 +971,10 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle method: "get_items", callback: function(r) { if(!r.exc) refresh_field("items"); - if(me.frm.doc.bom_no) attach_bom_items(me.frm.doc.bom_no) + if(me.frm.doc.bom_no) { + attach_bom_items(me.frm.doc.bom_no); + erpnext.accounts.dimensions.update_dimension(me.frm, me.frm.doctype); + } } }); } @@ -1065,7 +1114,8 @@ erpnext.stock.select_batch_and_serial_no = (frm, item) => { if (frm.doc.purpose === 'Material Receipt') return; frappe.require("assets/erpnext/js/utils/serial_no_batch_selector.js", function() { - new erpnext.SerialNoBatchSelector({ + if (frm.batch_selector?.dialog?.display) return; + frm.batch_selector = new erpnext.SerialNoBatchSelector({ frm: frm, item: item, warehouse_details: get_warehouse_type_and_name(item), diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.json b/erpnext/stock/doctype/stock_entry/stock_entry.json index abe98e2933..9c0f1fc03f 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.json +++ b/erpnext/stock/doctype/stock_entry/stock_entry.json @@ -7,7 +7,7 @@ "document_type": "Document", "engine": "InnoDB", "field_order": [ - "items_section", + "stock_entry_details_tab", "naming_series", "stock_entry_type", "outgoing_stock_entry", @@ -26,15 +26,20 @@ "posting_time", "set_posting_time", "inspection_required", - "from_bom", "apply_putaway_rule", - "sb1", - "bom_no", - "fg_completed_qty", - "cb1", + "items_tab", + "bom_info_section", + "from_bom", "use_multi_level_bom", + "bom_no", + "cb1", + "fg_completed_qty", "get_items", - "section_break_12", + "section_break_7qsm", + "process_loss_percentage", + "column_break_e92r", + "process_loss_qty", + "section_break_jwgn", "from_warehouse", "source_warehouse_address", "source_address_display", @@ -44,6 +49,7 @@ "target_address_display", "sb0", "scan_barcode", + "items_section", "items", "get_stock_and_rate", "section_break_19", @@ -54,6 +60,7 @@ "additional_costs_section", "additional_costs", "total_additional_costs", + "supplier_info_tab", "contact_section", "supplier", "supplier_name", @@ -61,7 +68,7 @@ "address_display", "accounting_dimensions_section", "project", - "dimension_col_break", + "other_info_tab", "printing_settings", "select_print_heading", "print_settings_col_break", @@ -78,11 +85,6 @@ "is_return" ], "fields": [ - { - "fieldname": "items_section", - "fieldtype": "Section Break", - "oldfieldtype": "Section Break" - }, { "fieldname": "naming_series", "fieldtype": "Select", @@ -148,19 +150,19 @@ "search_index": 1 }, { - "depends_on": "eval:doc.purpose==\"Send to Subcontractor\"", - "fieldname": "purchase_order", - "fieldtype": "Link", - "label": "Purchase Order", - "options": "Purchase Order" + "depends_on": "eval:doc.purpose==\"Send to Subcontractor\"", + "fieldname": "purchase_order", + "fieldtype": "Link", + "label": "Purchase Order", + "options": "Purchase Order" }, { - "depends_on": "eval:doc.purpose==\"Send to Subcontractor\"", - "fieldname": "subcontracting_order", - "fieldtype": "Link", - "label": "Subcontracting Order", - "options": "Subcontracting Order" - }, + "depends_on": "eval:doc.purpose==\"Send to Subcontractor\"", + "fieldname": "subcontracting_order", + "fieldtype": "Link", + "label": "Subcontracting Order", + "options": "Subcontracting Order" + }, { "depends_on": "eval:doc.purpose==\"Sales Return\"", "fieldname": "delivery_note_no", @@ -236,17 +238,12 @@ }, { "default": "0", - "depends_on": "eval:in_list([\"Material Issue\", \"Material Transfer\", \"Manufacture\", \"Repack\", \t\t\t\t\t\"Send to Subcontractor\", \"Material Transfer for Manufacture\", \"Material Consumption for Manufacture\"], doc.purpose)", + "depends_on": "eval:in_list([\"Material Issue\", \"Material Transfer\", \"Manufacture\", \"Repack\", \"Send to Subcontractor\", \"Material Transfer for Manufacture\", \"Material Consumption for Manufacture\"], doc.purpose)", "fieldname": "from_bom", "fieldtype": "Check", "label": "From BOM", "print_hide": 1 }, - { - "depends_on": "eval: doc.from_bom && (doc.purpose!==\"Sales Return\" && doc.purpose!==\"Purchase Return\")", - "fieldname": "sb1", - "fieldtype": "Section Break" - }, { "depends_on": "from_bom", "fieldname": "bom_no", @@ -285,10 +282,6 @@ "oldfieldtype": "Button", "print_hide": 1 }, - { - "fieldname": "section_break_12", - "fieldtype": "Section Break" - }, { "description": "Sets 'Source Warehouse' in each row of the items table.", "fieldname": "from_warehouse", @@ -411,7 +404,7 @@ "collapsible": 1, "collapsible_depends_on": "total_additional_costs", "fieldname": "additional_costs_section", - "fieldtype": "Section Break", + "fieldtype": "Tab Break", "label": "Additional Costs" }, { @@ -576,13 +569,9 @@ { "collapsible": 1, "fieldname": "accounting_dimensions_section", - "fieldtype": "Section Break", + "fieldtype": "Tab Break", "label": "Accounting Dimensions" }, - { - "fieldname": "dimension_col_break", - "fieldtype": "Column Break" - }, { "fieldname": "pick_list", "fieldtype": "Link", @@ -616,10 +605,71 @@ "fieldname": "is_return", "fieldtype": "Check", "hidden": 1, + "in_list_view": 1, "label": "Is Return", "no_copy": 1, "print_hide": 1, "read_only": 1 + }, + { + "fieldname": "items_tab", + "fieldtype": "Tab Break", + "label": "Items" + }, + { + "fieldname": "bom_info_section", + "fieldtype": "Section Break", + "label": "BOM Info" + }, + { + "collapsible": 1, + "fieldname": "section_break_jwgn", + "fieldtype": "Section Break", + "label": "Default Warehouse" + }, + { + "fieldname": "other_info_tab", + "fieldtype": "Tab Break", + "label": "Other Info" + }, + { + "fieldname": "supplier_info_tab", + "fieldtype": "Tab Break", + "label": "Supplier Info" + }, + { + "fieldname": "stock_entry_details_tab", + "fieldtype": "Tab Break", + "label": "Details", + "oldfieldtype": "Section Break" + }, + { + "fieldname": "section_break_7qsm", + "fieldtype": "Section Break" + }, + { + "depends_on": "process_loss_percentage", + "fieldname": "process_loss_qty", + "fieldtype": "Float", + "label": "Process Loss Qty", + "read_only": 1 + }, + { + "fieldname": "column_break_e92r", + "fieldtype": "Column Break" + }, + { + "depends_on": "eval:doc.from_bom && doc.fg_completed_qty", + "fetch_from": "bom_no.process_loss_percentage", + "fetch_if_empty": 1, + "fieldname": "process_loss_percentage", + "fieldtype": "Percent", + "label": "% Process Loss" + }, + { + "fieldname": "items_section", + "fieldtype": "Section Break", + "label": "Items" } ], "icon": "fa fa-file-text", @@ -627,7 +677,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2022-05-02 05:21:39.060501", + "modified": "2023-01-03 16:02:50.741816", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry", diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index f719c1efda..7f69397fce 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -83,9 +83,6 @@ class StockEntry(StockController): } ) - def get_feed(self): - return self.stock_entry_type - def onload(self): for item in self.get("items"): item.update(get_bin_details(item.item_code, item.s_warehouse)) @@ -116,6 +113,9 @@ class StockEntry(StockController): self.validate_warehouse() self.validate_work_order() self.validate_bom() + self.set_process_loss_qty() + self.validate_purchase_order() + self.validate_subcontracting_order() if self.purpose in ("Manufacture", "Repack"): self.mark_finished_and_scrap_items() @@ -124,7 +124,7 @@ class StockEntry(StockController): self.validate_with_material_request() self.validate_batch() self.validate_inspection() - # self.validate_fg_completed_qty() + self.validate_fg_completed_qty() self.validate_difference_account() self.set_job_card_data() self.set_purpose_for_stock_entry() @@ -158,6 +158,7 @@ class StockEntry(StockController): self.validate_subcontract_order() self.update_subcontract_order_supplied_items() self.update_subcontracting_order_status() + self.update_pick_list_status() self.make_gl_entries() @@ -386,11 +387,20 @@ class StockEntry(StockController): item_wise_qty = {} if self.purpose == "Manufacture" and self.work_order: for d in self.items: - if d.is_finished_item or d.is_process_loss: + if d.is_finished_item: item_wise_qty.setdefault(d.item_code, []).append(d.qty) + precision = frappe.get_precision("Stock Entry Detail", "qty") for item_code, qty_list in item_wise_qty.items(): - total = flt(sum(qty_list), frappe.get_precision("Stock Entry Detail", "qty")) + total = flt(sum(qty_list), precision) + + if (self.fg_completed_qty - total) > 0: + self.process_loss_qty = flt(self.fg_completed_qty - total, precision) + self.process_loss_percentage = flt(self.process_loss_qty * 100 / self.fg_completed_qty) + + if self.process_loss_qty: + total += flt(self.process_loss_qty, precision) + if self.fg_completed_qty != total: frappe.throw( _("The finished product {0} quantity {1} and For Quantity {2} cannot be different").format( @@ -469,7 +479,7 @@ class StockEntry(StockController): if self.purpose == "Manufacture": if validate_for_manufacture: - if d.is_finished_item or d.is_scrap_item or d.is_process_loss: + if d.is_finished_item or d.is_scrap_item: d.s_warehouse = None if not d.t_warehouse: frappe.throw(_("Target warehouse is mandatory for row {0}").format(d.idx)) @@ -646,9 +656,7 @@ class StockEntry(StockController): outgoing_items_cost = self.set_rate_for_outgoing_items( reset_outgoing_rate, raise_error_if_no_rate ) - finished_item_qty = sum( - d.transfer_qty for d in self.items if d.is_finished_item or d.is_process_loss - ) + finished_item_qty = sum(d.transfer_qty for d in self.items if d.is_finished_item) # Set basic rate for incoming items for d in self.get("items"): @@ -657,6 +665,13 @@ class StockEntry(StockController): if d.allow_zero_valuation_rate: d.basic_rate = 0.0 + frappe.msgprint( + _( + "Row {0}: Item rate has been updated to zero as Allow Zero Valuation Rate is checked for item {1}" + ).format(d.idx, d.item_code), + alert=1, + ) + elif d.is_finished_item: if self.purpose == "Manufacture": d.basic_rate = self.get_basic_rate_for_manufactured_item( @@ -680,8 +695,6 @@ class StockEntry(StockController): # do not round off basic rate to avoid precision loss d.basic_rate = flt(d.basic_rate) - if d.is_process_loss: - d.basic_rate = flt(0.0) d.basic_amount = flt(flt(d.transfer_qty) * flt(d.basic_rate), d.precision("basic_amount")) def set_rate_for_outgoing_items(self, reset_outgoing_rate=True, raise_error_if_no_rate=True): @@ -874,25 +887,24 @@ class StockEntry(StockController): ) ) - parent = frappe.qb.DocType("Stock Entry") - child = frappe.qb.DocType("Stock Entry Detail") - - conditions = ( - (parent.docstatus == 1) - & (child.item_code == se_item.item_code) - & ( - (parent.purchase_order == self.purchase_order) - if self.subcontract_data.order_doctype == "Purchase Order" - else (parent.subcontracting_order == self.subcontracting_order) - ) - ) + se = frappe.qb.DocType("Stock Entry") + se_detail = frappe.qb.DocType("Stock Entry Detail") total_supplied = ( - frappe.qb.from_(parent) - .inner_join(child) - .on(parent.name == child.parent) - .select(Sum(child.transfer_qty)) - .where(conditions) + frappe.qb.from_(se) + .inner_join(se_detail) + .on(se.name == se_detail.parent) + .select(Sum(se_detail.transfer_qty)) + .where( + (se.purpose == "Send to Subcontractor") + & (se.docstatus == 1) + & (se_detail.item_code == se_item.item_code) + & ( + (se.purchase_order == self.purchase_order) + if self.subcontract_data.order_doctype == "Purchase Order" + else (se.subcontracting_order == self.subcontracting_order) + ) + ) ).run()[0][0] if flt(total_supplied, precision) > flt(total_allowed, precision): @@ -918,6 +930,16 @@ class StockEntry(StockController): ) if order_rm_detail: se_item.db_set(self.subcontract_data.rm_detail_field, order_rm_detail) + else: + if not se_item.allow_alternative_item: + frappe.throw( + _("Row {0}# Item {1} not found in 'Raw Materials Supplied' table in {2} {3}").format( + se_item.idx, + se_item.item_code, + self.subcontract_data.order_doctype, + self.get(self.subcontract_data.order_field), + ) + ) elif backflush_raw_materials_based_on == "Material Transferred for Subcontract": for row in self.items: if not row.subcontracted_item: @@ -946,8 +968,37 @@ class StockEntry(StockController): item_code = d.original_item or d.item_code validate_bom_no(item_code, d.bom_no) + def validate_purchase_order(self): + if self.purpose == "Send to Subcontractor" and self.get("purchase_order"): + is_old_subcontracting_flow = frappe.db.get_value( + "Purchase Order", self.purchase_order, "is_old_subcontracting_flow" + ) + + if not is_old_subcontracting_flow: + frappe.throw( + _("Please select Subcontracting Order instead of Purchase Order {0}").format( + self.purchase_order + ) + ) + + def validate_subcontracting_order(self): + if self.get("subcontracting_order") and self.purpose in [ + "Send to Subcontractor", + "Material Transfer", + ]: + sco_status = frappe.db.get_value("Subcontracting Order", self.subcontracting_order, "status") + + if sco_status == "Closed": + frappe.throw( + _("Cannot create Stock Entry against a closed Subcontracting Order {0}.").format( + self.subcontracting_order + ) + ) + def mark_finished_and_scrap_items(self): - if any([d.item_code for d in self.items if (d.is_finished_item and d.t_warehouse)]): + if self.purpose != "Repack" and any( + [d.item_code for d in self.items if (d.is_finished_item and d.t_warehouse)] + ): return finished_item = self.get_finished_item() @@ -1035,8 +1086,8 @@ class StockEntry(StockController): # 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 + _("For quantity {0} should not be greater than allowed quantity {1}").format( + flt(self.fg_completed_qty), allowed_qty ) ) @@ -1174,13 +1225,19 @@ class StockEntry(StockController): def update_work_order(self): def _validate_work_order(pro_doc): + msg, title = "", "" if flt(pro_doc.docstatus) != 1: - frappe.throw(_("Work Order {0} must be submitted").format(self.work_order)) + msg = f"Work Order {self.work_order} must be submitted" if pro_doc.status == "Stopped": - frappe.throw( - _("Transaction not allowed against stopped Work Order {0}").format(self.work_order) - ) + msg = f"Transaction not allowed against stopped Work Order {self.work_order}" + + if self.is_return and pro_doc.status not in ["Completed", "Closed"]: + title = _("Stock Return") + msg = f"Work Order {self.work_order} must be completed or closed" + + if msg: + frappe.throw(_(msg), title=title) if self.job_card: job_doc = frappe.get_doc("Job Card", self.job_card) @@ -1190,7 +1247,6 @@ class StockEntry(StockController): if self.work_order: pro_doc = frappe.get_doc("Work Order", self.work_order) _validate_work_order(pro_doc) - pro_doc.run_method("update_status") if self.fg_completed_qty: pro_doc.run_method("update_work_order_qty") @@ -1198,6 +1254,7 @@ class StockEntry(StockController): pro_doc.run_method("update_planned_qty") pro_doc.update_batch_produced_qty(self) + pro_doc.run_method("update_status") if not pro_doc.operations: pro_doc.set_actual_dates() @@ -1418,11 +1475,11 @@ class StockEntry(StockController): # add finished goods item if self.purpose in ("Manufacture", "Repack"): + self.set_process_loss_qty() self.load_items_from_bom() self.set_scrap_items() self.set_actual_qty() - self.update_items_for_process_loss() self.validate_customer_provided_item() self.calculate_rate_and_amount(raise_error_if_no_rate=False) @@ -1435,6 +1492,21 @@ class StockEntry(StockController): self.add_to_stock_entry_detail(scrap_item_dict, bom_no=self.bom_no) + def set_process_loss_qty(self): + if self.purpose not in ("Manufacture", "Repack"): + return + + self.process_loss_qty = 0.0 + if not self.process_loss_percentage: + self.process_loss_percentage = frappe.get_cached_value( + "BOM", self.bom_no, "process_loss_percentage" + ) + + if self.process_loss_percentage: + self.process_loss_qty = flt( + (flt(self.fg_completed_qty) * flt(self.process_loss_percentage)) / 100 + ) + def set_work_order_details(self): if not getattr(self, "pro_doc", None): self.pro_doc = frappe._dict() @@ -1467,7 +1539,7 @@ class StockEntry(StockController): args = { "to_warehouse": to_warehouse, "from_warehouse": "", - "qty": self.fg_completed_qty, + "qty": flt(self.fg_completed_qty) - flt(self.process_loss_qty), "item_name": item.item_name, "description": item.description, "stock_uom": item.stock_uom, @@ -1494,6 +1566,7 @@ class StockEntry(StockController): "reference_name": self.pro_doc.name, "reference_doctype": self.pro_doc.doctype, "qty_to_produce": (">", 0), + "batch_qty": ("=", 0), } fields = ["qty_to_produce as qty", "produced_qty", "name"] @@ -1716,10 +1789,12 @@ class StockEntry(StockController): for key, row in available_materials.items(): remaining_qty_to_produce = flt(wo_data.trans_qty) - flt(wo_data.produced_qty) - if remaining_qty_to_produce <= 0: + if remaining_qty_to_produce <= 0 and not self.is_return: continue - qty = (flt(row.qty) * flt(self.fg_completed_qty)) / remaining_qty_to_produce + qty = flt(row.qty) + if not self.is_return: + qty = (flt(row.qty) * flt(self.fg_completed_qty)) / remaining_qty_to_produce item = row.item_details if cint(frappe.get_cached_value("UOM", item.stock_uom, "must_be_whole_number")): @@ -1743,6 +1818,9 @@ class StockEntry(StockController): self.update_item_in_stock_entry_detail(row, item, qty) def update_item_in_stock_entry_detail(self, row, item, qty) -> None: + if not qty: + return + ste_item_details = { "from_warehouse": item.warehouse, "to_warehouse": "", @@ -1756,6 +1834,9 @@ class StockEntry(StockController): "original_item": item.original_item, } + if self.is_return: + ste_item_details["to_warehouse"] = item.s_warehouse + if row.serial_nos: serial_nos = row.serial_nos if item.batch_no: @@ -1906,7 +1987,8 @@ class StockEntry(StockController): ) 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) + se_child.po_detail = item_row.get("po_detail") + se_child.sco_rm_detail = item_row.get("sco_rm_detail") for field in [ self.subcontract_data.rm_detail_field, @@ -2151,42 +2233,17 @@ class StockEntry(StockController): material_requests.append(material_request) frappe.db.set_value("Material Request", material_request, "transfer_status", status) - def update_items_for_process_loss(self): - process_loss_dict = {} - for d in self.get("items"): - if not d.is_process_loss: - continue - - scrap_warehouse = frappe.db.get_single_value( - "Manufacturing Settings", "default_scrap_warehouse" - ) - if scrap_warehouse is not None: - d.t_warehouse = scrap_warehouse - d.is_scrap_item = 0 - - if d.item_code not in process_loss_dict: - process_loss_dict[d.item_code] = [flt(0), flt(0)] - process_loss_dict[d.item_code][0] += flt(d.transfer_qty) - process_loss_dict[d.item_code][1] += flt(d.qty) - - for d in self.get("items"): - if not d.is_finished_item or d.item_code not in process_loss_dict: - continue - # Assumption: 1 finished item has 1 row. - d.transfer_qty -= process_loss_dict[d.item_code][0] - d.qty -= process_loss_dict[d.item_code][1] - def set_serial_no_batch_for_finished_good(self): - args = {} + serial_nos = [] if self.pro_doc.serial_no: - self.get_serial_nos_for_fg(args) + serial_nos = self.get_serial_nos_for_fg() or [] for row in self.items: if row.is_finished_item and row.item_code == self.pro_doc.production_item: - if args.get("serial_no"): - row.serial_no = "\n".join(args["serial_no"][0 : cint(row.qty)]) + if serial_nos: + row.serial_no = "\n".join(serial_nos[0 : cint(row.qty)]) - def get_serial_nos_for_fg(self, args): + def get_serial_nos_for_fg(self): fields = [ "`tabStock Entry`.`name`", "`tabStock Entry Detail`.`qty`", @@ -2197,14 +2254,12 @@ class StockEntry(StockController): filters = [ ["Stock Entry", "work_order", "=", self.work_order], ["Stock Entry", "purpose", "=", "Manufacture"], - ["Stock Entry", "docstatus", "=", 1], + ["Stock Entry", "docstatus", "<", 2], ["Stock Entry Detail", "item_code", "=", self.pro_doc.production_item], ] stock_entries = frappe.get_all("Stock Entry", fields=fields, filters=filters) - - if self.pro_doc.serial_no: - args["serial_no"] = self.get_available_serial_nos(stock_entries) + return self.get_available_serial_nos(stock_entries) def get_available_serial_nos(self, stock_entries): used_serial_nos = [] @@ -2215,13 +2270,18 @@ class StockEntry(StockController): return sorted(list(set(get_serial_nos(self.pro_doc.serial_no)) - set(used_serial_nos))) def update_subcontracting_order_status(self): - if self.subcontracting_order and self.purpose == "Send to Subcontractor": + if self.subcontracting_order and self.purpose in ["Send to Subcontractor", "Material Transfer"]: from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import ( update_subcontracting_order_status, ) update_subcontracting_order_status(self.subcontracting_order) + def update_pick_list_status(self): + from erpnext.stock.doctype.pick_list.pick_list import update_pick_list_status + + update_pick_list_status(self.pick_list) + def set_missing_values(self): "Updates rate and availability of all the items of mapped doc." self.set_transfer_qty() @@ -2434,7 +2494,7 @@ def get_uom_details(item_code, uom, qty): if not conversion_factor: frappe.msgprint( - _("UOM coversion factor required for UOM: {0} in Item: {1}").format(uom, item_code) + _("UOM conversion factor required for UOM: {0} in Item: {1}").format(uom, item_code) ) ret = {"uom": ""} else: @@ -2553,34 +2613,16 @@ def get_supplied_items( @frappe.whitelist() -def get_items_from_subcontracting_order(source_name, target_doc=None): - sco = frappe.get_doc("Subcontracting Order", source_name) +def get_items_from_subcontract_order(source_name, target_doc=None): + from erpnext.controllers.subcontracting_controller import make_rm_stock_entry - if sco.docstatus == 1: - if target_doc and isinstance(target_doc, str): - target_doc = frappe.get_doc(json.loads(target_doc)) + if isinstance(target_doc, str): + target_doc = frappe.get_doc(json.loads(target_doc)) - if target_doc.items: - target_doc.items = [] - - warehouses = {} - for item in sco.items: - warehouses[item.name] = item.warehouse - - for item in sco.supplied_items: - target_doc.append( - "items", - { - "s_warehouse": warehouses.get(item.reference_name), - "t_warehouse": sco.supplier_warehouse, - "item_code": item.rm_item_code, - "qty": item.required_qty, - "transfer_qty": item.required_qty, - "uom": item.stock_uom, - "stock_uom": item.stock_uom, - "conversion_factor": 1, - }, - ) + order_doctype = "Purchase Order" if target_doc.purchase_order else "Subcontracting Order" + target_doc = make_rm_stock_entry( + subcontract_order=source_name, order_doctype=order_doctype, target_doc=target_doc + ) return target_doc diff --git a/erpnext/stock/doctype/stock_entry/stock_entry_list.js b/erpnext/stock/doctype/stock_entry/stock_entry_list.js index cbc3491eba..af29d495ff 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry_list.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry_list.js @@ -1,8 +1,12 @@ frappe.listview_settings['Stock Entry'] = { add_fields: ["`tabStock Entry`.`from_warehouse`", "`tabStock Entry`.`to_warehouse`", - "`tabStock Entry`.`purpose`", "`tabStock Entry`.`work_order`", "`tabStock Entry`.`bom_no`"], + "`tabStock Entry`.`purpose`", "`tabStock Entry`.`work_order`", "`tabStock Entry`.`bom_no`", + "`tabStock Entry`.`is_return`"], get_indicator: function (doc) { - if (doc.docstatus === 0) { + if(doc.is_return===1 && doc.purpose === "Material Transfer for Manufacture") { + return [__("Material Returned from WIP"), "orange", + "is_return,=,1|purpose,=,Material Transfer for Manufacture|docstatus,<,2"]; + } else if (doc.docstatus === 0) { return [__("Draft"), "red", "docstatus,=,0"]; } else if (doc.purpose === 'Send to Warehouse' && doc.per_transferred < 100) { diff --git a/erpnext/stock/doctype/stock_entry/stock_entry_utils.py b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py index 41a3b8916d..0f9001392d 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry_utils.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py @@ -117,6 +117,7 @@ def make_stock_entry(**args): args.item = "_Test Item" s.company = args.company or erpnext.get_default_company() + s.add_to_transit = args.add_to_transit or 0 s.purchase_receipt_no = args.purchase_receipt_no s.delivery_note_no = args.delivery_note_no s.sales_invoice_no = args.sales_invoice_no diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index a2f9978670..cc06bd709a 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -5,7 +5,7 @@ import frappe from frappe.permissions import add_user_permission, remove_user_permission from frappe.tests.utils import FrappeTestCase, change_settings -from frappe.utils import add_days, flt, nowdate, nowtime +from frappe.utils import add_days, flt, nowdate, nowtime, today from erpnext.accounts.doctype.account.test_account import get_inventory_account from erpnext.stock.doctype.item.test_item import ( @@ -17,6 +17,7 @@ from erpnext.stock.doctype.item.test_item import ( from erpnext.stock.doctype.serial_no.serial_no import * # noqa from erpnext.stock.doctype.stock_entry.stock_entry import ( FinishedGoodError, + make_stock_in_entry, move_sample_to_retention_warehouse, ) from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry @@ -160,6 +161,53 @@ class TestStockEntry(FrappeTestCase): self.assertTrue(item_code in items) + def test_add_to_transit_entry(self): + from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + + item_code = "_Test Transit Item" + company = "_Test Company" + + create_warehouse("Test From Warehouse") + create_warehouse("Test Transit Warehouse") + create_warehouse("Test To Warehouse") + + create_item( + item_code=item_code, + is_stock_item=1, + is_purchase_item=1, + company=company, + ) + + # create inward stock entry + make_stock_entry( + item_code=item_code, + target="Test From Warehouse - _TC", + qty=10, + basic_rate=100, + expense_account="Stock Adjustment - _TC", + cost_center="Main - _TC", + ) + + transit_entry = make_stock_entry( + item_code=item_code, + source="Test From Warehouse - _TC", + target="Test Transit Warehouse - _TC", + add_to_transit=1, + stock_entry_type="Material Transfer", + purpose="Material Transfer", + qty=10, + basic_rate=100, + expense_account="Stock Adjustment - _TC", + cost_center="Main - _TC", + ) + + end_transit_entry = make_stock_in_entry(transit_entry.name) + self.assertEqual(transit_entry.name, end_transit_entry.outgoing_stock_entry) + self.assertEqual(transit_entry.name, end_transit_entry.items[0].against_stock_entry) + self.assertEqual(transit_entry.items[0].name, end_transit_entry.items[0].ste_detail) + + # create add to transit + def test_material_receipt_gl_entry(self): company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company") @@ -1589,6 +1637,73 @@ class TestStockEntry(FrappeTestCase): self.assertEqual(obj.items[index].basic_rate, 200) self.assertEqual(obj.items[index].basic_amount, 2000) + def test_batch_expiry(self): + from erpnext.controllers.stock_controller import BatchExpiredError + from erpnext.stock.doctype.batch.test_batch import make_new_batch + + item_code = "Test Batch Expiry Test Item - 001" + item_doc = create_item(item_code=item_code, is_stock_item=1, valuation_rate=10) + + item_doc.has_batch_no = 1 + item_doc.save() + + batch = make_new_batch( + batch_id=frappe.generate_hash("", 5), item_code=item_doc.name, expiry_date=add_days(today(), -1) + ) + + se = make_stock_entry( + item_code=item_code, + purpose="Material Receipt", + qty=4, + to_warehouse="_Test Warehouse - _TC", + batch_no=batch.name, + do_not_save=True, + ) + + self.assertRaises(BatchExpiredError, se.save) + + def test_negative_stock_reco(self): + from erpnext.controllers.stock_controller import BatchExpiredError + from erpnext.stock.doctype.batch.test_batch import make_new_batch + + frappe.db.set_single_value("Stock Settings", "allow_negative_stock", 0) + + item_code = "Test Negative Item - 001" + item_doc = create_item(item_code=item_code, is_stock_item=1, valuation_rate=10) + + make_stock_entry( + item_code=item_code, + posting_date=add_days(today(), -3), + posting_time="00:00:00", + purpose="Material Receipt", + qty=10, + to_warehouse="_Test Warehouse - _TC", + do_not_save=True, + ) + + make_stock_entry( + item_code=item_code, + posting_date=today(), + posting_time="00:00:00", + purpose="Material Receipt", + qty=8, + from_warehouse="_Test Warehouse - _TC", + do_not_save=True, + ) + + sr_doc = create_stock_reconciliation( + purpose="Stock Reconciliation", + posting_date=add_days(today(), -3), + posting_time="00:00:00", + item_code=item_code, + warehouse="_Test Warehouse - _TC", + valuation_rate=10, + qty=7, + do_not_submit=True, + ) + + self.assertRaises(frappe.ValidationError, sr_doc.submit) + def make_serialized_item(**args): args = frappe._dict(args) diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json index 5fe11a2aa7..fe81a87558 100644 --- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json +++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json @@ -8,6 +8,7 @@ "engine": "InnoDB", "field_order": [ "barcode", + "has_item_scanned", "section_break_2", "s_warehouse", "col_break1", @@ -19,7 +20,6 @@ "is_finished_item", "is_scrap_item", "quality_inspection", - "is_process_loss", "subcontracted_item", "section_break_8", "description", @@ -498,14 +498,14 @@ "read_only": 1 }, { - "fieldname": "sco_rm_detail", - "fieldtype": "Data", - "hidden": 1, - "label": "SCO Supplied Item", - "no_copy": 1, - "print_hide": 1, - "read_only": 1 - }, + "fieldname": "sco_rm_detail", + "fieldtype": "Data", + "hidden": 1, + "label": "SCO Supplied Item", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, { "default": "0", "depends_on": "eval:parent.purpose===\"Repack\" && doc.t_warehouse", @@ -560,16 +560,18 @@ }, { "default": "0", - "fieldname": "is_process_loss", + "depends_on": "barcode", + "fieldname": "has_item_scanned", "fieldtype": "Check", - "label": "Is Process Loss" + "label": "Has Item Scanned", + "read_only": 1 } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-06-17 05:06:33.621264", + "modified": "2023-01-03 14:51:16.575515", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry Detail", 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 329cd7da09..052f7781c1 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py @@ -153,6 +153,11 @@ class StockLedgerEntry(Document): def validate_batch(self): if self.batch_no and self.voucher_type != "Stock Entry": + if (self.voucher_type in ["Purchase Receipt", "Purchase Invoice"] and self.actual_qty < 0) or ( + self.voucher_type in ["Delivery Note", "Sales Invoice"] and self.actual_qty > 0 + ): + return + expiry_date = frappe.db.get_value("Batch", self.batch_no, "expiry_date") if expiry_date: if getdate(self.posting_date) > getdate(expiry_date): @@ -216,14 +221,9 @@ class StockLedgerEntry(Document): def on_doctype_update(): - if not frappe.db.has_index("tabStock Ledger Entry", "posting_sort_index"): - frappe.db.commit() - frappe.db.add_index( - "Stock Ledger Entry", - fields=["posting_date", "posting_time", "name"], - index_name="posting_sort_index", - ) - + frappe.db.add_index( + "Stock Ledger Entry", fields=["posting_date", "posting_time"], index_name="posting_sort_index" + ) frappe.db.add_index("Stock Ledger Entry", ["voucher_no", "voucher_type"]) frappe.db.add_index("Stock Ledger Entry", ["batch_no", "item_code", "warehouse"]) frappe.db.add_index("Stock Ledger Entry", ["warehouse", "item_code"], "item_warehouse") diff --git a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py index 1410da56a3..6c341d9e9e 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py @@ -1323,13 +1323,15 @@ def create_product_bundle_item(new_item_code, packed_items): item.save() -def create_items(): - items = [ - "_Test Item for Reposting", - "_Test Finished Item for Reposting", - "_Test Subcontracted Item for Reposting", - "_Test Bundled Item for Reposting", - ] +def create_items(items=None, uoms=None): + if not items: + items = [ + "_Test Item for Reposting", + "_Test Finished Item for Reposting", + "_Test Subcontracted Item for Reposting", + "_Test Bundled Item for Reposting", + ] + for d in items: properties = {"valuation_method": "FIFO"} if d == "_Test Bundled Item for Reposting": @@ -1337,7 +1339,7 @@ def create_items(): elif d == "_Test Subcontracted Item for Reposting": properties.update({"is_sub_contracted_item": 1}) - make_item(d, properties=properties) + make_item(d, properties=properties, uoms=uoms) return items diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 23e0f1efaf..398b3c98e3 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -132,7 +132,9 @@ class StockReconciliation(StockController): key.append(row.get(field)) if key in item_warehouse_combinations: - self.validation_messages.append(_get_msg(row_num, _("Duplicate entry"))) + self.validation_messages.append( + _get_msg(row_num, _("Same item and warehouse combination already entered.")) + ) else: item_warehouse_combinations.append(key) @@ -228,7 +230,7 @@ class StockReconciliation(StockController): if item.has_serial_no or item.has_batch_no: has_serial_no = True - self.get_sle_for_serialized_items(row, sl_entries) + self.get_sle_for_serialized_items(row, sl_entries, item) else: if row.serial_no or row.batch_no: frappe.throw( @@ -280,7 +282,7 @@ class StockReconciliation(StockController): if has_serial_no and sl_entries: self.update_valuation_rate_for_serial_no() - def get_sle_for_serialized_items(self, row, sl_entries): + def get_sle_for_serialized_items(self, row, sl_entries, item): from erpnext.stock.stock_ledger import get_previous_sle serial_nos = get_serial_nos(row.serial_no) @@ -346,6 +348,9 @@ class StockReconciliation(StockController): if row.qty: args = self.get_sle_for_items(row) + if item.has_serial_no and item.has_batch_no: + args["qty_after_transaction"] = row.qty + args.update( { "actual_qty": row.qty, @@ -710,8 +715,8 @@ def get_itemwise_batch(warehouse, posting_date, company, item_code=None): def get_stock_balance_for( item_code: str, warehouse: str, - posting_date: str, - posting_time: str, + posting_date, + posting_time, batch_no: Optional[str] = None, with_valuation_rate: bool = True, ): diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 191c03f5f1..eaea301432 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -644,6 +644,38 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin): ) self.assertEqual(len(active_sr_no), 0) + def test_serial_no_batch_no_item(self): + item = self.make_item( + "Test Serial No Batch No Item", + { + "is_stock_item": 1, + "has_serial_no": 1, + "has_batch_no": 1, + "serial_no_series": "SRS9.####", + "batch_number_series": "BNS9.####", + "create_new_batch": 1, + }, + ) + + warehouse = "_Test Warehouse - _TC" + + sr = create_stock_reconciliation( + item_code=item.name, + warehouse=warehouse, + qty=1, + rate=100, + ) + + sl_entry = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Stock Reconciliation", "voucher_no": sr.name}, + ["actual_qty", "qty_after_transaction"], + as_dict=1, + ) + + self.assertEqual(flt(sl_entry.actual_qty), 1.0) + self.assertEqual(flt(sl_entry.qty_after_transaction), 1.0) + def create_batch_item_with_batch(item_name, batch_id): batch_item_doc = create_item(item_name, is_stock_item=1) @@ -727,12 +759,21 @@ def create_stock_reconciliation(**args): sr.set_posting_time = 1 sr.company = args.company or "_Test Company" sr.expense_account = args.expense_account or ( - "Stock Adjustment - _TC" if frappe.get_all("Stock Ledger Entry") else "Temporary Opening - _TC" + ( + frappe.get_cached_value("Company", sr.company, "stock_adjustment_account") + or frappe.get_cached_value( + "Account", {"account_type": "Stock Adjustment", "company": sr.company}, "name" + ) + ) + if frappe.get_all("Stock Ledger Entry", {"company": sr.company}) + else frappe.get_cached_value( + "Account", {"account_type": "Temporary", "company": sr.company}, "name" + ) ) sr.cost_center = ( args.cost_center or frappe.get_cached_value("Company", sr.company, "cost_center") - or "_Test Cost Center - _TC" + or frappe.get_cached_value("Cost Center", filters={"is_group": 0, "company": sr.company}) ) sr.append( diff --git a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json index 79c2fcc252..7c3e151663 100644 --- a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json +++ b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json @@ -7,6 +7,7 @@ "engine": "InnoDB", "field_order": [ "barcode", + "has_item_scanned", "item_code", "item_name", "warehouse", @@ -177,11 +178,18 @@ "label": "Allow Zero Valuation Rate", "print_hide": 1, "read_only": 1 + }, + { + "depends_on": "barcode", + "fieldname": "has_item_scanned", + "fieldtype": "Data", + "label": "Has Item Scanned", + "read_only": 1 } ], "istable": 1, "links": [], - "modified": "2022-04-02 04:19:40.380587", + "modified": "2022-11-02 13:01:23.580937", "modified_by": "Administrator", "module": "Stock", "name": "Stock Reconciliation Item", diff --git a/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.js b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.js index 42d0723d42..5f81679bad 100644 --- a/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.js +++ b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.js @@ -2,7 +2,22 @@ // For license information, please see license.txt frappe.ui.form.on('Stock Reposting Settings', { - // refresh: function(frm) { + refresh: function(frm) { + frm.trigger('convert_to_item_based_reposting'); + }, - // } + convert_to_item_based_reposting: function(frm) { + frm.add_custom_button(__('Convert to Item Based Reposting'), function() { + frm.call({ + method: 'convert_to_item_wh_reposting', + frezz: true, + doc: frm.doc, + callback: function(r) { + if (!r.exc) { + frm.reload_doc(); + } + } + }) + }) + } }); diff --git a/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.py b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.py index e0c8ed12e7..51fb5ac4c4 100644 --- a/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.py +++ b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.py @@ -1,6 +1,8 @@ # 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 add_to_date, get_datetime, get_time_str, time_diff_in_hours @@ -24,3 +26,62 @@ class StockRepostingSettings(Document): if diff < 10: self.end_time = get_time_str(add_to_date(self.start_time, hours=10, as_datetime=True)) + + @frappe.whitelist() + def convert_to_item_wh_reposting(self): + """Convert Transaction reposting to Item Warehouse based reposting if Item Based Reposting has enabled.""" + + reposting_data = get_reposting_entries() + + vouchers = [d.voucher_no for d in reposting_data] + + item_warehouses = {} + + for ledger in get_stock_ledgers(vouchers): + key = (ledger.item_code, ledger.warehouse) + if key not in item_warehouses: + item_warehouses[key] = ledger.posting_date + elif frappe.utils.getdate(item_warehouses.get(key)) > frappe.utils.getdate(ledger.posting_date): + item_warehouses[key] = ledger.posting_date + + for key, posting_date in item_warehouses.items(): + item_code, warehouse = key + create_repost_item_valuation(item_code, warehouse, posting_date) + + for row in reposting_data: + frappe.db.set_value("Repost Item Valuation", row.name, "status", "Skipped") + + self.db_set("item_based_reposting", 1) + frappe.msgprint(_("Item Warehouse based reposting has been enabled.")) + + +def get_reposting_entries(): + return frappe.get_all( + "Repost Item Valuation", + fields=["voucher_no", "name"], + filters={"status": ("in", ["Queued", "In Progress"]), "docstatus": 1, "based_on": "Transaction"}, + ) + + +def get_stock_ledgers(vouchers): + return frappe.get_all( + "Stock Ledger Entry", + fields=["item_code", "warehouse", "posting_date"], + filters={"voucher_no": ("in", vouchers)}, + ) + + +def create_repost_item_valuation(item_code, warehouse, posting_date): + frappe.get_doc( + { + "doctype": "Repost Item Valuation", + "company": frappe.get_cached_value("Warehouse", warehouse, "company"), + "posting_date": posting_date, + "based_on": "Item and Warehouse", + "posting_time": "00:00:01", + "item_code": item_code, + "warehouse": warehouse, + "allow_negative_stock": True, + "status": "Queued", + } + ).submit() diff --git a/erpnext/stock/doctype/warehouse/warehouse.py b/erpnext/stock/doctype/warehouse/warehouse.py index ab784ca107..430a8d17e0 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.py +++ b/erpnext/stock/doctype/warehouse/warehouse.py @@ -2,13 +2,12 @@ # License: GNU General Public License v3. See license.txt -from collections import defaultdict - import frappe from frappe import _, throw from frappe.contacts.address_and_contact import load_address_and_contact -from frappe.utils import cint, flt +from frappe.utils import cint from frappe.utils.nestedset import NestedSet +from pypika.terms import ExistsCriterion from erpnext.stock import get_warehouse_account @@ -165,60 +164,7 @@ def get_children(doctype, parent=None, company=None, is_root=False): ["company", "in", (company, None, "")], ] - warehouses = frappe.get_list(doctype, fields=fields, filters=filters, order_by="name") - - company_currency = "" - if company: - company_currency = frappe.get_cached_value("Company", company, "default_currency") - - warehouse_wise_value = get_warehouse_wise_stock_value(company) - - # return warehouses - for wh in warehouses: - wh["balance"] = warehouse_wise_value.get(wh.value) - if company_currency: - wh["company_currency"] = company_currency - return warehouses - - -def get_warehouse_wise_stock_value(company): - warehouses = frappe.get_all( - "Warehouse", fields=["name", "parent_warehouse"], filters={"company": company} - ) - parent_warehouse = {d.name: d.parent_warehouse for d in warehouses} - - filters = {"warehouse": ("in", [data.name for data in warehouses])} - bin_data = frappe.get_all( - "Bin", - fields=["sum(stock_value) as stock_value", "warehouse"], - filters=filters, - group_by="warehouse", - ) - - warehouse_wise_stock_value = defaultdict(float) - for row in bin_data: - if not row.stock_value: - continue - - warehouse_wise_stock_value[row.warehouse] = row.stock_value - update_value_in_parent_warehouse( - warehouse_wise_stock_value, parent_warehouse, row.warehouse, row.stock_value - ) - - return warehouse_wise_stock_value - - -def update_value_in_parent_warehouse( - warehouse_wise_stock_value, parent_warehouse_dict, warehouse, stock_value -): - parent_warehouse = parent_warehouse_dict.get(warehouse) - if not parent_warehouse: - return - - warehouse_wise_stock_value[parent_warehouse] += flt(stock_value) - update_value_in_parent_warehouse( - warehouse_wise_stock_value, parent_warehouse_dict, parent_warehouse, stock_value - ) + return frappe.get_list(doctype, fields=fields, filters=filters, order_by="name") @frappe.whitelist() @@ -266,3 +212,23 @@ def get_warehouses_based_on_account(account, company=None): frappe.throw(_("Warehouse not found against the account {0}").format(account)) return warehouses + + +# Will be use for frappe.qb +def apply_warehouse_filter(query, sle, filters): + if warehouse := filters.get("warehouse"): + warehouse_table = frappe.qb.DocType("Warehouse") + + lft, rgt = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"]) + chilren_subquery = ( + frappe.qb.from_(warehouse_table) + .select(warehouse_table.name) + .where( + (warehouse_table.lft >= lft) + & (warehouse_table.rgt <= rgt) + & (warehouse_table.name == sle.warehouse) + ) + ) + query = query.where(ExistsCriterion(chilren_subquery)) + + return query diff --git a/erpnext/stock/doctype/warehouse/warehouse_tree.js b/erpnext/stock/doctype/warehouse/warehouse_tree.js index e9e14c7246..eb635e6757 100644 --- a/erpnext/stock/doctype/warehouse/warehouse_tree.js +++ b/erpnext/stock/doctype/warehouse/warehouse_tree.js @@ -17,11 +17,4 @@ frappe.treeview_settings['Warehouse'] = { description: __("Child nodes can be only created under 'Group' type nodes")} ], ignore_fields:["parent_warehouse"], - onrender: function(node) { - if (node.data && node.data.balance!==undefined) { - $('' - + format_currency((node.data.balance), node.data.company_currency) - + '').insertBefore(node.$ul); - } - } } diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index e83182f411..b53f429edf 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -88,8 +88,15 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru update_party_blanket_order(args, out) + # Never try to find a customer price if customer is set in these Doctype + current_customer = args.customer + if args.get("doctype") in ["Purchase Order", "Purchase Receipt", "Purchase Invoice"]: + args.customer = None + out.update(get_price_list_rate(args, item)) + args.customer = current_customer + if args.customer and cint(args.is_pos): out.update(get_pos_profile_item_details(args.company, args, update_data=True)) @@ -102,9 +109,11 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru elif out.get("warehouse"): if doc and doc.get("doctype") == "Purchase Order": # calculate company_total_stock only for po - bin_details = get_bin_details(args.item_code, out.warehouse, args.company) + bin_details = get_bin_details( + args.item_code, out.warehouse, args.company, include_child_warehouses=True + ) else: - bin_details = get_bin_details(args.item_code, out.warehouse) + bin_details = get_bin_details(args.item_code, out.warehouse, include_child_warehouses=True) out.update(bin_details) @@ -113,7 +122,7 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru if args.get(key) is None: args[key] = value - data = get_pricing_rule_for_item(args, out.price_list_rate, doc, for_validate=for_validate) + data = get_pricing_rule_for_item(args, doc=doc, for_validate=for_validate) out.update(data) @@ -234,8 +243,10 @@ def validate_item_details(args, item): validate_end_of_life(item.name, item.end_of_life, item.disabled) - if args.transaction_type == "selling" and cint(item.has_variants): - throw(_("Item {0} is a template, please select one of its variants").format(item.name)) + if cint(item.has_variants): + msg = f"Item {item.name} is a template, please select one of its variants" + + throw(_(msg), title=_("Template Item Selected")) elif args.transaction_type == "buying" and args.doctype != "Material Request": if args.get("is_subcontracted"): @@ -330,6 +341,9 @@ def get_basic_details(args, item, overwrite_warehouse=True): else: args.uom = item.stock_uom + # Set stock UOM in args, so that it can be used while fetching item price + args.stock_uom = item.stock_uom + if args.get("batch_no") and item.name != frappe.get_cached_value( "Batch", args.get("batch_no"), "item" ): @@ -406,7 +420,9 @@ def get_basic_details(args, item, overwrite_warehouse=True): args.stock_qty = out.stock_qty # calculate last purchase rate - if args.get("doctype") in purchase_doctypes: + if args.get("doctype") in purchase_doctypes and not frappe.db.get_single_value( + "Buying Settings", "disable_last_purchase_rate" + ): from erpnext.buying.doctype.purchase_order.purchase_order import item_last_purchase_rate out.last_purchase_rate = item_last_purchase_rate( @@ -808,6 +824,9 @@ def get_price_list_rate(args, item_doc, out=None): flt(price_list_rate) * flt(args.plc_conversion_rate) / flt(args.conversion_rate) ) + if frappe.db.get_single_value("Buying Settings", "disable_last_purchase_rate"): + return out + if not out.price_list_rate and args.transaction_type == "buying": from erpnext.stock.doctype.item.item import get_last_purchase_details @@ -825,9 +844,9 @@ def insert_item_price(args): ): if frappe.has_permission("Item Price", "write"): price_list_rate = ( - (args.rate + args.discount_amount) / args.get("conversion_factor") + (flt(args.rate) + flt(args.discount_amount)) / args.get("conversion_factor") if args.get("conversion_factor") - else (args.rate + args.discount_amount) + else (flt(args.rate) + flt(args.discount_amount)) ) item_price = frappe.db.get_value( @@ -1057,7 +1076,9 @@ def get_pos_profile_item_details(company, args, pos_profile=None, update_data=Fa res[fieldname] = pos_profile.get(fieldname) if res.get("warehouse"): - res.actual_qty = get_bin_details(args.item_code, res.warehouse).get("actual_qty") + res.actual_qty = get_bin_details( + args.item_code, res.warehouse, include_child_warehouses=True + ).get("actual_qty") return res @@ -1168,16 +1189,30 @@ def get_projected_qty(item_code, warehouse): @frappe.whitelist() -def get_bin_details(item_code, warehouse, company=None): - bin_details = frappe.db.get_value( - "Bin", - {"item_code": item_code, "warehouse": warehouse}, - ["projected_qty", "actual_qty", "reserved_qty"], - as_dict=True, - cache=True, - ) or {"projected_qty": 0, "actual_qty": 0, "reserved_qty": 0} +def get_bin_details(item_code, warehouse, company=None, include_child_warehouses=False): + bin_details = {"projected_qty": 0, "actual_qty": 0, "reserved_qty": 0} + + if warehouse: + from frappe.query_builder.functions import Coalesce, Sum + + from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses + + warehouses = get_child_warehouses(warehouse) if include_child_warehouses else [warehouse] + + bin = frappe.qb.DocType("Bin") + bin_details = ( + frappe.qb.from_(bin) + .select( + Coalesce(Sum(bin.projected_qty), 0).as_("projected_qty"), + Coalesce(Sum(bin.actual_qty), 0).as_("actual_qty"), + Coalesce(Sum(bin.reserved_qty), 0).as_("reserved_qty"), + ) + .where((bin.item_code == item_code) & (bin.warehouse.isin(warehouses))) + ).run(as_dict=True)[0] + if company: bin_details["company_total_stock"] = get_company_total_stock(item_code, company) + return bin_details @@ -1302,7 +1337,7 @@ def apply_price_list_on_item(args): 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)) + item_details.update(get_pricing_rule_for_item(args)) return item_details diff --git a/erpnext/stock/landed_taxes_and_charges_common.js b/erpnext/stock/landed_taxes_and_charges_common.js index ff8a69fb03..1d76a3d95a 100644 --- a/erpnext/stock/landed_taxes_and_charges_common.js +++ b/erpnext/stock/landed_taxes_and_charges_common.js @@ -1,4 +1,4 @@ -let document_list = ['Landed Cost Voucher', 'Stock Entry']; +let document_list = ['Landed Cost Voucher', 'Stock Entry', 'Subcontracting Order', 'Subcontracting Receipt']; document_list.forEach((doctype) => { frappe.ui.form.on(doctype, { diff --git a/erpnext/stock/report/batch_item_expiry_status/batch_item_expiry_status.py b/erpnext/stock/report/batch_item_expiry_status/batch_item_expiry_status.py index 3d9b046197..ef7d6e6816 100644 --- a/erpnext/stock/report/batch_item_expiry_status/batch_item_expiry_status.py +++ b/erpnext/stock/report/batch_item_expiry_status/batch_item_expiry_status.py @@ -4,6 +4,7 @@ import frappe from frappe import _ +from frappe.query_builder.functions import IfNull from frappe.utils import cint, getdate @@ -54,31 +55,28 @@ def get_columns(filters): return columns -def get_conditions(filters): - conditions = "" +def get_stock_ledger_entries(filters): if not filters.get("from_date"): frappe.throw(_("'From Date' is required")) - if filters.get("to_date"): - conditions += " and posting_date <= '%s'" % filters["to_date"] - else: + if not filters.get("to_date"): frappe.throw(_("'To Date' is required")) - return conditions - - -def get_stock_ledger_entries(filters): - conditions = get_conditions(filters) - return frappe.db.sql( - """select item_code, batch_no, warehouse, - posting_date, actual_qty - from `tabStock Ledger Entry` - where is_cancelled = 0 - and docstatus < 2 and ifnull(batch_no, '') != '' %s order by item_code, warehouse""" - % conditions, - as_dict=1, + sle = frappe.qb.DocType("Stock Ledger Entry") + query = ( + frappe.qb.from_(sle) + .select(sle.item_code, sle.batch_no, sle.warehouse, sle.posting_date, sle.actual_qty) + .where( + (sle.is_cancelled == 0) + & (sle.docstatus < 2) + & (IfNull(sle.batch_no, "") != "") + & (sle.posting_date <= filters["to_date"]) + ) + .orderby(sle.item_code, sle.warehouse) ) + return query.run(as_dict=True) + def get_item_warehouse_batch_map(filters, float_precision): sle = get_stock_ledger_entries(filters) @@ -112,7 +110,7 @@ def get_item_warehouse_batch_map(filters, float_precision): def get_item_details(filters): item_map = {} - for d in frappe.db.sql("select name, item_name, description from tabItem", as_dict=1): + for d in (frappe.qb.from_("Item").select("name", "item_name", "description")).run(as_dict=True): item_map.setdefault(d.name, d) return item_map diff --git a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py index 8a13300dc8..0d57938e31 100644 --- a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py +++ b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py @@ -5,6 +5,9 @@ import frappe from frappe import _ from frappe.utils import cint, flt, getdate +from pypika import functions as fn + +from erpnext.stock.doctype.warehouse.warehouse import apply_warehouse_filter def execute(filters=None): @@ -64,37 +67,40 @@ def get_columns(filters): return columns -def get_conditions(filters): - conditions = "" - if not filters.get("from_date"): - frappe.throw(_("'From Date' is required")) - - if filters.get("to_date"): - conditions += " and posting_date <= '%s'" % filters["to_date"] - else: - frappe.throw(_("'To Date' is required")) - - for field in ["item_code", "warehouse", "batch_no", "company"]: - if filters.get(field): - conditions += " and {0} = {1}".format(field, frappe.db.escape(filters.get(field))) - - return conditions - - # get all details def get_stock_ledger_entries(filters): - conditions = get_conditions(filters) - return frappe.db.sql( - """ - select item_code, batch_no, warehouse, posting_date, sum(actual_qty) as actual_qty - from `tabStock Ledger Entry` - where is_cancelled = 0 and docstatus < 2 and ifnull(batch_no, '') != '' %s - group by voucher_no, batch_no, item_code, warehouse - order by item_code, warehouse""" - % conditions, - as_dict=1, + if not filters.get("from_date"): + frappe.throw(_("'From Date' is required")) + if not filters.get("to_date"): + frappe.throw(_("'To Date' is required")) + + sle = frappe.qb.DocType("Stock Ledger Entry") + query = ( + frappe.qb.from_(sle) + .select( + sle.item_code, + sle.warehouse, + sle.batch_no, + sle.posting_date, + fn.Sum(sle.actual_qty).as_("actual_qty"), + ) + .where( + (sle.docstatus < 2) + & (sle.is_cancelled == 0) + & (fn.IfNull(sle.batch_no, "") != "") + & (sle.posting_date <= filters["to_date"]) + ) + .groupby(sle.voucher_no, sle.batch_no, sle.item_code, sle.warehouse) + .orderby(sle.item_code, sle.warehouse) ) + query = apply_warehouse_filter(query, sle, filters) + for field in ["item_code", "batch_no", "company"]: + if filters.get(field): + query = query.where(sle[field] == filters.get(field)) + + return query.run(as_dict=True) + def get_item_warehouse_batch_map(filters, float_precision): sle = get_stock_ledger_entries(filters) @@ -127,7 +133,9 @@ def get_item_warehouse_batch_map(filters, float_precision): def get_item_details(filters): item_map = {} - for d in frappe.db.sql("select name, item_name, description, stock_uom from tabItem", as_dict=1): + for d in (frappe.qb.from_("Item").select("name", "item_name", "description", "stock_uom")).run( + as_dict=1 + ): item_map.setdefault(d.name, d) return item_map diff --git a/erpnext/stock/report/delayed_item_report/delayed_item_report.py b/erpnext/stock/report/delayed_item_report/delayed_item_report.py index 9df24d6559..546a834da8 100644 --- a/erpnext/stock/report/delayed_item_report/delayed_item_report.py +++ b/erpnext/stock/report/delayed_item_report/delayed_item_report.py @@ -21,56 +21,54 @@ class DelayedItemReport(object): return self.get_columns(), self.get_data() or [] def get_data(self, consolidated=False): - conditions = "" - doctype = self.filters.get("based_on") - child_doc = "%s Item" % doctype + sales_order_field = "sales_order" if doctype == "Sales Invoice" else "against_sales_order" + + parent = frappe.qb.DocType(doctype) + child = frappe.qb.DocType(f"{doctype} Item") + + query = ( + frappe.qb.from_(child) + .from_(parent) + .select( + child.item_code, + child.item_name, + child.item_group, + child.qty, + child.rate, + child.amount, + child.so_detail, + child[sales_order_field].as_("sales_order"), + parent.shipping_address_name, + parent.po_no, + parent.customer, + parent.posting_date, + parent.name, + parent.grand_total, + ) + .where( + (child.parent == parent.name) + & (parent.docstatus == 1) + & (parent.posting_date.between(self.filters.get("from_date"), self.filters.get("to_date"))) + & (child[sales_order_field].notnull()) + & (child[sales_order_field] != "") + ) + ) if doctype == "Sales Invoice": - conditions = " and `tabSales Invoice`.update_stock = 1 and `tabSales Invoice`.is_pos = 0" + query = query.where((parent.update_stock == 1) & (parent.is_pos == 0)) if self.filters.get("item_group"): - conditions += " and `tab%s`.item_group = %s" % ( - child_doc, - frappe.db.escape(self.filters.get("item_group")), - ) - - for field in ["customer", "customer_group", "company"]: - if self.filters.get(field): - conditions += " and `tab%s`.%s = %s" % ( - doctype, - field, - frappe.db.escape(self.filters.get(field)), - ) - - sales_order_field = "against_sales_order" - if doctype == "Sales Invoice": - sales_order_field = "sales_order" + query = query.where(child.item_group == self.filters.get("item_group")) if self.filters.get("sales_order"): - conditions = " and `tab%s`.%s = '%s'" % ( - child_doc, - sales_order_field, - self.filters.get("sales_order"), - ) + query = query.where(child[sales_order_field] == self.filters.get("sales_order")) - self.transactions = frappe.db.sql( - """ SELECT `tab{child_doc}`.item_code, `tab{child_doc}`.item_name, - `tab{child_doc}`.item_group, `tab{child_doc}`.qty, `tab{child_doc}`.rate, `tab{child_doc}`.amount, - `tab{child_doc}`.so_detail, `tab{child_doc}`.{so_field} as sales_order, - `tab{doctype}`.shipping_address_name, `tab{doctype}`.po_no, `tab{doctype}`.customer, - `tab{doctype}`.posting_date, `tab{doctype}`.name, `tab{doctype}`.grand_total - FROM `tab{child_doc}`, `tab{doctype}` - WHERE - `tab{child_doc}`.parent = `tab{doctype}`.name and `tab{doctype}`.docstatus = 1 and - `tab{doctype}`.posting_date between %(from_date)s and %(to_date)s and - `tab{child_doc}`.{so_field} is not null and `tab{child_doc}`.{so_field} != '' {cond} - """.format( - cond=conditions, doctype=doctype, child_doc=child_doc, so_field=sales_order_field - ), - {"from_date": self.filters.get("from_date"), "to_date": self.filters.get("to_date")}, - as_dict=1, - ) + for field in ("customer", "customer_group", "company"): + if self.filters.get(field): + query = query.where(parent[field] == self.filters.get(field)) + + self.transactions = query.run(as_dict=True) if self.transactions: self.filter_transactions_data(consolidated) diff --git a/erpnext/stock/report/delivery_note_trends/delivery_note_trends.py b/erpnext/stock/report/delivery_note_trends/delivery_note_trends.py index 7a1b8c0cee..0ec4e1ce95 100644 --- a/erpnext/stock/report/delivery_note_trends/delivery_note_trends.py +++ b/erpnext/stock/report/delivery_note_trends/delivery_note_trends.py @@ -45,4 +45,5 @@ def get_chart_data(data, filters): "datasets": [{"name": _("Total Delivered Amount"), "values": datapoints}], }, "type": "bar", + "fieldtype": "Currency", } diff --git a/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py index 23e3c8a97f..df01b14d11 100644 --- a/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py +++ b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py @@ -4,6 +4,8 @@ import frappe from frappe import _ +from frappe.query_builder import Field +from frappe.query_builder.functions import Min, Timestamp from frappe.utils import add_days, getdate, today import erpnext @@ -28,7 +30,7 @@ def execute(filters=None): def get_unsync_date(filters): date = filters.from_date if not date: - date = frappe.db.sql(""" SELECT min(posting_date) from `tabStock Ledger Entry`""") + date = (frappe.qb.from_("Stock Ledger Entry").select(Min(Field("posting_date")))).run() date = date[0][0] if not date: @@ -54,22 +56,27 @@ def get_data(report_filters): result = [] voucher_wise_dict = {} - data = frappe.db.sql( - """ - SELECT - name, posting_date, posting_time, voucher_type, voucher_no, - stock_value_difference, stock_value, warehouse, item_code - FROM - `tabStock Ledger Entry` - WHERE - posting_date - = %s and company = %s - and is_cancelled = 0 - ORDER BY timestamp(posting_date, posting_time) asc, creation asc - """, - (from_date, report_filters.company), - as_dict=1, - ) + sle = frappe.qb.DocType("Stock Ledger Entry") + data = ( + frappe.qb.from_(sle) + .select( + sle.name, + sle.posting_date, + sle.posting_time, + sle.voucher_type, + sle.voucher_no, + sle.stock_value_difference, + sle.stock_value, + sle.warehouse, + sle.item_code, + ) + .where( + (sle.posting_date == from_date) + & (sle.company == report_filters.company) + & (sle.is_cancelled == 0) + ) + .orderby(Timestamp(sle.posting_date, sle.posting_time), sle.creation) + ).run(as_dict=True) for d in data: voucher_wise_dict.setdefault((d.item_code, d.warehouse), []).append(d) diff --git a/erpnext/stock/report/item_price_stock/item_price_stock.py b/erpnext/stock/report/item_price_stock/item_price_stock.py index 15218e63a8..1b07f596c7 100644 --- a/erpnext/stock/report/item_price_stock/item_price_stock.py +++ b/erpnext/stock/report/item_price_stock/item_price_stock.py @@ -62,22 +62,28 @@ def get_data(filters, columns): def get_item_price_qty_data(filters): - conditions = "" - if filters.get("item_code"): - conditions += "where a.item_code=%(item_code)s" + item_price = frappe.qb.DocType("Item Price") + bin = frappe.qb.DocType("Bin") - item_results = frappe.db.sql( - """select a.item_code, a.item_name, a.name as price_list_name, - a.brand as brand, b.warehouse as warehouse, b.actual_qty as actual_qty - from `tabItem Price` a left join `tabBin` b - ON a.item_code = b.item_code - {conditions}""".format( - conditions=conditions - ), - filters, - as_dict=1, + query = ( + frappe.qb.from_(item_price) + .left_join(bin) + .on(item_price.item_code == bin.item_code) + .select( + item_price.item_code, + item_price.item_name, + item_price.name.as_("price_list_name"), + item_price.brand.as_("brand"), + bin.warehouse.as_("warehouse"), + bin.actual_qty.as_("actual_qty"), + ) ) + if filters.get("item_code"): + query = query.where(item_price.item_code == filters.get("item_code")) + + item_results = query.run(as_dict=True) + price_list_names = list(set(item.price_list_name for item in item_results)) buying_price_map = get_price_map(price_list_names, buying=1) diff --git a/erpnext/stock/report/item_prices/item_prices.py b/erpnext/stock/report/item_prices/item_prices.py index 87f1a42e2b..ab47b4a8b9 100644 --- a/erpnext/stock/report/item_prices/item_prices.py +++ b/erpnext/stock/report/item_prices/item_prices.py @@ -4,6 +4,7 @@ import frappe from frappe import _ +from frappe.query_builder.functions import IfNull, Sum from frappe.utils import flt @@ -12,8 +13,7 @@ def execute(filters=None): filters = {} columns = get_columns(filters) - conditions = get_condition(filters) - item_map = get_item_details(conditions) + item_map = get_item_details(filters) pl = get_price_list() last_purchase_rate = get_last_purchase_rate() bom_rate = get_item_bom_rate() @@ -63,18 +63,24 @@ def get_columns(filters): return columns -def get_item_details(conditions): +def get_item_details(filters): """returns all items details""" item_map = {} - for i in frappe.db.sql( - """select name, item_group, item_name, description, - brand, stock_uom from tabItem %s - order by item_code, item_group""" - % (conditions), - as_dict=1, - ): + item = frappe.qb.DocType("Item") + query = ( + frappe.qb.from_(item) + .select(item.name, item.item_group, item.item_name, item.description, item.brand, item.stock_uom) + .orderby(item.item_code, item.item_group) + ) + + if filters.get("items") == "Enabled Items only": + query = query.where(item.disabled == 0) + elif filters.get("items") == "Disabled Items only": + query = query.where(item.disabled == 1) + + for i in query.run(as_dict=True): item_map.setdefault(i.name, i) return item_map @@ -85,19 +91,38 @@ def get_price_list(): rate = {} - price_list = frappe.db.sql( - """select ip.item_code, ip.buying, ip.selling, - concat(ifnull(cu.symbol,ip.currency), " ", round(ip.price_list_rate,2), " - ", ip.price_list) as price - from `tabItem Price` ip, `tabPrice List` pl, `tabCurrency` cu - where ip.price_list=pl.name and pl.currency=cu.name and pl.enabled=1""", - as_dict=1, - ) + ip = frappe.qb.DocType("Item Price") + pl = frappe.qb.DocType("Price List") + cu = frappe.qb.DocType("Currency") - for j in price_list: - if j.price: - rate.setdefault(j.item_code, {}).setdefault("Buying" if j.buying else "Selling", []).append( - j.price + price_list = ( + frappe.qb.from_(ip) + .from_(pl) + .from_(cu) + .select( + ip.item_code, + ip.buying, + ip.selling, + (IfNull(cu.symbol, ip.currency)).as_("currency"), + ip.price_list_rate, + ip.price_list, + ) + .where((ip.price_list == pl.name) & (pl.currency == cu.name) & (pl.enabled == 1)) + ).run(as_dict=True) + + for d in price_list: + d.update( + {"price": "{0} {1} - {2}".format(d.currency, round(d.price_list_rate, 2), d.price_list)} + ) + d.pop("currency") + d.pop("price_list_rate") + d.pop("price_list") + + if d.price: + rate.setdefault(d.item_code, {}).setdefault("Buying" if d.buying else "Selling", []).append( + d.price ) + item_rate_map = {} for item in rate: @@ -112,30 +137,39 @@ def get_price_list(): def get_last_purchase_rate(): item_last_purchase_rate_map = {} - query = """select * from ( - (select - po_item.item_code, - po.transaction_date as posting_date, - po_item.base_rate - from `tabPurchase Order` po, `tabPurchase Order Item` po_item - where po.name = po_item.parent and po.docstatus = 1) - union - (select - pr_item.item_code, - pr.posting_date, - pr_item.base_rate - from `tabPurchase Receipt` pr, `tabPurchase Receipt Item` pr_item - where pr.name = pr_item.parent and pr.docstatus = 1) - union - (select - pi_item.item_code, - pi.posting_date, - pi_item.base_rate - from `tabPurchase Invoice` pi, `tabPurchase Invoice Item` pi_item - where pi.name = pi_item.parent and pi.docstatus = 1 and pi.update_stock = 1) - ) result order by result.item_code asc, result.posting_date asc""" + po = frappe.qb.DocType("Purchase Order") + pr = frappe.qb.DocType("Purchase Receipt") + pi = frappe.qb.DocType("Purchase Invoice") + po_item = frappe.qb.DocType("Purchase Order Item") + pr_item = frappe.qb.DocType("Purchase Receipt Item") + pi_item = frappe.qb.DocType("Purchase Invoice Item") - for d in frappe.db.sql(query, as_dict=1): + query = ( + frappe.qb.from_( + ( + frappe.qb.from_(po) + .from_(po_item) + .select(po_item.item_code, po.transaction_date.as_("posting_date"), po_item.base_rate) + .where((po.name == po_item.parent) & (po.docstatus == 1)) + ) + + ( + frappe.qb.from_(pr) + .from_(pr_item) + .select(pr_item.item_code, pr.posting_date, pr_item.base_rate) + .where((pr.name == pr_item.parent) & (pr.docstatus == 1)) + ) + + ( + frappe.qb.from_(pi) + .from_(pi_item) + .select(pi_item.item_code, pi.posting_date, pi_item.base_rate) + .where((pi.name == pi_item.parent) & (pi.docstatus == 1) & (pi.update_stock == 1)) + ) + ) + .select("*") + .orderby("item_code", "posting_date") + ) + + for d in query.run(as_dict=True): item_last_purchase_rate_map[d.item_code] = d.base_rate return item_last_purchase_rate_map @@ -146,12 +180,15 @@ def get_item_bom_rate(): item_bom_map = {} - for b in frappe.db.sql( - """select item, (total_cost/quantity) as bom_rate - from `tabBOM` where is_active=1 and is_default=1""", - as_dict=1, - ): - item_bom_map.setdefault(b.item, flt(b.bom_rate)) + bom = frappe.qb.DocType("BOM") + bom_data = ( + frappe.qb.from_(bom) + .select(bom.item, (bom.total_cost / bom.quantity).as_("bom_rate")) + .where((bom.is_active == 1) & (bom.is_default == 1)) + ).run(as_dict=True) + + for d in bom_data: + item_bom_map.setdefault(d.item, flt(d.bom_rate)) return item_bom_map @@ -161,25 +198,17 @@ def get_valuation_rate(): item_val_rate_map = {} - for d in frappe.db.sql( - """select item_code, - sum(actual_qty*valuation_rate)/sum(actual_qty) as val_rate - from tabBin where actual_qty > 0 group by item_code""", - as_dict=1, - ): + bin = frappe.qb.DocType("Bin") + bin_data = ( + frappe.qb.from_(bin) + .select( + bin.item_code, Sum(bin.actual_qty * bin.valuation_rate) / Sum(bin.actual_qty).as_("val_rate") + ) + .where(bin.actual_qty > 0) + .groupby(bin.item_code) + ).run(as_dict=True) + + for d in bin_data: item_val_rate_map.setdefault(d.item_code, d.val_rate) return item_val_rate_map - - -def get_condition(filters): - """Get Filter Items""" - - if filters.get("items") == "Enabled Items only": - conditions = " where disabled=0 " - elif filters.get("items") == "Disabled Items only": - conditions = " where disabled=1 " - else: - conditions = "" - - return conditions diff --git a/erpnext/stock/report/item_shortage_report/item_shortage_report.py b/erpnext/stock/report/item_shortage_report/item_shortage_report.py index 03a3a6a0b8..9fafe91c3f 100644 --- a/erpnext/stock/report/item_shortage_report/item_shortage_report.py +++ b/erpnext/stock/report/item_shortage_report/item_shortage_report.py @@ -8,8 +8,7 @@ from frappe import _ def execute(filters=None): columns = get_columns() - conditions = get_conditions(filters) - data = get_data(conditions, filters) + data = get_data(filters) if not data: return [], [], None, [] @@ -19,49 +18,39 @@ def execute(filters=None): return columns, data, None, chart_data -def get_conditions(filters): - conditions = "" +def get_data(filters): + bin = frappe.qb.DocType("Bin") + wh = frappe.qb.DocType("Warehouse") + item = frappe.qb.DocType("Item") - if filters.get("warehouse"): - conditions += "AND warehouse in %(warehouse)s" - if filters.get("company"): - conditions += "AND company = %(company)s" - - return conditions - - -def get_data(conditions, filters): - data = frappe.db.sql( - """ - SELECT + query = ( + frappe.qb.from_(bin) + .from_(wh) + .from_(item) + .select( bin.warehouse, bin.item_code, - bin.actual_qty , - bin.ordered_qty , - bin.planned_qty , - bin.reserved_qty , + bin.actual_qty, + bin.ordered_qty, + bin.planned_qty, + bin.reserved_qty, bin.reserved_qty_for_production, - bin.projected_qty , - warehouse.company, - item.item_name , - item.description - FROM - `tabBin` bin, - `tabWarehouse` warehouse, - `tabItem` item - WHERE - bin.projected_qty<0 - AND warehouse.name = bin.warehouse - AND bin.item_code=item.name - {0} - ORDER BY bin.projected_qty;""".format( - conditions - ), - filters, - as_dict=1, + bin.projected_qty, + wh.company, + item.item_name, + item.description, + ) + .where((bin.projected_qty < 0) & (wh.name == bin.warehouse) & (bin.item_code == item.name)) + .orderby(bin.projected_qty) ) - return data + if filters.get("warehouse"): + query = query.where(bin.warehouse.isin(filters.get("warehouse"))) + + if filters.get("company"): + query = query.where(wh.company == filters.get("company")) + + return query.run(as_dict=True) def get_chart_data(data): diff --git a/erpnext/stock/report/item_shortage_report/test_item_shortage_report.py b/erpnext/stock/report/item_shortage_report/test_item_shortage_report.py new file mode 100644 index 0000000000..5884c32acc --- /dev/null +++ b/erpnext/stock/report/item_shortage_report/test_item_shortage_report.py @@ -0,0 +1,51 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe.tests.utils import FrappeTestCase + +from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.stock.report.item_shortage_report.item_shortage_report import ( + execute as item_shortage_report, +) + + +class TestItemShortageReport(FrappeTestCase): + def test_item_shortage_report(self): + item = make_item().name + so = make_sales_order(item_code=item) + + reserved_qty, projected_qty = frappe.db.get_value( + "Bin", + { + "item_code": item, + "warehouse": so.items[0].warehouse, + }, + ["reserved_qty", "projected_qty"], + ) + self.assertEqual(reserved_qty, so.items[0].qty) + self.assertEqual(projected_qty, -(so.items[0].qty)) + + filters = { + "company": so.company, + } + report_data = item_shortage_report(filters)[1] + item_code_list = [row.get("item_code") for row in report_data] + self.assertIn(item, item_code_list) + + filters = { + "company": so.company, + "warehouse": [so.items[0].warehouse], + } + report_data = item_shortage_report(filters)[1] + item_code_list = [row.get("item_code") for row in report_data] + self.assertIn(item, item_code_list) + + filters = { + "company": so.company, + "warehouse": ["Work In Progress - _TC"], + } + report_data = item_shortage_report(filters)[1] + item_code_list = [row.get("item_code") for row in report_data] + self.assertNotIn(item, item_code_list) 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 f308e9e41f..c4358b809f 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 @@ -3,6 +3,7 @@ import frappe from frappe import _ +from frappe.query_builder.functions import Abs, Sum from frappe.utils import flt, getdate @@ -11,8 +12,6 @@ def execute(filters=None): filters = {} float_precision = frappe.db.get_default("float_precision") - condition = get_condition(filters) - avg_daily_outgoing = 0 diff = ((getdate(filters.get("to_date")) - getdate(filters.get("from_date"))).days) + 1 if diff <= 0: @@ -20,8 +19,8 @@ def execute(filters=None): columns = get_columns() items = get_item_info(filters) - consumed_item_map = get_consumed_items(condition) - delivered_item_map = get_delivered_items(condition) + consumed_item_map = get_consumed_items(filters) + delivered_item_map = get_delivered_items(filters) data = [] for item in items: @@ -71,76 +70,86 @@ def get_columns(): def get_item_info(filters): from erpnext.stock.report.stock_ledger.stock_ledger import get_item_group_condition - 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 {}""".format( - " and ".join(conditions) - ), - filters, - as_dict=1, + item = frappe.qb.DocType("Item") + query = ( + frappe.qb.from_(item) + .select( + item.name, + item.item_name, + item.description, + item.brand, + item.item_group, + item.safety_stock, + item.lead_time_days, + ) + .where((item.is_stock_item == 1) & (item.disabled == 0)) ) + if brand := filters.get("brand"): + query = query.where(item.brand == brand) -def get_consumed_items(condition): + if conditions := get_item_group_condition(filters.get("item_group"), item): + query = query.where(conditions) + + return query.run(as_dict=True) + + +def get_consumed_items(filters): purpose_to_exclude = [ "Material Transfer for Manufacture", "Material Transfer", "Send to Subcontractor", ] - condition += """ - and ( - purpose is NULL - or purpose not in ({}) + se = frappe.qb.DocType("Stock Entry") + sle = frappe.qb.DocType("Stock Ledger Entry") + query = ( + frappe.qb.from_(sle) + .left_join(se) + .on(sle.voucher_no == se.name) + .select(sle.item_code, Abs(Sum(sle.actual_qty)).as_("consumed_qty")) + .where( + (sle.actual_qty < 0) + & (sle.is_cancelled == 0) + & (sle.voucher_type.notin(["Delivery Note", "Sales Invoice"])) + & ((se.purpose.isnull()) | (se.purpose.notin(purpose_to_exclude))) ) - """.format( - ", ".join(f"'{p}'" for p in purpose_to_exclude) + .groupby(sle.item_code) ) - condition = condition.replace("posting_date", "sle.posting_date") + query = get_filtered_query(filters, sle, query) - consumed_items = frappe.db.sql( - """ - select item_code, abs(sum(actual_qty)) as consumed_qty - from `tabStock Ledger Entry` as sle left join `tabStock Entry` as se - on sle.voucher_no = se.name - where - actual_qty < 0 - and is_cancelled = 0 - and voucher_type not in ('Delivery Note', 'Sales Invoice') - %s - group by item_code""" - % condition, - as_dict=1, - ) + consumed_items = query.run(as_dict=True) consumed_items_map = {item.item_code: item.consumed_qty for item in consumed_items} return consumed_items_map -def get_delivered_items(condition): - dn_items = frappe.db.sql( - """select dn_item.item_code, sum(dn_item.stock_qty) as dn_qty - from `tabDelivery Note` dn, `tabDelivery Note Item` dn_item - where dn.name = dn_item.parent and dn.docstatus = 1 %s - group by dn_item.item_code""" - % (condition), - as_dict=1, +def get_delivered_items(filters): + parent = frappe.qb.DocType("Delivery Note") + child = frappe.qb.DocType("Delivery Note Item") + query = ( + frappe.qb.from_(parent) + .from_(child) + .select(child.item_code, Sum(child.stock_qty).as_("dn_qty")) + .where((parent.name == child.parent) & (parent.docstatus == 1)) + .groupby(child.item_code) ) + query = get_filtered_query(filters, parent, query) - si_items = frappe.db.sql( - """select si_item.item_code, sum(si_item.stock_qty) as si_qty - from `tabSales Invoice` si, `tabSales Invoice Item` si_item - where si.name = si_item.parent and si.docstatus = 1 and - si.update_stock = 1 %s - group by si_item.item_code""" - % (condition), - as_dict=1, + dn_items = query.run(as_dict=True) + + parent = frappe.qb.DocType("Sales Invoice") + child = frappe.qb.DocType("Sales Invoice Item") + query = ( + frappe.qb.from_(parent) + .from_(child) + .select(child.item_code, Sum(child.stock_qty).as_("si_qty")) + .where((parent.name == child.parent) & (parent.docstatus == 1) & (parent.update_stock == 1)) + .groupby(child.item_code) ) + query = get_filtered_query(filters, parent, query) + + si_items = query.run(as_dict=True) dn_item_map = {} for item in dn_items: @@ -152,13 +161,10 @@ def get_delivered_items(condition): return dn_item_map -def get_condition(filters): - conditions = "" +def get_filtered_query(filters, table, query): if filters.get("from_date") and filters.get("to_date"): - conditions += " and posting_date between '%s' and '%s'" % ( - filters["from_date"], - filters["to_date"], - ) + query = query.where(table.posting_date.between(filters["from_date"], filters["to_date"])) else: - frappe.throw(_("From and To dates required")) - return conditions + frappe.throw(_("From and To dates are required")) + + return query diff --git a/erpnext/stock/report/product_bundle_balance/product_bundle_balance.py b/erpnext/stock/report/product_bundle_balance/product_bundle_balance.py index 854875a053..9e75201bd1 100644 --- a/erpnext/stock/report/product_bundle_balance/product_bundle_balance.py +++ b/erpnext/stock/report/product_bundle_balance/product_bundle_balance.py @@ -4,7 +4,9 @@ import frappe from frappe import _ +from frappe.query_builder.functions import IfNull from frappe.utils import flt +from pypika.terms import ExistsCriterion from erpnext.stock.report.stock_ledger.stock_ledger import get_item_group_condition @@ -123,43 +125,65 @@ def get_items(filters): pb_details = frappe._dict() item_details = frappe._dict() - conditions = get_parent_item_conditions(filters) - parent_item_details = frappe.db.sql( - """ - select item.name as item_code, item.item_name, pb.description, item.item_group, item.brand, item.stock_uom - from `tabItem` item - inner join `tabProduct Bundle` pb on pb.new_item_code = item.name - where ifnull(item.disabled, 0) = 0 {0} - """.format( - conditions - ), - filters, - as_dict=1, - ) # nosec + item = frappe.qb.DocType("Item") + pb = frappe.qb.DocType("Product Bundle") + + query = ( + frappe.qb.from_(item) + .inner_join(pb) + .on(pb.new_item_code == item.name) + .select( + item.name.as_("item_code"), + item.item_name, + pb.description, + item.item_group, + item.brand, + item.stock_uom, + ) + .where(IfNull(item.disabled, 0) == 0) + ) + + if item_code := filters.get("item_code"): + query = query.where(item.item_code == item_code) + else: + if brand := filters.get("brand"): + query = query.where(item.brand == brand) + if item_group := filters.get("item_group"): + if conditions := get_item_group_condition(item_group, item): + query = query.where(conditions) + + parent_item_details = query.run(as_dict=True) parent_items = [] for d in parent_item_details: parent_items.append(d.item_code) item_details[d.item_code] = d + child_item_details = [] if parent_items: - child_item_details = frappe.db.sql( - """ - select - pb.new_item_code as parent_item, pbi.item_code, item.item_name, pbi.description, item.item_group, item.brand, - item.stock_uom, pbi.uom, pbi.qty - from `tabProduct Bundle Item` pbi - inner join `tabProduct Bundle` pb on pb.name = pbi.parent - inner join `tabItem` item on item.name = pbi.item_code - where pb.new_item_code in ({0}) - """.format( - ", ".join(["%s"] * len(parent_items)) - ), - parent_items, - as_dict=1, - ) # nosec - else: - child_item_details = [] + item = frappe.qb.DocType("Item") + pb = frappe.qb.DocType("Product Bundle") + pbi = frappe.qb.DocType("Product Bundle Item") + + child_item_details = ( + frappe.qb.from_(pbi) + .inner_join(pb) + .on(pb.name == pbi.parent) + .inner_join(item) + .on(item.name == pbi.item_code) + .select( + pb.new_item_code.as_("parent_item"), + pbi.item_code, + item.item_name, + pbi.description, + item.item_group, + item.brand, + item.stock_uom, + pbi.uom, + pbi.qty, + ) + .where(pb.new_item_code.isin(parent_items)) + ).run(as_dict=1) child_items = set() for d in child_item_details: @@ -184,58 +208,42 @@ def get_stock_ledger_entries(filters, items): if not items: return [] - item_conditions_sql = " and sle.item_code in ({})".format( - ", ".join(frappe.db.escape(i) for i in items) + sle = frappe.qb.DocType("Stock Ledger Entry") + sle2 = frappe.qb.DocType("Stock Ledger Entry") + + query = ( + frappe.qb.from_(sle) + .force_index("posting_sort_index") + .left_join(sle2) + .on( + (sle.item_code == sle2.item_code) + & (sle.warehouse == sle2.warehouse) + & (sle.posting_date < sle2.posting_date) + & (sle.posting_time < sle2.posting_time) + & (sle.name < sle2.name) + ) + .select(sle.item_code, sle.warehouse, sle.qty_after_transaction, sle.company) + .where((sle2.name.isnull()) & (sle.docstatus < 2) & (sle.item_code.isin(items))) ) - conditions = get_sle_conditions(filters) - - return frappe.db.sql( - """ - select - sle.item_code, sle.warehouse, sle.qty_after_transaction, sle.company - from - `tabStock Ledger Entry` sle force index (posting_sort_index) - left join `tabStock Ledger Entry` sle2 on - sle.item_code = sle2.item_code and sle.warehouse = sle2.warehouse - and (sle.posting_date, sle.posting_time, sle.name) < (sle2.posting_date, sle2.posting_time, sle2.name) - where sle2.name is null and sle.docstatus < 2 %s %s""" - % (item_conditions_sql, conditions), - as_dict=1, - ) # nosec - - -def get_parent_item_conditions(filters): - conditions = [] - - if filters.get("item_code"): - conditions.append("item.item_code = %(item_code)s") + if date := filters.get("date"): + query = query.where(sle.posting_date <= date) else: - if filters.get("brand"): - conditions.append("item.brand=%(brand)s") - if filters.get("item_group"): - conditions.append(get_item_group_condition(filters.get("item_group"))) - - conditions = " and ".join(conditions) - return "and {0}".format(conditions) if conditions else "" - - -def get_sle_conditions(filters): - conditions = "" - if not filters.get("date"): frappe.throw(_("'Date' is required")) - conditions += " and sle.posting_date <= %s" % frappe.db.escape(filters.get("date")) - if filters.get("warehouse"): warehouse_details = frappe.db.get_value( "Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1 ) - if warehouse_details: - conditions += ( - " and exists (select name from `tabWarehouse` wh \ - where wh.lft >= %s and wh.rgt <= %s and sle.warehouse = wh.name)" - % (warehouse_details.lft, warehouse_details.rgt) - ) # nosec - return conditions + if warehouse_details: + wh = frappe.qb.DocType("Warehouse") + query = query.where( + ExistsCriterion( + frappe.qb.from_(wh) + .select(wh.name) + .where((wh.lft >= warehouse_details.lft) & (wh.rgt <= warehouse_details.rgt)) + ) + ) + + return query.run(as_dict=True) diff --git a/erpnext/stock/report/purchase_receipt_trends/purchase_receipt_trends.py b/erpnext/stock/report/purchase_receipt_trends/purchase_receipt_trends.py index fe2d55a391..b62a6ee6fd 100644 --- a/erpnext/stock/report/purchase_receipt_trends/purchase_receipt_trends.py +++ b/erpnext/stock/report/purchase_receipt_trends/purchase_receipt_trends.py @@ -46,4 +46,5 @@ def get_chart_data(data, filters): }, "type": "bar", "colors": ["#5e64ff"], + "fieldtype": "Currency", } diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py index 7c430e491a..2fa97ae354 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/stock_ageing.py @@ -198,11 +198,11 @@ def setup_ageing_columns(filters: Filters, range_columns: List): 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')}", + _("{0} - Above").format(cint(filters["range3"]) + 1), ] for i, label in enumerate(ranges): fieldname = "range" + str(i + 1) - add_column(range_columns, label=f"Age ({label})", fieldname=fieldname) + add_column(range_columns, label=_("Age ({0})").format(label), fieldname=fieldname) def add_column( diff --git a/erpnext/stock/report/stock_analytics/stock_analytics.py b/erpnext/stock/report/stock_analytics/stock_analytics.py index 89ca9d9126..27b94ab3f9 100644 --- a/erpnext/stock/report/stock_analytics/stock_analytics.py +++ b/erpnext/stock/report/stock_analytics/stock_analytics.py @@ -114,11 +114,13 @@ def get_period(posting_date, filters): months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] if filters.range == "Weekly": - period = "Week " + str(posting_date.isocalendar()[1]) + " " + str(posting_date.year) + period = _("Week {0} {1}").format(str(posting_date.isocalendar()[1]), str(posting_date.year)) elif filters.range == "Monthly": - period = str(months[posting_date.month - 1]) + " " + str(posting_date.year) + period = _(str(months[posting_date.month - 1])) + " " + str(posting_date.year) elif filters.range == "Quarterly": - period = "Quarter " + str(((posting_date.month - 1) // 3) + 1) + " " + str(posting_date.year) + period = _("Quarter {0} {1}").format( + str(((posting_date.month - 1) // 3) + 1), str(posting_date.year) + ) else: year = get_fiscal_year(posting_date, company=filters.company) period = str(year[2]) diff --git a/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py b/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py index 99f820ecac..106e877c4c 100644 --- a/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py +++ b/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py @@ -41,7 +41,7 @@ def get_data(report_filters): key = (d.voucher_type, d.voucher_no) gl_data = voucher_wise_gl_data.get(key) or {} d.account_value = gl_data.get("account_value", 0) - d.difference_value = abs(d.stock_value - d.account_value) + d.difference_value = d.stock_value - d.account_value if abs(d.difference_value) > 0.1: data.append(d) diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index 679d234c9f..0fc642ef20 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -10,10 +10,10 @@ from frappe import _ from frappe.query_builder.functions import CombineDatetime from frappe.utils import cint, date_diff, flt, getdate from frappe.utils.nestedset import get_descendants_of -from pypika.terms import ExistsCriterion import erpnext from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions +from erpnext.stock.doctype.warehouse.warehouse import apply_warehouse_filter from erpnext.stock.report.stock_ageing.stock_ageing import FIFOSlots, get_average_age from erpnext.stock.utils import add_additional_uom_columns, is_reposting_item_valuation_in_progress @@ -270,18 +270,8 @@ def apply_conditions(query, filters): if company := filters.get("company"): query = query.where(sle.company == company) - if warehouse := filters.get("warehouse"): - lft, rgt = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"]) - chilren_subquery = ( - frappe.qb.from_(warehouse_table) - .select(warehouse_table.name) - .where( - (warehouse_table.lft >= lft) - & (warehouse_table.rgt <= rgt) - & (warehouse_table.name == sle.warehouse) - ) - ) - query = query.where(ExistsCriterion(chilren_subquery)) + if filters.get("warehouse"): + query = apply_warehouse_filter(query, sle, filters) elif warehouse_type := filters.get("warehouse_type"): query = ( query.join(warehouse_table) diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py index e18d4c7522..da17cdeb5a 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.py +++ b/erpnext/stock/report/stock_ledger/stock_ledger.py @@ -6,11 +6,11 @@ import frappe from frappe import _ from frappe.query_builder.functions import CombineDatetime from frappe.utils import cint, flt -from pypika.terms import ExistsCriterion from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import get_stock_balance_for +from erpnext.stock.doctype.warehouse.warehouse import apply_warehouse_filter from erpnext.stock.utils import ( is_reposting_item_valuation_in_progress, update_included_uom_in_report, @@ -58,6 +58,12 @@ def execute(filters=None): if sle.serial_no: update_available_serial_nos(available_serial_nos, sle) + if sle.actual_qty: + sle["in_out_rate"] = flt(sle.stock_value_difference / sle.actual_qty, precision) + + elif sle.voucher_type == "Stock Reconciliation": + sle["in_out_rate"] = sle.valuation_rate + data.append(sle) if include_uom: @@ -185,10 +191,18 @@ def get_columns(filters): "convertible": "rate", }, { - "label": _("Valuation Rate"), + "label": _("Avg Rate (Balance Stock)"), "fieldname": "valuation_rate", "fieldtype": "Currency", - "width": 110, + "width": 180, + "options": "Company:company:default_currency", + "convertible": "rate", + }, + { + "label": _("Valuation Rate"), + "fieldname": "in_out_rate", + "fieldtype": "Currency", + "width": 140, "options": "Company:company:default_currency", "convertible": "rate", }, @@ -292,23 +306,10 @@ def get_stock_ledger_entries(filters, items): query = query.where(sle.item_code.isin(items)) for field in ["voucher_no", "batch_no", "project", "company"]: - if filters.get(field): + if filters.get(field) and field not in inventory_dimension_fields: query = query.where(sle[field] == filters.get(field)) - if warehouse := filters.get("warehouse"): - lft, rgt = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"]) - - warehouse_table = frappe.qb.DocType("Warehouse") - chilren_subquery = ( - frappe.qb.from_(warehouse_table) - .select(warehouse_table.name) - .where( - (warehouse_table.lft >= lft) - & (warehouse_table.rgt <= rgt) - & (warehouse_table.name == sle.warehouse) - ) - ) - query = query.where(ExistsCriterion(chilren_subquery)) + query = apply_warehouse_filter(query, sle, filters) return query.run(as_dict=True) @@ -318,20 +319,25 @@ def get_inventory_dimension_fields(): def get_items(filters): + item = frappe.qb.DocType("Item") + query = frappe.qb.from_(item).select(item.name) conditions = [] - if filters.get("item_code"): - conditions.append("item.name=%(item_code)s") + + if item_code := filters.get("item_code"): + conditions.append(item.name == item_code) else: - if filters.get("brand"): - conditions.append("item.brand=%(brand)s") - if filters.get("item_group"): - conditions.append(get_item_group_condition(filters.get("item_group"))) + if brand := filters.get("brand"): + conditions.append(item.brand == brand) + if item_group := filters.get("item_group"): + if condition := get_item_group_condition(item_group, item): + conditions.append(condition) items = [] if conditions: - items = frappe.db.sql_list( - """select name from `tabItem` item where {}""".format(" and ".join(conditions)), filters - ) + for condition in conditions: + query = query.where(condition) + items = [r[0] for r in query.run()] + return items @@ -343,29 +349,22 @@ def get_item_details(items, sl_entries, include_uom): if not items: return item_details - cf_field = cf_join = "" + item = frappe.qb.DocType("Item") + query = ( + frappe.qb.from_(item) + .select(item.name, item.item_name, item.description, item.item_group, item.brand, item.stock_uom) + .where(item.name.isin(items)) + ) + if include_uom: - cf_field = ", ucd.conversion_factor" - cf_join = ( - "left join `tabUOM Conversion Detail` ucd on ucd.parent=item.name and ucd.uom=%s" - % frappe.db.escape(include_uom) + ucd = frappe.qb.DocType("UOM Conversion Detail") + query = ( + query.left_join(ucd) + .on((ucd.parent == item.name) & (ucd.uom == include_uom)) + .select(ucd.conversion_factor) ) - res = frappe.db.sql( - """ - select - item.name, item.item_name, item.description, item.item_group, item.brand, item.stock_uom {cf_field} - from - `tabItem` item - {cf_join} - where - item.name in ({item_codes}) - """.format( - cf_field=cf_field, cf_join=cf_join, item_codes=",".join(["%s"] * len(items)) - ), - items, - as_dict=1, - ) + res = query.run(as_dict=True) for item in res: item_details.setdefault(item.name, item) @@ -409,7 +408,7 @@ def get_opening_balance(filters, columns, sl_entries): ) # check if any SLEs are actually Opening Stock Reconciliation - for sle in sl_entries: + for sle in list(sl_entries): if ( sle.get("voucher_type") == "Stock Reconciliation" and sle.posting_date == filters.from_date @@ -440,16 +439,28 @@ def get_warehouse_condition(warehouse): return "" -def get_item_group_condition(item_group): +def get_item_group_condition(item_group, item_table=None): item_group_details = frappe.db.get_value("Item Group", item_group, ["lft", "rgt"], as_dict=1) if item_group_details: - return ( - "item.item_group in (select ig.name from `tabItem Group` ig \ - where ig.lft >= %s and ig.rgt <= %s and item.item_group = ig.name)" - % (item_group_details.lft, item_group_details.rgt) - ) - - return "" + if item_table: + ig = frappe.qb.DocType("Item Group") + return item_table.item_group.isin( + ( + frappe.qb.from_(ig) + .select(ig.name) + .where( + (ig.lft >= item_group_details.lft) + & (ig.rgt <= item_group_details.rgt) + & (item_table.item_group == ig.name) + ) + ) + ) + else: + return ( + "item.item_group in (select ig.name from `tabItem Group` ig \ + where ig.lft >= %s and ig.rgt <= %s and item.item_group = ig.name)" + % (item_group_details.lft, item_group_details.rgt) + ) def check_inventory_dimension_filters_applied(filters) -> bool: diff --git a/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py b/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py index 49e797d6a3..f477d8f08f 100644 --- a/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py +++ b/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py @@ -5,6 +5,7 @@ import frappe from frappe import _ from frappe.utils import flt, today +from pypika.terms import ExistsCriterion from erpnext.accounts.doctype.pos_invoice.pos_invoice import get_pos_reserved_qty from erpnext.stock.utils import ( @@ -218,10 +219,26 @@ def get_columns(): def get_bin_list(filters): - conditions = [] + bin = frappe.qb.DocType("Bin") + query = ( + frappe.qb.from_(bin) + .select( + bin.item_code, + bin.warehouse, + bin.actual_qty, + bin.planned_qty, + bin.indented_qty, + bin.ordered_qty, + bin.reserved_qty, + bin.reserved_qty_for_production, + bin.reserved_qty_for_sub_contract, + bin.projected_qty, + ) + .orderby(bin.item_code, bin.warehouse) + ) if filters.item_code: - conditions.append("item_code = '%s' " % filters.item_code) + query = query.where(bin.item_code == filters.item_code) if filters.warehouse: warehouse_details = frappe.db.get_value( @@ -229,21 +246,20 @@ def get_bin_list(filters): ) if warehouse_details: - conditions.append( - " exists (select name from `tabWarehouse` wh \ - where wh.lft >= %s and wh.rgt <= %s and bin.warehouse = wh.name)" - % (warehouse_details.lft, warehouse_details.rgt) + wh = frappe.qb.DocType("Warehouse") + query = query.where( + ExistsCriterion( + frappe.qb.from_(wh) + .select(wh.name) + .where( + (wh.lft >= warehouse_details.lft) + & (wh.rgt <= warehouse_details.rgt) + & (bin.warehouse == wh.name) + ) + ) ) - bin_list = frappe.db.sql( - """select item_code, warehouse, actual_qty, planned_qty, indented_qty, - ordered_qty, reserved_qty, reserved_qty_for_production, reserved_qty_for_sub_contract, projected_qty - from tabBin bin {conditions} order by item_code, warehouse - """.format( - conditions=" where " + " and ".join(conditions) if conditions else "" - ), - as_dict=1, - ) + bin_list = query.run(as_dict=True) return bin_list @@ -251,45 +267,43 @@ def get_bin_list(filters): def get_item_map(item_code, include_uom): """Optimization: get only the item doc and re_order_levels table""" - condition = "" - if item_code: - condition = "and item_code = {0}".format(frappe.db.escape(item_code, percent=False)) + bin = frappe.qb.DocType("Bin") + item = frappe.qb.DocType("Item") - cf_field = cf_join = "" - if include_uom: - cf_field = ", ucd.conversion_factor" - cf_join = ( - "left join `tabUOM Conversion Detail` ucd on ucd.parent=item.name and ucd.uom=%(include_uom)s" + query = ( + frappe.qb.from_(item) + .select(item.name, item.item_name, item.description, item.item_group, item.brand, item.stock_uom) + .where( + (item.is_stock_item == 1) + & (item.disabled == 0) + & ( + (item.end_of_life > today()) | (item.end_of_life.isnull()) | (item.end_of_life == "0000-00-00") + ) + & (ExistsCriterion(frappe.qb.from_(bin).select(bin.name).where(bin.item_code == item.name))) ) - - items = frappe.db.sql( - """ - select item.name, item.item_name, item.description, item.item_group, item.brand, item.stock_uom{cf_field} - from `tabItem` item - {cf_join} - where item.is_stock_item = 1 - and item.disabled=0 - {condition} - and (item.end_of_life > %(today)s or item.end_of_life is null or item.end_of_life='0000-00-00') - and exists (select name from `tabBin` bin where bin.item_code=item.name)""".format( - cf_field=cf_field, cf_join=cf_join, condition=condition - ), - {"today": today(), "include_uom": include_uom}, - as_dict=True, ) - condition = "" if item_code: - condition = "where parent={0}".format(frappe.db.escape(item_code, percent=False)) + query = query.where(item.item_code == item_code) + + if include_uom: + ucd = frappe.qb.DocType("UOM Conversion Detail") + query = query.left_join(ucd).on((ucd.parent == item.name) & (ucd.uom == include_uom)) + + items = query.run(as_dict=True) + + ir = frappe.qb.DocType("Item Reorder") + query = frappe.qb.from_(ir).select("*") + + if item_code: + query = query.where(ir.parent == item_code) reorder_levels = frappe._dict() - for ir in frappe.db.sql( - """select * from `tabItem Reorder` {condition}""".format(condition=condition), as_dict=1 - ): - if ir.parent not in reorder_levels: - reorder_levels[ir.parent] = [] + for d in query.run(as_dict=True): + if d.parent not in reorder_levels: + reorder_levels[d.parent] = [] - reorder_levels[ir.parent].append(ir) + reorder_levels[d.parent].append(d) item_map = frappe._dict() for item in items: diff --git a/erpnext/stock/report/supplier_wise_sales_analytics/supplier_wise_sales_analytics.py b/erpnext/stock/report/supplier_wise_sales_analytics/supplier_wise_sales_analytics.py index 5430fe6969..8c76908cc3 100644 --- a/erpnext/stock/report/supplier_wise_sales_analytics/supplier_wise_sales_analytics.py +++ b/erpnext/stock/report/supplier_wise_sales_analytics/supplier_wise_sales_analytics.py @@ -4,6 +4,7 @@ import frappe from frappe import _ +from frappe.query_builder.functions import IfNull from frappe.utils import flt @@ -70,31 +71,33 @@ def get_columns(filters): return columns -def get_conditions(filters): - conditions = "" - values = [] +def get_consumed_details(filters): + item = frappe.qb.DocType("Item") + sle = frappe.qb.DocType("Stock Ledger Entry") + + query = ( + frappe.qb.from_(sle) + .from_(item) + .select( + sle.item_code, + item.item_name, + item.description, + item.stock_uom, + sle.actual_qty, + sle.stock_value_difference, + sle.voucher_no, + sle.voucher_type, + ) + .where((sle.is_cancelled == 0) & (sle.item_code == item.name) & (sle.actual_qty < 0)) + ) if filters.get("from_date") and filters.get("to_date"): - conditions = "and sle.posting_date>=%s and sle.posting_date<=%s" - values = [filters.get("from_date"), filters.get("to_date")] + query = query.where( + (sle.posting_date >= filters.get("from_date")) & (sle.posting_date <= filters.get("to_date")) + ) - return conditions, values - - -def get_consumed_details(filters): - conditions, values = get_conditions(filters) consumed_details = {} - - for d in frappe.db.sql( - """select sle.item_code, i.item_name, i.description, - i.stock_uom, sle.actual_qty, sle.stock_value_difference, - sle.voucher_no, sle.voucher_type - from `tabStock Ledger Entry` sle, `tabItem` i - where sle.is_cancelled = 0 and sle.item_code=i.name and sle.actual_qty < 0 %s""" - % conditions, - values, - as_dict=1, - ): + for d in query.run(as_dict=True): consumed_details.setdefault(d.item_code, []).append(d) return consumed_details @@ -104,24 +107,54 @@ def get_suppliers_details(filters): item_supplier_map = {} supplier = filters.get("supplier") - for d in frappe.db.sql( - """select pr.supplier, pri.item_code from - `tabPurchase Receipt` pr, `tabPurchase Receipt Item` pri - where pr.name=pri.parent and pr.docstatus=1 and - pri.item_code=(select name from `tabItem` where - is_stock_item=1 and name=pri.item_code)""", - as_dict=1, - ): + item = frappe.qb.DocType("Item") + pr = frappe.qb.DocType("Purchase Receipt") + pr_item = frappe.qb.DocType("Purchase Receipt Item") + + query = ( + frappe.qb.from_(pr) + .from_(pr_item) + .select(pr.supplier, pr_item.item_code) + .where( + (pr.name == pr_item.parent) + & (pr.docstatus == 1) + & ( + pr_item.item_code + == ( + frappe.qb.from_(item) + .select(item.name) + .where((item.is_stock_item == 1) & (item.name == pr_item.item_code)) + ) + ) + ) + ) + + for d in query.run(as_dict=True): item_supplier_map.setdefault(d.item_code, []).append(d.supplier) - for d in frappe.db.sql( - """select pr.supplier, pri.item_code from - `tabPurchase Invoice` pr, `tabPurchase Invoice Item` pri - where pr.name=pri.parent and pr.docstatus=1 and - ifnull(pr.update_stock, 0) = 1 and pri.item_code=(select name from `tabItem` - where is_stock_item=1 and name=pri.item_code)""", - as_dict=1, - ): + pi = frappe.qb.DocType("Purchase Invoice") + pi_item = frappe.qb.DocType("Purchase Invoice Item") + + query = ( + frappe.qb.from_(pi) + .from_(pi_item) + .select(pi.supplier, pi_item.item_code) + .where( + (pi.name == pi_item.parent) + & (pi.docstatus == 1) + & (IfNull(pi.update_stock, 0) == 1) + & ( + pi_item.item_code + == ( + frappe.qb.from_(item) + .select(item.name) + .where((item.is_stock_item == 1) & (item.name == pi_item.item_code)) + ) + ) + ) + ) + + for d in query.run(as_dict=True): if d.item_code not in item_supplier_map: item_supplier_map.setdefault(d.item_code, []).append(d.supplier) @@ -138,7 +171,11 @@ def get_suppliers_details(filters): def get_material_transfer_vouchers(): - return frappe.db.sql_list( - """select name from `tabStock Entry` where - purpose='Material Transfer' and docstatus=1""" + se = frappe.qb.DocType("Stock Entry") + query = ( + frappe.qb.from_(se) + .select(se.name) + .where((se.purpose == "Material Transfer") & (se.docstatus == 1)) ) + + return [r[0] for r in query.run()] 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 21529da2a1..c3155bd1a5 100644 --- a/erpnext/stock/report/total_stock_summary/total_stock_summary.py +++ b/erpnext/stock/report/total_stock_summary/total_stock_summary.py @@ -4,60 +4,58 @@ import frappe from frappe import _ +from frappe.query_builder.functions import Sum def execute(filters=None): if not filters: filters = {} - columns = get_columns() + columns = get_columns(filters) stock = get_total_stock(filters) return columns, stock -def get_columns(): +def get_columns(filters): columns = [ - _("Company") + ":Link/Company:250", - _("Warehouse") + ":Link/Warehouse:150", _("Item") + ":Link/Item:150", _("Description") + "::300", _("Current Qty") + ":Float:100", ] + if filters.get("group_by") == "Warehouse": + columns.insert(0, _("Warehouse") + ":Link/Warehouse:150") + else: + columns.insert(0, _("Company") + ":Link/Company:250") + return columns def get_total_stock(filters): - conditions = "" - columns = "" + bin = frappe.qb.DocType("Bin") + item = frappe.qb.DocType("Item") + wh = frappe.qb.DocType("Warehouse") + + query = ( + frappe.qb.from_(bin) + .inner_join(item) + .on(bin.item_code == item.item_code) + .inner_join(wh) + .on(wh.name == bin.warehouse) + .where(bin.actual_qty != 0) + ) if filters.get("group_by") == "Warehouse": if filters.get("company"): - conditions += " AND warehouse.company = %s" % frappe.db.escape( - filters.get("company"), percent=False - ) + query = query.where(wh.company == filters.get("company")) - conditions += " GROUP BY ledger.warehouse, item.item_code" - columns += "'' as company, ledger.warehouse" + query = query.select(bin.warehouse).groupby(bin.warehouse) else: - conditions += " GROUP BY warehouse.company, item.item_code" - columns += " warehouse.company, '' as warehouse" + query = query.select(wh.company).groupby(wh.company) - return frappe.db.sql( - """ - SELECT - %s, - item.item_code, - item.description, - sum(ledger.actual_qty) as actual_qty - FROM - `tabBin` AS ledger - INNER JOIN `tabItem` AS item - ON ledger.item_code = item.item_code - INNER JOIN `tabWarehouse` warehouse - ON warehouse.name = ledger.warehouse - WHERE - ledger.actual_qty != 0 %s""" - % (columns, conditions) - ) + query = query.select( + item.item_code, item.description, Sum(bin.actual_qty).as_("actual_qty") + ).groupby(item.item_code) + + return query.run() 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 a54373f364..abbb33b2f1 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 @@ -7,6 +7,7 @@ import frappe from frappe import _ +from frappe.query_builder.functions import Count from frappe.utils import flt from erpnext.stock.report.stock_ageing.stock_ageing import FIFOSlots, get_average_age @@ -61,7 +62,7 @@ def execute(filters=None): continue total_stock_value = sum(item_value[(item, item_group)]) - row = [item, item_group, total_stock_value] + row = [item, item_map[item]["item_name"], item_group, total_stock_value] fifo_queue = item_ageing[item]["fifo_queue"] average_age = 0.00 @@ -88,17 +89,18 @@ def get_columns(filters): """return columns""" columns = [ - _("Item") + ":Link/Item:180", - _("Item Group") + "::100", - _("Value") + ":Currency:100", - _("Age") + ":Float:60", + _("Item") + ":Link/Item:150", + _("Item Name") + ":Link/Item:150", + _("Item Group") + "::120", + _("Value") + ":Currency:120", + _("Age") + ":Float:120", ] return columns def validate_filters(filters): if not (filters.get("item_code") or filters.get("warehouse")): - sle_count = flt(frappe.db.sql("""select count(name) from `tabStock Ledger Entry`""")[0][0]) + sle_count = flt(frappe.qb.from_("Stock Ledger Entry").select(Count("name")).run()[0][0]) if sle_count > 500000: frappe.throw(_("Please set filter based on Item or Warehouse")) if not filters.get("company"): @@ -108,30 +110,21 @@ def validate_filters(filters): def get_warehouse_list(filters): from frappe.core.doctype.user_permission.user_permission import get_permitted_documents - condition = "" - user_permitted_warehouse = get_permitted_documents("Warehouse") - value = () - if user_permitted_warehouse: - condition = "and name in %s" - value = set(user_permitted_warehouse) - elif not user_permitted_warehouse and filters.get("warehouse"): - condition = "and name = %s" - value = filters.get("warehouse") + wh = frappe.qb.DocType("Warehouse") + query = frappe.qb.from_(wh).select(wh.name).where(wh.is_group == 0) - return frappe.db.sql( - """select name - from `tabWarehouse` where is_group = 0 - {condition}""".format( - condition=condition - ), - value, - as_dict=1, - ) + user_permitted_warehouse = get_permitted_documents("Warehouse") + if user_permitted_warehouse: + query = query.where(wh.name.isin(set(user_permitted_warehouse))) + elif filters.get("warehouse"): + query = query.where(wh.name == filters.get("warehouse")) + + return query.run(as_dict=True) def add_warehouse_column(columns, warehouse_list): if len(warehouse_list) > 1: - columns += [_("Total Qty") + ":Int:50"] + columns += [_("Total Qty") + ":Int:120"] for wh in warehouse_list: - columns += [_(wh.name) + ":Int:54"] + columns += [_(wh.name) + ":Int:100"] diff --git a/erpnext/regional/saudi_arabia/wizard/data/__init__.py b/erpnext/stock/report/warehouse_wise_stock_balance/__init__.py similarity index 100% rename from erpnext/regional/saudi_arabia/wizard/data/__init__.py rename to erpnext/stock/report/warehouse_wise_stock_balance/__init__.py diff --git a/erpnext/stock/report/warehouse_wise_stock_balance/warehouse_wise_stock_balance.js b/erpnext/stock/report/warehouse_wise_stock_balance/warehouse_wise_stock_balance.js new file mode 100644 index 0000000000..58a043ec20 --- /dev/null +++ b/erpnext/stock/report/warehouse_wise_stock_balance/warehouse_wise_stock_balance.js @@ -0,0 +1,20 @@ +// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Warehouse Wise Stock Balance"] = { + "filters": [ + { + "fieldname":"company", + "label": __("Company"), + "fieldtype": "Link", + "options": "Company", + "reqd": 1, + "default": frappe.defaults.get_user_default("Company") + } + ], + "initial_depth": 3, + "tree": true, + "parent_field": "parent_warehouse", + "name_field": "warehouse" +}; diff --git a/erpnext/stock/report/warehouse_wise_stock_balance/warehouse_wise_stock_balance.json b/erpnext/stock/report/warehouse_wise_stock_balance/warehouse_wise_stock_balance.json new file mode 100644 index 0000000000..4f7ec65de3 --- /dev/null +++ b/erpnext/stock/report/warehouse_wise_stock_balance/warehouse_wise_stock_balance.json @@ -0,0 +1,30 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2022-12-06 14:15:31.924345", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "json": "{}", + "modified": "2022-12-06 14:16:55.969214", + "modified_by": "Administrator", + "module": "Stock", + "name": "Warehouse Wise Stock Balance", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Stock Ledger Entry", + "report_name": "Warehouse Wise Stock Balance", + "report_type": "Script Report", + "roles": [ + { + "role": "Stock User" + }, + { + "role": "Accounts Manager" + } + ] +} \ No newline at end of file diff --git a/erpnext/stock/report/warehouse_wise_stock_balance/warehouse_wise_stock_balance.py b/erpnext/stock/report/warehouse_wise_stock_balance/warehouse_wise_stock_balance.py new file mode 100644 index 0000000000..d364b577a2 --- /dev/null +++ b/erpnext/stock/report/warehouse_wise_stock_balance/warehouse_wise_stock_balance.py @@ -0,0 +1,103 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from typing import Any, Dict, List, Optional, TypedDict + +import frappe +from frappe import _ +from frappe.query_builder.functions import Sum + + +class StockBalanceFilter(TypedDict): + company: Optional[str] + warehouse: Optional[str] + + +SLEntry = Dict[str, Any] + + +def execute(filters=None): + columns, data = [], [] + columns = get_columns() + data = get_data(filters) + + return columns, data + + +def get_warehouse_wise_balance(filters: StockBalanceFilter) -> List[SLEntry]: + sle = frappe.qb.DocType("Stock Ledger Entry") + + query = ( + frappe.qb.from_(sle) + .select(sle.warehouse, Sum(sle.stock_value_difference).as_("stock_balance")) + .where((sle.docstatus < 2) & (sle.is_cancelled == 0)) + .groupby(sle.warehouse) + ) + + if filters.get("company"): + query = query.where(sle.company == filters.get("company")) + + data = query.run(as_list=True) + return frappe._dict(data) if data else frappe._dict() + + +def get_warehouses(report_filters: StockBalanceFilter): + return frappe.get_all( + "Warehouse", + fields=["name", "parent_warehouse", "is_group"], + filters={"company": report_filters.company}, + order_by="lft", + ) + + +def get_data(filters: StockBalanceFilter): + warehouse_balance = get_warehouse_wise_balance(filters) + warehouses = get_warehouses(filters) + + for warehouse in warehouses: + warehouse.stock_balance = warehouse_balance.get(warehouse.name, 0) or 0.0 + + update_indent(warehouses) + set_balance_in_parent(warehouses) + + return warehouses + + +def update_indent(warehouses): + for warehouse in warehouses: + + def add_indent(warehouse, indent): + warehouse.indent = indent + for child in warehouses: + if child.parent_warehouse == warehouse.name: + add_indent(child, indent + 1) + + if warehouse.is_group: + add_indent(warehouse, warehouse.indent or 0) + + +def set_balance_in_parent(warehouses): + # sort warehouses by indent in descending order + warehouses = sorted(warehouses, key=lambda x: x.get("indent", 0), reverse=1) + + for warehouse in warehouses: + + def update_balance(warehouse, balance): + for parent in warehouses: + if warehouse.parent_warehouse == parent.name: + parent.stock_balance += balance + + update_balance(warehouse, warehouse.stock_balance) + + +def get_columns(): + return [ + { + "label": _("Warehouse"), + "fieldname": "name", + "fieldtype": "Link", + "options": "Warehouse", + "width": 200, + }, + {"label": _("Stock Balance"), "fieldname": "stock_balance", "fieldtype": "Float", "width": 150}, + ] diff --git a/erpnext/stock/stock_balance.py b/erpnext/stock/stock_balance.py index 14cedd2e8a..439ed7a8e0 100644 --- a/erpnext/stock/stock_balance.py +++ b/erpnext/stock/stock_balance.py @@ -121,7 +121,7 @@ def get_reserved_qty(item_code, warehouse): and parenttype='Sales Order' and item_code != parent_item and exists (select * from `tabSales Order` so - where name = dnpi_in.parent and docstatus = 1 and status != 'Closed') + where name = dnpi_in.parent and docstatus = 1 and status not in ('On Hold', 'Closed')) ) dnpi) union (select stock_qty as dnpi_qty, qty as so_item_qty, @@ -131,7 +131,7 @@ def get_reserved_qty(item_code, warehouse): and (so_item.delivered_by_supplier is null or so_item.delivered_by_supplier = 0) and exists(select * from `tabSales Order` so where so.name = so_item.parent and so.docstatus = 1 - and so.status != 'Closed')) + and so.status not in ('On Hold', 'Closed'))) ) tab where so_item_qty >= so_item_delivered_qty diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index fd1aece7b1..08fc6fbd42 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -263,8 +263,8 @@ def repost_future_sle( def validate_item_warehouse(args): for field in ["item_code", "warehouse", "posting_date", "posting_time"]: - if not args.get(field): - validation_msg = f"The field {frappe.unscrub(args.get(field))} is required for the reposting" + if args.get(field) in [None, ""]: + validation_msg = f"The field {frappe.unscrub(field)} is required for the reposting" frappe.throw(_(validation_msg)) @@ -470,8 +470,10 @@ class update_entries_after(object): item_code = %(item_code)s and warehouse = %(warehouse)s and is_cancelled = 0 - and timestamp(posting_date, time_format(posting_time, %(time_format)s)) = timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s)) - + and ( + posting_date = %(posting_date)s and + time_format(posting_time, %(time_format)s) = time_format(%(posting_time)s, %(time_format)s) + ) order by creation ASC for update @@ -542,6 +544,14 @@ class update_entries_after(object): if not self.args.get("sle_id"): self.get_dynamic_incoming_outgoing_rate(sle) + if ( + sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"] + and sle.voucher_detail_no + and sle.actual_qty < 0 + and frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_internal_supplier") + ): + sle.outgoing_rate = get_incoming_rate_for_inter_company_transfer(sle) + if get_serial_nos(sle.serial_no): self.get_serialized_values(sle) self.wh_data.qty_after_transaction += flt(sle.actual_qty) @@ -589,6 +599,7 @@ class update_entries_after(object): sle.stock_queue = json.dumps(self.wh_data.stock_queue) sle.stock_value_difference = stock_value_difference sle.doctype = "Stock Ledger Entry" + frappe.get_doc(sle).db_update() if not self.args.get("sle_id"): @@ -646,6 +657,13 @@ class update_entries_after(object): voucher_detail_no=sle.voucher_detail_no, sle=sle, ) + + elif ( + sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"] + and sle.voucher_detail_no + and frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_internal_supplier") + ): + rate = get_incoming_rate_for_inter_company_transfer(sle) else: if sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"): rate_field = "valuation_rate" @@ -726,9 +744,12 @@ class update_entries_after(object): def update_rate_on_purchase_receipt(self, sle, outgoing_rate): if frappe.db.exists(sle.voucher_type + " Item", sle.voucher_detail_no): - frappe.db.set_value( - sle.voucher_type + " Item", sle.voucher_detail_no, "base_net_rate", outgoing_rate - ) + if sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"] and frappe.get_cached_value( + sle.voucher_type, sle.voucher_no, "is_internal_supplier" + ): + frappe.db.set_value( + f"{sle.voucher_type} Item", sle.voucher_detail_no, "valuation_rate", sle.outgoing_rate + ) else: frappe.db.set_value( "Purchase Receipt Item Supplied", sle.voucher_detail_no, "rate", outgoing_rate @@ -1026,10 +1047,10 @@ class update_entries_after(object): updated_values = {"actual_qty": data.qty_after_transaction, "stock_value": data.stock_value} if data.valuation_rate is not None: updated_values["valuation_rate"] = data.valuation_rate - frappe.db.set_value("Bin", bin_name, updated_values) + frappe.db.set_value("Bin", bin_name, updated_values, update_modified=True) -def get_previous_sle_of_current_voucher(args, exclude_current_voucher=False): +def get_previous_sle_of_current_voucher(args, operator="<", exclude_current_voucher=False): """get stock ledger entries filtered by specific posting datetime conditions""" args["time_format"] = "%H:%i:%s" @@ -1051,11 +1072,17 @@ def get_previous_sle_of_current_voucher(args, exclude_current_voucher=False): and warehouse = %(warehouse)s and is_cancelled = 0 {voucher_condition} - and timestamp(posting_date, time_format(posting_time, %(time_format)s)) < timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s)) + and ( + posting_date < %(posting_date)s or + ( + posting_date = %(posting_date)s and + time_format(posting_time, %(time_format)s) {operator} time_format(%(posting_time)s, %(time_format)s) + ) + ) order by timestamp(posting_date, posting_time) desc, creation desc limit 1 for update""".format( - voucher_condition=voucher_condition + operator=operator, voucher_condition=voucher_condition ), args, as_dict=1, @@ -1152,7 +1179,7 @@ def get_stock_ledger_entries( def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None): return frappe.db.get_value( "Stock Ledger Entry", - {"voucher_detail_no": voucher_detail_no, "name": ["!=", excluded_sle]}, + {"voucher_detail_no": voucher_detail_no, "name": ["!=", excluded_sle], "is_cancelled": 0}, [ "item_code", "warehouse", @@ -1243,20 +1270,6 @@ def get_valuation_rate( (item_code, warehouse, voucher_no, voucher_type), ) - if not last_valuation_rate: - # Get valuation rate from last sle for the item against any warehouse - last_valuation_rate = frappe.db.sql( - """select valuation_rate - from `tabStock Ledger Entry` force index (item_code) - where - item_code = %s - AND valuation_rate > 0 - AND is_cancelled = 0 - AND NOT(voucher_no = %s AND voucher_type = %s) - order by posting_date desc, posting_time desc, name desc limit 1""", - (item_code, voucher_no, voucher_type), - ) - if last_valuation_rate: return flt(last_valuation_rate[0][0]) @@ -1336,8 +1349,13 @@ def update_qty_in_future_sle(args, allow_negative_stock=False): and warehouse = %(warehouse)s and voucher_no != %(voucher_no)s and is_cancelled = 0 - and timestamp(posting_date, time_format(posting_time, %(time_format)s)) - > timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s)) + and ( + posting_date > %(posting_date)s or + ( + posting_date = %(posting_date)s and + time_format(posting_time, %(time_format)s) > time_format(%(posting_time)s, %(time_format)s) + ) + ) {datetime_limit_condition} """, args, @@ -1357,7 +1375,7 @@ def get_stock_reco_qty_shift(args): stock_reco_qty_shift = flt(args.actual_qty) else: # reco is being submitted - last_balance = get_previous_sle_of_current_voucher(args, exclude_current_voucher=True).get( + last_balance = get_previous_sle_of_current_voucher(args, "<=", exclude_current_voucher=True).get( "qty_after_transaction" ) @@ -1519,3 +1537,25 @@ def is_negative_stock_allowed(*, item_code: Optional[str] = None) -> bool: if item_code and cint(frappe.db.get_value("Item", item_code, "allow_negative_stock", cache=True)): return True return False + + +def get_incoming_rate_for_inter_company_transfer(sle) -> float: + """ + For inter company transfer, incoming rate is the average of the outgoing rate + """ + rate = 0.0 + + field = "delivery_note_item" if sle.voucher_type == "Purchase Receipt" else "sales_invoice_item" + + doctype = "Delivery Note Item" if sle.voucher_type == "Purchase Receipt" else "Sales Invoice Item" + + reference_name = frappe.get_cached_value(sle.voucher_type + " Item", sle.voucher_detail_no, field) + + if reference_name: + rate = frappe.get_cached_value( + doctype, + reference_name, + "incoming_rate", + ) + + return rate diff --git a/erpnext/stock/tests/test_get_item_details.py b/erpnext/stock/tests/test_get_item_details.py new file mode 100644 index 0000000000..b53e29e9e8 --- /dev/null +++ b/erpnext/stock/tests/test_get_item_details.py @@ -0,0 +1,40 @@ +import json + +import frappe +from frappe.test_runner import make_test_records +from frappe.tests.utils import FrappeTestCase + +from erpnext.stock.get_item_details import get_item_details + +test_ignore = ["BOM"] +test_dependencies = ["Customer", "Supplier", "Item", "Price List", "Item Price"] + + +class TestGetItemDetail(FrappeTestCase): + def setUp(self): + make_test_records("Price List") + super().setUp() + + def test_get_item_detail_purchase_order(self): + + args = frappe._dict( + { + "item_code": "_Test Item", + "company": "_Test Company", + "customer": "_Test Customer", + "conversion_rate": 1.0, + "price_list_currency": "USD", + "plc_conversion_rate": 1.0, + "doctype": "Purchase Order", + "name": None, + "supplier": "_Test Supplier", + "transaction_date": None, + "conversion_rate": 1.0, + "price_list": "_Test Buying Price List", + "is_subcontracted": 0, + "ignore_pricing_rule": 1, + "qty": 1, + } + ) + details = get_item_details(args) + self.assertEqual(details.get("price_list_rate"), 100) diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index 9fb3be5188..b8c5187b2c 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -13,6 +13,8 @@ from frappe.utils import cstr, flt, get_link_to_form, nowdate, nowtime import erpnext from erpnext.stock.valuation import FIFOValuation, LIFOValuation +BarcodeScanResult = Dict[str, Optional[str]] + class InvalidWarehouseCompany(frappe.ValidationError): pass @@ -552,7 +554,16 @@ def check_pending_reposting(posting_date: str, throw_error: bool = True) -> bool @frappe.whitelist() -def scan_barcode(search_value: str) -> Dict[str, Optional[str]]: +def scan_barcode(search_value: str) -> BarcodeScanResult: + def set_cache(data: BarcodeScanResult): + frappe.cache().set_value(f"erpnext:barcode_scan:{search_value}", data, expires_in_sec=120) + + def get_cache() -> Optional[BarcodeScanResult]: + if data := frappe.cache().get_value(f"erpnext:barcode_scan:{search_value}"): + return data + + if scan_data := get_cache(): + return scan_data # search barcode no barcode_data = frappe.db.get_value( @@ -562,7 +573,9 @@ def scan_barcode(search_value: str) -> Dict[str, Optional[str]]: as_dict=True, ) if barcode_data: - return _update_item_info(barcode_data) + _update_item_info(barcode_data) + set_cache(barcode_data) + return barcode_data # search serial no serial_no_data = frappe.db.get_value( @@ -572,7 +585,9 @@ def scan_barcode(search_value: str) -> Dict[str, Optional[str]]: as_dict=True, ) if serial_no_data: - return _update_item_info(serial_no_data) + _update_item_info(serial_no_data) + set_cache(serial_no_data) + return serial_no_data # search batch no batch_no_data = frappe.db.get_value( @@ -582,7 +597,9 @@ def scan_barcode(search_value: str) -> Dict[str, Optional[str]]: as_dict=True, ) if batch_no_data: - return _update_item_info(batch_no_data) + _update_item_info(batch_no_data) + set_cache(batch_no_data) + return batch_no_data return {} diff --git a/erpnext/stock/workspace/stock/stock.json b/erpnext/stock/workspace/stock/stock.json index ed33067e73..de5e6de8f1 100644 --- a/erpnext/stock/workspace/stock/stock.json +++ b/erpnext/stock/workspace/stock/stock.json @@ -5,7 +5,7 @@ "label": "Warehouse wise Stock Value" } ], - "content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Stock\",\"col\":12}},{\"type\":\"chart\",\"data\":{\"chart_name\":\"Warehouse wise Stock Value\",\"col\":12}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Quick Access\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Item\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Material Request\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Stock Entry\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Purchase Receipt\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Delivery Note\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Stock Ledger\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Stock Balance\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Masters & Reports\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Items and Pricing\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Stock Transactions\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Stock Reports\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Serial No and Batch\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Tools\",\"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\":\"Stock\",\"col\":12}},{\"type\":\"chart\",\"data\":{\"chart_name\":\"Warehouse wise Stock Value\",\"col\":12}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Quick Access\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Item\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Material Request\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Stock Entry\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Purchase Receipt\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Delivery Note\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Stock Ledger\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Stock Balance\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Masters & Reports\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Items and Pricing\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Stock Transactions\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Stock Reports\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Serial No and Batch\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Tools\",\"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:43:10.096528", "docstatus": 0, "doctype": "Workspace", @@ -207,80 +207,6 @@ "onboard": 0, "type": "Link" }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Stock Reports", - "link_count": 0, - "onboard": 0, - "type": "Card Break" - }, - { - "dependencies": "Item", - "hidden": 0, - "is_query_report": 1, - "label": "Stock Ledger", - "link_count": 0, - "link_to": "Stock Ledger", - "link_type": "Report", - "onboard": 1, - "type": "Link" - }, - { - "dependencies": "Item", - "hidden": 0, - "is_query_report": 1, - "label": "Stock Balance", - "link_count": 0, - "link_to": "Stock Balance", - "link_type": "Report", - "onboard": 1, - "type": "Link" - }, - { - "dependencies": "Item", - "hidden": 0, - "is_query_report": 1, - "label": "Stock Projected Qty", - "link_count": 0, - "link_to": "Stock Projected Qty", - "link_type": "Report", - "onboard": 1, - "type": "Link" - }, - { - "dependencies": "Item", - "hidden": 0, - "is_query_report": 0, - "label": "Stock Summary", - "link_count": 0, - "link_to": "stock-balance", - "link_type": "Page", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "Item", - "hidden": 0, - "is_query_report": 1, - "label": "Stock Ageing", - "link_count": 0, - "link_to": "Stock Ageing", - "link_type": "Report", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "Item", - "hidden": 0, - "is_query_report": 1, - "label": "Item Price Stock", - "link_count": 0, - "link_to": "Item Price Stock", - "link_type": "Report", - "onboard": 0, - "type": "Link" - }, { "hidden": 0, "is_query_report": 0, @@ -705,15 +631,100 @@ "link_type": "Report", "onboard": 0, "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Stock Reports", + "link_count": 7, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "Item", + "hidden": 0, + "is_query_report": 1, + "label": "Stock Ledger", + "link_count": 0, + "link_to": "Stock Ledger", + "link_type": "Report", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "Item", + "hidden": 0, + "is_query_report": 1, + "label": "Stock Balance", + "link_count": 0, + "link_to": "Stock Balance", + "link_type": "Report", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "Item", + "hidden": 0, + "is_query_report": 1, + "label": "Stock Projected Qty", + "link_count": 0, + "link_to": "Stock Projected Qty", + "link_type": "Report", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "Item", + "hidden": 0, + "is_query_report": 0, + "label": "Stock Summary", + "link_count": 0, + "link_to": "stock-balance", + "link_type": "Page", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Item", + "hidden": 0, + "is_query_report": 1, + "label": "Stock Ageing", + "link_count": 0, + "link_to": "Stock Ageing", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Item", + "hidden": 0, + "is_query_report": 1, + "label": "Item Price Stock", + "link_count": 0, + "link_to": "Item Price Stock", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Warehouse Wise Stock Balance", + "link_count": 0, + "link_to": "Warehouse Wise Stock Balance", + "link_type": "Report", + "onboard": 0, + "type": "Link" } ], - "modified": "2022-01-13 17:47:38.339931", + "modified": "2022-12-06 17:03:56.397272", "modified_by": "Administrator", "module": "Stock", "name": "Stock", "owner": "Administrator", "parent_page": "", "public": 1, + "quick_lists": [], "restrict_to_domain": "", "roles": [], "sequence_id": 24.0, diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js index dbd337afd4..15a2ac9091 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js @@ -3,6 +3,8 @@ frappe.provide('erpnext.buying'); +{% include 'erpnext/stock/landed_taxes_and_charges_common.js' %}; + frappe.ui.form.on('Subcontracting Order', { setup: (frm) => { frm.get_field("items").grid.cannot_add_rows = true; @@ -105,9 +107,9 @@ frappe.ui.form.on('Subcontracting Order', { get_materials_from_supplier: function (frm) { let sco_rm_details = []; - if (frm.doc.supplied_items && (frm.doc.per_received == 100)) { + if (frm.doc.status != "Closed" && frm.doc.supplied_items && frm.doc.per_received > 0) { frm.doc.supplied_items.forEach(d => { - if (d.total_supplied_qty && d.total_supplied_qty != d.consumed_qty) { + if (d.total_supplied_qty > 0 && d.total_supplied_qty != d.consumed_qty) { sco_rm_details.push(d.name); } }); @@ -136,6 +138,16 @@ frappe.ui.form.on('Subcontracting Order', { } }); +frappe.ui.form.on('Landed Cost Taxes and Charges', { + amount: function (frm, cdt, cdn) { + frm.events.set_base_amount(frm, cdt, cdn); + }, + + expense_account: function (frm, cdt, cdn) { + frm.events.set_account_currency(frm, cdt, cdn); + } +}); + erpnext.buying.SubcontractingOrderController = class SubcontractingOrderController { setup() { this.frm.custom_make_buttons = { @@ -148,14 +160,11 @@ erpnext.buying.SubcontractingOrderController = class SubcontractingOrderControll var me = this; if (doc.docstatus == 1) { - if (doc.status != 'Completed') { + if (!['Closed', 'Completed'].includes(doc.status)) { if (flt(doc.per_received) < 100) { cur_frm.add_custom_button(__('Subcontracting Receipt'), this.make_subcontracting_receipt, __('Create')); if (me.has_unsupplied_items()) { - cur_frm.add_custom_button(__('Material to Supplier'), - () => { - me.make_stock_entry(); - }, __('Transfer')); + cur_frm.add_custom_button(__('Material to Supplier'), this.make_stock_entry, __('Transfer')); } } cur_frm.page.set_inner_btn_group_as_primary(__('Create')); @@ -183,120 +192,6 @@ erpnext.buying.SubcontractingOrderController = class SubcontractingOrderControll transaction_controller.autofill_warehouse(child_table, warehouse_field, warehouse); } - make_stock_entry() { - var items = $.map(cur_frm.doc.items, (d) => d.bom ? d.item_code : false); - var me = this; - - if (items.length >= 1) { - me.raw_material_data = []; - me.show_dialog = 1; - let title = __('Transfer Material to Supplier'); - let fields = [ - { fieldtype: 'Section Break', label: __('Raw Materials') }, - { - fieldname: 'sub_con_rm_items', fieldtype: 'Table', label: __('Items'), - fields: [ - { - fieldtype: 'Data', - fieldname: 'item_code', - label: __('Item'), - read_only: 1, - in_list_view: 1 - }, - { - fieldtype: 'Data', - fieldname: 'rm_item_code', - label: __('Raw Material'), - read_only: 1, - in_list_view: 1 - }, - { - fieldtype: 'Float', - read_only: 1, - fieldname: 'qty', - label: __('Quantity'), - in_list_view: 1 - }, - { - fieldtype: 'Data', - read_only: 1, - fieldname: 'warehouse', - label: __('Reserve Warehouse'), - in_list_view: 1 - }, - { - fieldtype: 'Float', - read_only: 1, - fieldname: 'rate', - label: __('Rate'), - hidden: 1 - }, - { - fieldtype: 'Float', - read_only: 1, - fieldname: 'amount', - label: __('Amount'), - hidden: 1 - }, - { - fieldtype: 'Link', - read_only: 1, - fieldname: 'uom', - label: __('UOM'), - hidden: 1 - } - ], - data: me.raw_material_data, - get_data: () => me.raw_material_data - } - ]; - - me.dialog = new frappe.ui.Dialog({ - title: title, fields: fields - }); - - if (me.frm.doc['supplied_items']) { - me.frm.doc['supplied_items'].forEach((item) => { - if (item.rm_item_code && item.main_item_code && item.required_qty - item.supplied_qty != 0) { - me.raw_material_data.push({ - 'name': item.name, - 'item_code': item.main_item_code, - 'rm_item_code': item.rm_item_code, - 'item_name': item.rm_item_code, - 'qty': item.required_qty - item.supplied_qty, - 'warehouse': item.reserve_warehouse, - 'rate': item.rate, - 'amount': item.amount, - 'stock_uom': item.stock_uom - }); - me.dialog.fields_dict.sub_con_rm_items.grid.refresh(); - } - }); - } - - me.dialog.get_field('sub_con_rm_items').check_all_rows(); - - me.dialog.show(); - this.dialog.set_primary_action(__('Transfer'), () => { - me.values = me.dialog.get_values(); - if (me.values) { - me.values.sub_con_rm_items.map((row, i) => { - if (!row.item_code || !row.rm_item_code || !row.warehouse || !row.qty || row.qty === 0) { - let row_id = i + 1; - frappe.throw(__('Item Code, warehouse and quantity are required on row {0}', [row_id])); - } - }); - me.make_rm_stock_entry(me.dialog.fields_dict.sub_con_rm_items.grid.get_selected_children()); - me.dialog.hide(); - } - }); - } - - me.dialog.get_close_btn().on('click', () => { - me.dialog.hide(); - }); - } - has_unsupplied_items() { return this.frm.doc['supplied_items'].some(item => item.required_qty > item.supplied_qty); } @@ -309,12 +204,11 @@ erpnext.buying.SubcontractingOrderController = class SubcontractingOrderControll }); } - make_rm_stock_entry(rm_items) { + make_stock_entry() { frappe.call({ method: 'erpnext.controllers.subcontracting_controller.make_rm_stock_entry', args: { subcontract_order: cur_frm.doc.name, - rm_items: rm_items, order_doctype: cur_frm.doc.doctype }, callback: (r) => { diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json index c6e76c76d7..f98f559d5c 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json @@ -19,6 +19,10 @@ "transaction_date", "schedule_date", "amended_from", + "accounting_dimensions_section", + "cost_center", + "dimension_col_break", + "project", "address_and_contact_section", "supplier_address", "address_display", @@ -422,12 +426,34 @@ "fieldtype": "Select", "label": "Distribute Additional Costs Based On ", "options": "Qty\nAmount" + }, + { + "collapsible": 1, + "fieldname": "accounting_dimensions_section", + "fieldtype": "Section Break", + "label": "Accounting Dimensions" + }, + { + "fieldname": "cost_center", + "fieldtype": "Link", + "label": "Cost Center", + "options": "Cost Center" + }, + { + "fieldname": "dimension_col_break", + "fieldtype": "Column Break" + }, + { + "fieldname": "project", + "fieldtype": "Link", + "label": "Project", + "options": "Project" } ], "icon": "fa fa-file-text", "is_submittable": 1, "links": [], - "modified": "2022-04-11 21:02:44.097841", + "modified": "2022-08-15 14:08:49.204218", "modified_by": "Administrator", "module": "Subcontracting", "name": "Subcontracting Order", diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py index 71cdc94a3a..e6de72d494 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py @@ -82,25 +82,6 @@ class SubcontractingOrder(SubcontractingController): self.set_missing_values_in_supplied_items() self.set_missing_values_in_items() - def set_missing_values_in_additional_costs(self): - if self.get("additional_costs"): - self.total_additional_costs = sum(flt(item.amount) for item in self.get("additional_costs")) - - if self.total_additional_costs: - if self.distribute_additional_costs_based_on == "Amount": - total_amt = sum(flt(item.amount) for item in self.get("items")) - for item in self.items: - item.additional_cost_per_qty = ( - (item.amount * self.total_additional_costs) / total_amt - ) / item.qty - else: - total_qty = sum(flt(item.qty) for item in self.get("items")) - additional_cost_per_qty = self.total_additional_costs / total_qty - for item in self.items: - item.additional_cost_per_qty = additional_cost_per_qty - else: - self.total_additional_costs = 0 - def set_missing_values_in_service_items(self): for idx, item in enumerate(self.get("service_items")): self.items[idx].service_cost_per_qty = item.amount / self.items[idx].qty @@ -114,9 +95,7 @@ class SubcontractingOrder(SubcontractingController): def set_missing_values_in_items(self): total_qty = total = 0 for item in self.items: - item.rate = ( - item.rm_cost_per_qty + item.service_cost_per_qty + (item.additional_cost_per_qty or 0) - ) + item.rate = item.rm_cost_per_qty + item.service_cost_per_qty + flt(item.additional_cost_per_qty) item.amount = item.qty * item.rate total_qty += flt(item.qty) total += flt(item.amount) @@ -174,7 +153,7 @@ class SubcontractingOrder(SubcontractingController): else: self.set_missing_values() - def update_status(self, status=None, update_modified=False): + def update_status(self, status=None, update_modified=True): if self.docstatus >= 1 and not status: if self.docstatus == 1: if self.status == "Draft": @@ -183,11 +162,15 @@ class SubcontractingOrder(SubcontractingController): status = "Completed" elif self.per_received > 0 and self.per_received < 100: status = "Partially Received" + for item in self.supplied_items: + if item.returned_qty: + status = "Closed" + break else: total_required_qty = total_supplied_qty = 0 for item in self.supplied_items: total_required_qty += item.required_qty - total_supplied_qty += item.supplied_qty or 0 + total_supplied_qty += flt(item.supplied_qty) if total_supplied_qty: status = "Partial Material Transferred" if total_supplied_qty >= total_required_qty: @@ -197,7 +180,10 @@ class SubcontractingOrder(SubcontractingController): elif self.docstatus == 2: status = "Cancelled" - frappe.db.set_value("Subcontracting Order", self.name, "status", status, update_modified) + if status: + frappe.db.set_value( + "Subcontracting Order", self.name, "status", status, update_modified=update_modified + ) @frappe.whitelist() diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order_list.js b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order_list.js index 650419cf74..7ca12642c5 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order_list.js +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order_list.js @@ -10,6 +10,8 @@ frappe.listview_settings['Subcontracting Order'] = { "Completed": "green", "Partial Material Transferred": "purple", "Material Transferred": "blue", + "Closed": "red", + "Cancelled": "red", }; return [__(doc.status), status_colors[doc.status], "status,=," + doc.status]; }, diff --git a/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py index 94bb38e980..6a2983faaa 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py +++ b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py @@ -2,12 +2,16 @@ # See license.txt import copy +from collections import defaultdict import frappe from frappe.tests.utils import FrappeTestCase from erpnext.buying.doctype.purchase_order.purchase_order import get_mapped_subcontracting_order -from erpnext.controllers.subcontracting_controller import make_rm_stock_entry +from erpnext.controllers.subcontracting_controller import ( + get_materials_from_supplier, + make_rm_stock_entry, +) from erpnext.controllers.tests.test_subcontracting_controller import ( get_rm_items, get_subcontracting_order, @@ -89,6 +93,16 @@ class TestSubcontractingOrder(FrappeTestCase): sco.load_from_db() self.assertEqual(sco.status, "Partially Received") + # Closed + ste = get_materials_from_supplier(sco.name, [d.name for d in sco.supplied_items]) + ste.save() + ste.submit() + sco.load_from_db() + self.assertEqual(sco.status, "Closed") + ste.cancel() + sco.load_from_db() + self.assertEqual(sco.status, "Partially Received") + # Completed scr = make_subcontracting_receipt(sco.name) scr.save() @@ -173,23 +187,48 @@ class TestSubcontractingOrder(FrappeTestCase): ) self.assertEqual(len(ste.items), len(rm_items)) + def test_make_rm_stock_entry_for_batch_items_with_less_transfer(self): + set_backflush_based_on("BOM") + + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 4", + "qty": 5, + "rate": 100, + "fg_item": "Subcontracted Item SA4", + "fg_item_qty": 5, + } + ] + + sco = get_subcontracting_order(service_items=service_items) + rm_items = get_rm_items(sco.supplied_items) + itemwise_details = make_stock_in_entry(rm_items=rm_items) + + itemwise_transfer_qty = defaultdict(int) + for item in rm_items: + item["qty"] -= 1 + itemwise_transfer_qty[item["item_code"]] += item["qty"] + + ste = make_stock_transfer_entry( + sco_no=sco.name, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) + + scr = make_subcontracting_receipt(sco.name) + + for row in scr.supplied_items: + self.assertEqual(row.consumed_qty, itemwise_transfer_qty.get(row.rm_item_code) + 1) + def test_update_reserved_qty_for_subcontracting(self): - # Make stock available for raw materials - make_stock_entry(target="_Test Warehouse - _TC", qty=10, basic_rate=100) + # Create RM Material Receipt + make_stock_entry(target="_Test Warehouse - _TC", item_code="_Test Item", qty=10, basic_rate=100) make_stock_entry( target="_Test Warehouse - _TC", item_code="_Test Item Home Desktop 100", qty=20, basic_rate=100 ) - make_stock_entry( - target="_Test Warehouse 1 - _TC", item_code="_Test Item", qty=30, basic_rate=100 - ) - make_stock_entry( - target="_Test Warehouse 1 - _TC", - item_code="_Test Item Home Desktop 100", - qty=30, - basic_rate=100, - ) - bin1 = frappe.db.get_value( + bin_before_sco = frappe.db.get_value( "Bin", filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, fieldname=["reserved_qty_for_sub_contract", "projected_qty", "modified"], @@ -209,102 +248,97 @@ class TestSubcontractingOrder(FrappeTestCase): ] sco = get_subcontracting_order(service_items=service_items) - bin2 = frappe.db.get_value( + bin_after_sco = frappe.db.get_value( "Bin", filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, fieldname=["reserved_qty_for_sub_contract", "projected_qty", "modified"], as_dict=1, ) - self.assertEqual(bin2.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract + 10) - self.assertEqual(bin2.projected_qty, bin1.projected_qty - 10) - self.assertNotEqual(bin1.modified, bin2.modified) + # reserved_qty_for_sub_contract should be increased by 10 + self.assertEqual( + bin_after_sco.reserved_qty_for_sub_contract, bin_before_sco.reserved_qty_for_sub_contract + 10 + ) - # Create stock transfer + # projected_qty should be decreased by 10 + self.assertEqual(bin_after_sco.projected_qty, bin_before_sco.projected_qty - 10) + + self.assertNotEqual(bin_before_sco.modified, bin_after_sco.modified) + + # Create Stock Entry(Send to Subcontractor) rm_items = [ { "item_code": "_Test FG Item", "rm_item_code": "_Test Item", "item_name": "_Test Item", - "qty": 6, + "qty": 10, "warehouse": "_Test Warehouse - _TC", "rate": 100, - "amount": 600, + "amount": 1000, "stock_uom": "Nos", - } + }, + { + "item_code": "_Test FG Item", + "rm_item_code": "_Test Item Home Desktop 100", + "item_name": "_Test Item Home Desktop 100", + "qty": 20, + "warehouse": "_Test Warehouse - _TC", + "rate": 100, + "amount": 2000, + "stock_uom": "Nos", + }, ] ste = frappe.get_doc(make_rm_stock_entry(sco.name, rm_items)) ste.to_warehouse = "_Test Warehouse 1 - _TC" ste.save() ste.submit() - bin3 = frappe.db.get_value( + bin_after_rm_transfer = frappe.db.get_value( "Bin", filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, fieldname="reserved_qty_for_sub_contract", as_dict=1, ) - self.assertEqual(bin3.reserved_qty_for_sub_contract, bin2.reserved_qty_for_sub_contract - 6) - - make_stock_entry( - target="_Test Warehouse 1 - _TC", item_code="_Test Item", qty=40, basic_rate=100 - ) - make_stock_entry( - target="_Test Warehouse 1 - _TC", - item_code="_Test Item Home Desktop 100", - qty=40, - basic_rate=100, + # reserved_qty_for_sub_contract should be decreased by 10 + self.assertEqual( + bin_after_rm_transfer.reserved_qty_for_sub_contract, + bin_after_sco.reserved_qty_for_sub_contract - 10, ) - # Make SCR against the SCO - scr = make_subcontracting_receipt(sco.name) - scr.save() - scr.submit() - - bin4 = frappe.db.get_value( - "Bin", - filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, - fieldname="reserved_qty_for_sub_contract", - as_dict=1, - ) - - self.assertEqual(bin4.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract) - - # Cancel SCR - scr.reload() - scr.cancel() - bin5 = frappe.db.get_value( - "Bin", - filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, - fieldname="reserved_qty_for_sub_contract", - as_dict=1, - ) - - self.assertEqual(bin5.reserved_qty_for_sub_contract, bin2.reserved_qty_for_sub_contract - 6) - - # Cancel Stock Entry + # Cancel Stock Entry(Send to Subcontractor) ste.cancel() - bin6 = frappe.db.get_value( + bin_after_cancel_ste = frappe.db.get_value( "Bin", filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, fieldname="reserved_qty_for_sub_contract", as_dict=1, ) - self.assertEqual(bin6.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract + 10) + # reserved_qty_for_sub_contract should be increased by 10 + self.assertEqual( + bin_after_cancel_ste.reserved_qty_for_sub_contract, + bin_after_rm_transfer.reserved_qty_for_sub_contract + 10, + ) - # Cancel PO + # Cancel SCO sco.reload() sco.cancel() - bin7 = frappe.db.get_value( + bin_after_cancel_sco = frappe.db.get_value( "Bin", filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, fieldname="reserved_qty_for_sub_contract", as_dict=1, ) - self.assertEqual(bin7.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract) + # reserved_qty_for_sub_contract should be decreased by 10 + self.assertEqual( + bin_after_cancel_sco.reserved_qty_for_sub_contract, + bin_after_cancel_ste.reserved_qty_for_sub_contract - 10, + ) + self.assertEqual( + bin_after_cancel_sco.reserved_qty_for_sub_contract, bin_before_sco.reserved_qty_for_sub_contract + ) def test_exploded_items(self): item_code = "_Test Subcontracted FG Item 11" @@ -503,6 +537,35 @@ class TestSubcontractingOrder(FrappeTestCase): set_backflush_based_on("BOM") + def test_get_materials_from_supplier(self): + # Create SCO + sco = get_subcontracting_order() + + # Transfer RM + rm_items = get_rm_items(sco.supplied_items) + itemwise_details = make_stock_in_entry(rm_items=rm_items) + make_stock_transfer_entry( + sco_no=sco.name, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) + + # Create SCR (Partial) + scr = make_subcontracting_receipt(sco.name) + scr.items[0].qty -= 5 + scr.save() + scr.submit() + + # Get RM from Supplier + ste = get_materials_from_supplier(sco.name, [d.name for d in sco.supplied_items]) + ste.save() + ste.submit() + + sco.load_from_db() + + self.assertEqual(sco.status, "Closed") + self.assertEqual(sco.supplied_items[0].returned_qty, 5) + def create_subcontracting_order(**args): args = frappe._dict(args) @@ -511,7 +574,7 @@ def create_subcontracting_order(**args): for item in sco.items: item.include_exploded_items = args.get("include_exploded_items", 1) - if args.get("warehouse"): + if args.warehouse: for item in sco.items: item.warehouse = args.warehouse else: diff --git a/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.json b/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.json index 291f47a634..d77e77440e 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.json +++ b/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.json @@ -1,326 +1,353 @@ { - "actions": [], - "autoname": "hash", - "creation": "2022-04-01 19:26:31.475015", - "doctype": "DocType", - "document_type": "Document", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "item_code", - "item_name", - "bom", - "include_exploded_items", - "column_break_3", - "schedule_date", - "expected_delivery_date", - "description_section", - "description", - "column_break_8", - "image", - "image_view", - "quantity_and_rate_section", - "qty", - "received_qty", - "returned_qty", - "column_break_13", - "stock_uom", - "conversion_factor", - "section_break_16", - "rate", - "amount", - "column_break_19", - "rm_cost_per_qty", - "service_cost_per_qty", - "additional_cost_per_qty", - "warehouse_section", - "warehouse", - "accounting_details_section", - "expense_account", - "manufacture_section", - "manufacturer", - "manufacturer_part_no", - "section_break_34", - "page_break" - ], - "fields": [ - { - "bold": 1, - "columns": 2, - "fieldname": "item_code", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Item Code", - "options": "Item", - "read_only": 1, - "reqd": 1, - "search_index": 1 - }, - { - "fetch_from": "item_code.item_name", - "fetch_if_empty": 1, - "fieldname": "item_name", - "fieldtype": "Data", - "in_global_search": 1, - "label": "Item Name", - "print_hide": 1, - "reqd": 1 - }, - { - "fieldname": "column_break_3", - "fieldtype": "Column Break" - }, - { - "bold": 1, - "columns": 2, - "fieldname": "schedule_date", - "fieldtype": "Date", - "label": "Required By", - "print_hide": 1, - "read_only": 1 - }, - { - "allow_on_submit": 1, - "bold": 1, - "fieldname": "expected_delivery_date", - "fieldtype": "Date", - "label": "Expected Delivery Date", - "search_index": 1 - }, - { - "collapsible": 1, - "fieldname": "description_section", - "fieldtype": "Section Break", - "label": "Description" - }, - { - "fetch_from": "item_code.description", - "fetch_if_empty": 1, - "fieldname": "description", - "fieldtype": "Text Editor", - "label": "Description", - "print_width": "300px", - "reqd": 1, - "width": "300px" - }, - { - "fieldname": "column_break_8", - "fieldtype": "Column Break" - }, - { - "fieldname": "image", - "fieldtype": "Attach", - "hidden": 1, - "label": "Image" - }, - { - "fieldname": "image_view", - "fieldtype": "Image", - "label": "Image View", - "options": "image", - "print_hide": 1 - }, - { - "fieldname": "quantity_and_rate_section", - "fieldtype": "Section Break", - "label": "Quantity and Rate" - }, - { - "bold": 1, - "columns": 1, - "default": "1", - "fieldname": "qty", - "fieldtype": "Float", - "in_list_view": 1, - "label": "Quantity", - "print_width": "60px", - "read_only": 1, - "reqd": 1, - "width": "60px" - }, - { - "fieldname": "column_break_13", - "fieldtype": "Column Break", - "print_hide": 1 - }, - { - "fieldname": "stock_uom", - "fieldtype": "Link", - "label": "Stock UOM", - "options": "UOM", - "print_width": "100px", - "read_only": 1, - "reqd": 1, - "width": "100px" - }, - { - "default": "1", - "fieldname": "conversion_factor", - "fieldtype": "Float", - "hidden": 1, - "label": "Conversion Factor", - "read_only": 1 - }, - { - "fieldname": "section_break_16", - "fieldtype": "Section Break" - }, - { - "bold": 1, - "columns": 2, - "fetch_from": "item_code.standard_rate", - "fetch_if_empty": 1, - "fieldname": "rate", - "fieldtype": "Currency", - "in_list_view": 1, - "label": "Rate", - "options": "currency", - "read_only": 1, - "reqd": 1 - }, - { - "fieldname": "column_break_19", - "fieldtype": "Column Break" - }, - { - "columns": 2, - "fieldname": "amount", - "fieldtype": "Currency", - "in_list_view": 1, - "label": "Amount", - "options": "currency", - "read_only": 1, - "reqd": 1 - }, - { - "fieldname": "warehouse_section", - "fieldtype": "Section Break", - "label": "Warehouse Details" - }, - { - "fieldname": "warehouse", - "fieldtype": "Link", - "label": "Warehouse", - "options": "Warehouse", - "print_hide": 1, - "reqd": 1 - }, - { - "collapsible": 1, - "fieldname": "accounting_details_section", - "fieldtype": "Section Break", - "label": "Accounting Details" - }, - { - "fieldname": "expense_account", - "fieldtype": "Link", - "label": "Expense Account", - "options": "Account", - "print_hide": 1 - }, - { - "collapsible": 1, - "fieldname": "manufacture_section", - "fieldtype": "Section Break", - "label": "Manufacture" - }, - { - "fieldname": "manufacturer", - "fieldtype": "Link", - "label": "Manufacturer", - "options": "Manufacturer" - }, - { - "fieldname": "manufacturer_part_no", - "fieldtype": "Data", - "label": "Manufacturer Part Number" - }, - { - "depends_on": "item_code", - "fetch_from": "item_code.default_bom", - "fieldname": "bom", - "fieldtype": "Link", - "in_list_view": 1, - "label": "BOM", - "options": "BOM", - "print_hide": 1, - "reqd": 1 - }, - { - "default": "0", - "fieldname": "include_exploded_items", - "fieldtype": "Check", - "label": "Include Exploded Items", - "print_hide": 1 - }, - { - "fieldname": "service_cost_per_qty", - "fieldtype": "Currency", - "label": "Service Cost Per Qty", - "read_only": 1, - "reqd": 1 - }, - { - "default": "0", - "fieldname": "additional_cost_per_qty", - "fieldtype": "Currency", - "label": "Additional Cost Per Qty", - "read_only": 1 - }, - { - "fieldname": "rm_cost_per_qty", - "fieldtype": "Currency", - "label": "Raw Material Cost Per Qty", - "no_copy": 1, - "read_only": 1 - }, - { - "allow_on_submit": 1, - "default": "0", - "fieldname": "page_break", - "fieldtype": "Check", - "label": "Page Break", - "no_copy": 1, - "print_hide": 1 - }, - { - "fieldname": "section_break_34", - "fieldtype": "Section Break" - }, - { - "depends_on": "received_qty", - "fieldname": "received_qty", - "fieldtype": "Float", - "label": "Received Qty", - "no_copy": 1, - "print_hide": 1, - "read_only": 1 - }, - { - "depends_on": "returned_qty", - "fieldname": "returned_qty", - "fieldtype": "Float", - "label": "Returned Qty", - "no_copy": 1, - "print_hide": 1, - "read_only": 1 - } - ], - "idx": 1, - "index_web_pages_for_search": 1, - "istable": 1, - "links": [], - "modified": "2022-04-11 21:28:06.585338", - "modified_by": "Administrator", - "module": "Subcontracting", - "name": "Subcontracting Order Item", - "naming_rule": "Random", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "search_fields": "item_name", - "sort_field": "modified", - "sort_order": "DESC", - "states": [], - "track_changes": 1 + "actions": [], + "autoname": "hash", + "creation": "2022-04-01 19:26:31.475015", + "doctype": "DocType", + "document_type": "Document", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "item_code", + "item_name", + "bom", + "include_exploded_items", + "column_break_3", + "schedule_date", + "expected_delivery_date", + "description_section", + "description", + "column_break_8", + "image", + "image_view", + "quantity_and_rate_section", + "qty", + "received_qty", + "returned_qty", + "column_break_13", + "stock_uom", + "conversion_factor", + "section_break_16", + "rate", + "amount", + "column_break_19", + "rm_cost_per_qty", + "service_cost_per_qty", + "additional_cost_per_qty", + "warehouse_section", + "warehouse", + "accounting_details_section", + "expense_account", + "manufacture_section", + "manufacturer", + "manufacturer_part_no", + "accounting_dimensions_section", + "cost_center", + "dimension_col_break", + "project", + "section_break_34", + "page_break" + ], + "fields": [ + { + "bold": 1, + "columns": 2, + "fieldname": "item_code", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Item Code", + "options": "Item", + "read_only": 1, + "reqd": 1, + "search_index": 1 + }, + { + "fetch_from": "item_code.item_name", + "fetch_if_empty": 1, + "fieldname": "item_name", + "fieldtype": "Data", + "in_global_search": 1, + "label": "Item Name", + "print_hide": 1, + "reqd": 1 + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "bold": 1, + "columns": 2, + "fieldname": "schedule_date", + "fieldtype": "Date", + "label": "Required By", + "print_hide": 1, + "read_only": 1 + }, + { + "allow_on_submit": 1, + "bold": 1, + "fieldname": "expected_delivery_date", + "fieldtype": "Date", + "label": "Expected Delivery Date", + "search_index": 1 + }, + { + "collapsible": 1, + "fieldname": "description_section", + "fieldtype": "Section Break", + "label": "Description" + }, + { + "fetch_from": "item_code.description", + "fetch_if_empty": 1, + "fieldname": "description", + "fieldtype": "Text Editor", + "label": "Description", + "print_width": "300px", + "reqd": 1, + "width": "300px" + }, + { + "fieldname": "column_break_8", + "fieldtype": "Column Break" + }, + { + "fieldname": "image", + "fieldtype": "Attach", + "hidden": 1, + "label": "Image" + }, + { + "fieldname": "image_view", + "fieldtype": "Image", + "label": "Image View", + "options": "image", + "print_hide": 1 + }, + { + "fieldname": "quantity_and_rate_section", + "fieldtype": "Section Break", + "label": "Quantity and Rate" + }, + { + "bold": 1, + "columns": 1, + "default": "1", + "fieldname": "qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Quantity", + "print_width": "60px", + "read_only": 1, + "reqd": 1, + "width": "60px" + }, + { + "fieldname": "column_break_13", + "fieldtype": "Column Break", + "print_hide": 1 + }, + { + "fieldname": "stock_uom", + "fieldtype": "Link", + "label": "Stock UOM", + "options": "UOM", + "print_width": "100px", + "read_only": 1, + "reqd": 1, + "width": "100px" + }, + { + "default": "1", + "fieldname": "conversion_factor", + "fieldtype": "Float", + "hidden": 1, + "label": "Conversion Factor", + "read_only": 1 + }, + { + "fieldname": "section_break_16", + "fieldtype": "Section Break" + }, + { + "bold": 1, + "columns": 2, + "fetch_from": "item_code.standard_rate", + "fetch_if_empty": 1, + "fieldname": "rate", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Rate", + "options": "currency", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "column_break_19", + "fieldtype": "Column Break" + }, + { + "columns": 2, + "fieldname": "amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Amount", + "options": "currency", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "warehouse_section", + "fieldtype": "Section Break", + "label": "Warehouse Details" + }, + { + "fieldname": "warehouse", + "fieldtype": "Link", + "label": "Warehouse", + "options": "Warehouse", + "print_hide": 1, + "reqd": 1 + }, + { + "collapsible": 1, + "fieldname": "accounting_details_section", + "fieldtype": "Section Break", + "label": "Accounting Details" + }, + { + "fieldname": "expense_account", + "fieldtype": "Link", + "label": "Expense Account", + "options": "Account", + "print_hide": 1 + }, + { + "collapsible": 1, + "fieldname": "manufacture_section", + "fieldtype": "Section Break", + "label": "Manufacture" + }, + { + "fieldname": "manufacturer", + "fieldtype": "Link", + "label": "Manufacturer", + "options": "Manufacturer" + }, + { + "fieldname": "manufacturer_part_no", + "fieldtype": "Data", + "label": "Manufacturer Part Number" + }, + { + "depends_on": "item_code", + "fetch_from": "item_code.default_bom", + "fetch_if_empty": 1, + "fieldname": "bom", + "fieldtype": "Link", + "in_list_view": 1, + "label": "BOM", + "options": "BOM", + "print_hide": 1, + "reqd": 1 + }, + { + "default": "0", + "fieldname": "include_exploded_items", + "fieldtype": "Check", + "label": "Include Exploded Items", + "print_hide": 1 + }, + { + "fieldname": "service_cost_per_qty", + "fieldtype": "Currency", + "label": "Service Cost Per Qty", + "read_only": 1, + "reqd": 1 + }, + { + "default": "0", + "fieldname": "additional_cost_per_qty", + "fieldtype": "Currency", + "label": "Additional Cost Per Qty", + "read_only": 1 + }, + { + "fieldname": "rm_cost_per_qty", + "fieldtype": "Currency", + "label": "Raw Material Cost Per Qty", + "no_copy": 1, + "read_only": 1 + }, + { + "allow_on_submit": 1, + "default": "0", + "fieldname": "page_break", + "fieldtype": "Check", + "label": "Page Break", + "no_copy": 1, + "print_hide": 1 + }, + { + "fieldname": "section_break_34", + "fieldtype": "Section Break" + }, + { + "depends_on": "received_qty", + "fieldname": "received_qty", + "fieldtype": "Float", + "label": "Received Qty", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, + { + "depends_on": "returned_qty", + "fieldname": "returned_qty", + "fieldtype": "Float", + "label": "Returned Qty", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, + { + "collapsible": 1, + "fieldname": "accounting_dimensions_section", + "fieldtype": "Section Break", + "label": "Accounting Dimensions" + }, + { + "fieldname": "cost_center", + "fieldtype": "Link", + "label": "Cost Center", + "options": "Cost Center" + }, + { + "fieldname": "dimension_col_break", + "fieldtype": "Column Break" + }, + { + "fieldname": "project", + "fieldtype": "Link", + "label": "Project", + "options": "Project" + } + ], + "idx": 1, + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2023-01-20 23:25:45.363281", + "modified_by": "Administrator", + "module": "Subcontracting", + "name": "Subcontracting Order Item", + "naming_rule": "Random", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "search_fields": "item_name", + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/subcontracting/doctype/subcontracting_order_supplied_item/subcontracting_order_supplied_item.json b/erpnext/subcontracting/doctype/subcontracting_order_supplied_item/subcontracting_order_supplied_item.json index a206a21ca6..8f7128be9a 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order_supplied_item/subcontracting_order_supplied_item.json +++ b/erpnext/subcontracting/doctype/subcontracting_order_supplied_item/subcontracting_order_supplied_item.json @@ -150,8 +150,7 @@ "label": "Returned Qty", "no_copy": 1, "print_hide": 1, - "read_only": 1, - "hidden": 1 + "read_only": 1 }, { "fieldname": "total_supplied_qty", @@ -166,7 +165,7 @@ "hide_toolbar": 1, "istable": 1, "links": [], - "modified": "2022-04-07 12:58:28.208847", + "modified": "2022-08-26 16:04:56.125951", "modified_by": "Administrator", "module": "Subcontracting", "name": "Subcontracting Order Supplied Item", diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js index b2506cd143..3a2c53f4e4 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js @@ -3,6 +3,8 @@ frappe.provide('erpnext.buying'); +{% include 'erpnext/stock/landed_taxes_and_charges_common.js' %}; + frappe.ui.form.on('Subcontracting Receipt', { setup: (frm) => { frm.get_field('supplied_items').grid.cannot_add_rows = true; @@ -48,11 +50,48 @@ frappe.ui.form.on('Subcontracting Receipt', { is_group: 0 } })); + + frm.set_query('expense_account', 'items', function () { + return { + query: 'erpnext.controllers.queries.get_expense_account', + filters: { 'company': frm.doc.company } + }; + }); + + frm.set_query('batch_no', 'items', function(doc, cdt, cdn) { + var row = locals[cdt][cdn]; + return { + filters: { + item: row.item_code + } + } + }); + + let batch_no_field = frm.get_docfield("items", "batch_no"); + if (batch_no_field) { + batch_no_field.get_route_options_for_new_doc = function(row) { + return { + "item": row.doc.item_code + } + }; + } + + frappe.db.get_single_value('Buying Settings', 'backflush_raw_materials_of_subcontract_based_on').then(val => { + if (val == 'Material Transferred for Subcontract') { + frm.fields_dict['supplied_items'].grid.grid_rows.forEach((grid_row) => { + grid_row.docfields.forEach((df) => { + if (df.fieldname == 'consumed_qty') { + df.read_only = 0; + } + }); + }); + } + }); }, refresh: (frm) => { if (frm.doc.docstatus > 0) { - frm.add_custom_button(__("Stock Ledger"), function () { + frm.add_custom_button(__('Stock Ledger'), function () { frappe.route_options = { voucher_no: frm.doc.name, from_date: frm.doc.posting_date, @@ -60,8 +99,8 @@ frappe.ui.form.on('Subcontracting Receipt', { company: frm.doc.company, show_cancelled_entries: frm.doc.docstatus === 2 }; - frappe.set_route("query-report", "Stock Ledger"); - }, __("View")); + frappe.set_route('query-report', 'Stock Ledger'); + }, __('View')); frm.add_custom_button(__('Accounting Ledger'), function () { frappe.route_options = { @@ -69,11 +108,11 @@ frappe.ui.form.on('Subcontracting Receipt', { from_date: frm.doc.posting_date, to_date: moment(frm.doc.modified).format('YYYY-MM-DD'), company: frm.doc.company, - group_by: "Group by Voucher (Consolidated)", + group_by: 'Group by Voucher (Consolidated)', show_cancelled_entries: frm.doc.docstatus === 2 }; - frappe.set_route("query-report", "General Ledger"); - }, __("View")); + frappe.set_route('query-report', 'General Ledger'); + }, __('View')); } if (!frm.doc.is_return && frm.doc.docstatus == 1 && frm.doc.per_returned < 100) { @@ -90,25 +129,25 @@ frappe.ui.form.on('Subcontracting Receipt', { frm.add_custom_button(__('Subcontracting Order'), function () { if (!frm.doc.supplier) { frappe.throw({ - title: __("Mandatory"), - message: __("Please Select a Supplier") + title: __('Mandatory'), + message: __('Please Select a Supplier') }); } erpnext.utils.map_current_doc({ method: 'erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order.make_subcontracting_receipt', - source_doctype: "Subcontracting Order", + source_doctype: 'Subcontracting Order', target: frm, setters: { supplier: frm.doc.supplier, }, get_query_filters: { docstatus: 1, - per_received: ["<", 100], + per_received: ['<', 100], company: frm.doc.company } }); - }, __("Get Items From")); + }, __('Get Items From')); } }, @@ -121,6 +160,16 @@ frappe.ui.form.on('Subcontracting Receipt', { }, }); +frappe.ui.form.on('Landed Cost Taxes and Charges', { + amount: function (frm, cdt, cdn) { + frm.events.set_base_amount(frm, cdt, cdn); + }, + + expense_account: function (frm, cdt, cdn) { + frm.events.set_account_currency(frm, cdt, cdn); + } +}); + frappe.ui.form.on('Subcontracting Receipt Item', { item_code(frm) { set_missing_values(frm); diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json index e9638144a7..3385eac052 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json @@ -1,645 +1,701 @@ { - "actions": [], - "autoname": "naming_series:", - "creation": "2022-04-18 11:20:44.226738", - "doctype": "DocType", - "document_type": "Document", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "title", - "naming_series", - "supplier", - "supplier_name", - "column_break1", - "company", - "posting_date", - "posting_time", - "is_return", - "return_against", - "section_addresses", - "supplier_address", - "contact_person", - "address_display", - "contact_display", - "contact_mobile", - "contact_email", - "col_break_address", - "shipping_address", - "shipping_address_display", - "billing_address", - "billing_address_display", - "sec_warehouse", - "set_warehouse", - "rejected_warehouse", - "col_break_warehouse", - "supplier_warehouse", - "items_section", - "items", - "section_break0", - "total_qty", - "column_break_27", - "total", - "raw_material_details", - "get_current_stock", - "supplied_items", - "section_break_46", - "in_words", - "bill_no", - "bill_date", - "accounting_details_section", - "provisional_expense_account", - "more_info", - "status", - "column_break_39", - "per_returned", - "section_break_47", - "amended_from", - "range", - "column_break4", - "represents_company", - "subscription_detail", - "auto_repeat", - "printing_settings", - "letter_head", - "language", - "instructions", - "column_break_97", - "select_print_heading", - "other_details", - "remarks", - "transporter_info", - "transporter_name", - "column_break5", - "lr_no", - "lr_date" - ], - "fields": [ - { - "allow_on_submit": 1, - "default": "{supplier_name}", - "fieldname": "title", - "fieldtype": "Data", - "hidden": 1, - "label": "Title", - "no_copy": 1, - "print_hide": 1 - }, - { - "fieldname": "naming_series", - "fieldtype": "Select", - "label": "Series", - "no_copy": 1, - "options": "MAT-SCR-.YYYY.-\nMAT-SCR-RET-.YYYY.-", - "print_hide": 1, - "reqd": 1, - "set_only_once": 1 - }, - { - "bold": 1, - "fieldname": "supplier", - "fieldtype": "Link", - "in_global_search": 1, - "label": "Supplier", - "options": "Supplier", - "print_hide": 1, - "print_width": "150px", - "reqd": 1, - "search_index": 1, - "width": "150px" - }, - { - "bold": 1, - "depends_on": "supplier", - "fetch_from": "supplier.supplier_name", - "fieldname": "supplier_name", - "fieldtype": "Data", - "in_global_search": 1, - "label": "Supplier Name", - "read_only": 1 - }, - { - "fieldname": "column_break1", - "fieldtype": "Column Break", - "print_width": "50%", - "width": "50%" - }, - { - "default": "Today", - "fieldname": "posting_date", - "fieldtype": "Date", - "in_list_view": 1, - "label": "Date", - "no_copy": 1, - "print_width": "100px", - "reqd": 1, - "search_index": 1, - "width": "100px" - }, - { - "description": "Time at which materials were received", - "fieldname": "posting_time", - "fieldtype": "Time", - "label": "Posting Time", - "no_copy": 1, - "print_hide": 1, - "print_width": "100px", - "reqd": 1, - "width": "100px" - }, - { - "fieldname": "company", - "fieldtype": "Link", - "in_standard_filter": 1, - "label": "Company", - "options": "Company", - "print_hide": 1, - "print_width": "150px", - "remember_last_selected_value": 1, - "reqd": 1, - "width": "150px" - }, - { - "collapsible": 1, - "fieldname": "section_addresses", - "fieldtype": "Section Break", - "label": "Address and Contact" - }, - { - "fieldname": "supplier_address", - "fieldtype": "Link", - "label": "Select Supplier Address", - "options": "Address", - "print_hide": 1 - }, - { - "fieldname": "contact_person", - "fieldtype": "Link", - "label": "Contact Person", - "options": "Contact", - "print_hide": 1 - }, - { - "fieldname": "address_display", - "fieldtype": "Small Text", - "label": "Address", - "read_only": 1 - }, - { - "fieldname": "contact_display", - "fieldtype": "Small Text", - "in_global_search": 1, - "label": "Contact", - "read_only": 1 - }, - { - "fieldname": "contact_mobile", - "fieldtype": "Small Text", - "label": "Mobile No", - "read_only": 1 - }, - { - "fieldname": "contact_email", - "fieldtype": "Small Text", - "label": "Contact Email", - "options": "Email", - "print_hide": 1, - "read_only": 1 - }, - { - "fieldname": "col_break_address", - "fieldtype": "Column Break" - }, - { - "fieldname": "shipping_address", - "fieldtype": "Link", - "label": "Select Shipping Address", - "options": "Address", - "print_hide": 1 - }, - { - "fieldname": "shipping_address_display", - "fieldtype": "Small Text", - "label": "Shipping Address", - "print_hide": 1, - "read_only": 1 - }, - { - "fieldname": "sec_warehouse", - "fieldtype": "Section Break" - }, - { - "description": "Sets 'Accepted Warehouse' in each row of the Items table.", - "fieldname": "set_warehouse", - "fieldtype": "Link", - "label": "Accepted Warehouse", - "options": "Warehouse", - "print_hide": 1 - }, - { - "description": "Sets 'Rejected Warehouse' in each row of the Items table.", - "fieldname": "rejected_warehouse", - "fieldtype": "Link", - "label": "Rejected Warehouse", - "no_copy": 1, - "options": "Warehouse", - "print_hide": 1 - }, - { - "fieldname": "col_break_warehouse", - "fieldtype": "Column Break" - }, - { - "fieldname": "supplier_warehouse", - "fieldtype": "Link", - "label": "Supplier Warehouse", - "no_copy": 1, - "options": "Warehouse", - "print_hide": 1, - "print_width": "50px", - "width": "50px" - }, - { - "fieldname": "items_section", - "fieldtype": "Section Break", - "options": "fa fa-shopping-cart" - }, - { - "allow_bulk_edit": 1, - "fieldname": "items", - "fieldtype": "Table", - "label": "Items", - "options": "Subcontracting Receipt Item", - "reqd": 1 - }, - { - "depends_on": "supplied_items", - "fieldname": "get_current_stock", - "fieldtype": "Button", - "label": "Get Current Stock", - "options": "get_current_stock", - "print_hide": 1 - }, - { - "collapsible": 1, - "collapsible_depends_on": "supplied_items", - "depends_on": "supplied_items", - "fieldname": "raw_material_details", - "fieldtype": "Section Break", - "label": "Raw Materials Consumed", - "options": "fa fa-table", - "print_hide": 1, - "read_only": 1 - }, - { - "fieldname": "supplied_items", - "fieldtype": "Table", - "label": "Consumed Items", - "no_copy": 1, - "options": "Subcontracting Receipt Supplied Item", - "print_hide": 1 - }, - { - "fieldname": "section_break0", - "fieldtype": "Section Break" - }, - { - "fieldname": "total_qty", - "fieldtype": "Float", - "label": "Total Quantity", - "read_only": 1 - }, - { - "fieldname": "column_break_27", - "fieldtype": "Column Break" - }, - { - "fieldname": "total", - "fieldtype": "Currency", - "label": "Total", - "options": "currency", - "read_only": 1 - }, - { - "fieldname": "section_break_46", - "fieldtype": "Section Break" - }, - { - "fieldname": "in_words", - "fieldtype": "Data", - "label": "In Words", - "length": 240, - "print_hide": 1, - "read_only": 1 - }, - { - "fieldname": "bill_no", - "fieldtype": "Data", - "hidden": 1, - "label": "Bill No", - "print_hide": 1 - }, - { - "fieldname": "bill_date", - "fieldtype": "Date", - "hidden": 1, - "label": "Bill Date", - "print_hide": 1 - }, - { - "collapsible": 1, - "fieldname": "more_info", - "fieldtype": "Section Break", - "label": "More Information", - "options": "fa fa-file-text" - }, - { - "default": "Draft", - "fieldname": "status", - "fieldtype": "Select", - "in_standard_filter": 1, - "label": "Status", - "no_copy": 1, - "options": "\nDraft\nCompleted\nReturn\nReturn Issued\nCancelled", - "print_hide": 1, - "print_width": "150px", - "read_only": 1, - "reqd": 1, - "search_index": 1, - "width": "150px" - }, - { - "fieldname": "amended_from", - "fieldtype": "Link", - "hidden": 1, - "ignore_user_permissions": 1, - "label": "Amended From", - "no_copy": 1, - "options": "Subcontracting Receipt", - "print_hide": 1, - "print_width": "150px", - "read_only": 1, - "width": "150px" - }, - { - "fieldname": "range", - "fieldtype": "Data", - "hidden": 1, - "label": "Range", - "print_hide": 1 - }, - { - "fieldname": "column_break4", - "fieldtype": "Column Break", - "print_hide": 1, - "print_width": "50%", - "width": "50%" - }, - { - "fieldname": "subscription_detail", - "fieldtype": "Section Break", - "label": "Auto Repeat Detail" - }, - { - "fieldname": "auto_repeat", - "fieldtype": "Link", - "label": "Auto Repeat", - "no_copy": 1, - "options": "Auto Repeat", - "print_hide": 1, - "read_only": 1 - }, - { - "collapsible": 1, - "fieldname": "printing_settings", - "fieldtype": "Section Break", - "label": "Printing Settings" - }, - { - "allow_on_submit": 1, - "fieldname": "letter_head", - "fieldtype": "Link", - "label": "Letter Head", - "options": "Letter Head", - "print_hide": 1 - }, - { - "allow_on_submit": 1, - "fieldname": "select_print_heading", - "fieldtype": "Link", - "label": "Print Heading", - "no_copy": 1, - "options": "Print Heading", - "print_hide": 1, - "report_hide": 1 - }, - { - "fieldname": "language", - "fieldtype": "Data", - "label": "Print Language", - "read_only": 1 - }, - { - "fieldname": "column_break_97", - "fieldtype": "Column Break" - }, - { - "fieldname": "other_details", - "fieldtype": "HTML", - "hidden": 1, - "label": "Other Details", - "options": "
Other Details
", - "print_hide": 1, - "print_width": "30%", - "width": "30%" - }, - { - "fieldname": "instructions", - "fieldtype": "Small Text", - "label": "Instructions" - }, - { - "fieldname": "remarks", - "fieldtype": "Small Text", - "label": "Remarks", - "print_hide": 1 - }, - { - "collapsible": 1, - "collapsible_depends_on": "transporter_name", - "fieldname": "transporter_info", - "fieldtype": "Section Break", - "label": "Transporter Details", - "options": "fa fa-truck" - }, - { - "fieldname": "transporter_name", - "fieldtype": "Data", - "label": "Transporter Name" - }, - { - "fieldname": "column_break5", - "fieldtype": "Column Break", - "print_width": "50%", - "width": "50%" - }, - { - "fieldname": "lr_no", - "fieldtype": "Data", - "label": "Vehicle Number", - "no_copy": 1, - "print_width": "100px", - "width": "100px" - }, - { - "fieldname": "lr_date", - "fieldtype": "Date", - "label": "Vehicle Date", - "no_copy": 1, - "print_width": "100px", - "width": "100px" - }, - { - "fieldname": "billing_address", - "fieldtype": "Link", - "label": "Select Billing Address", - "options": "Address" - }, - { - "fieldname": "billing_address_display", - "fieldtype": "Small Text", - "label": "Billing Address", - "read_only": 1 - }, - { - "fetch_from": "supplier.represents_company", - "fieldname": "represents_company", - "fieldtype": "Link", - "ignore_user_permissions": 1, - "label": "Represents Company", - "options": "Company", - "read_only": 1 - }, - { - "collapsible": 1, - "fieldname": "accounting_details_section", - "fieldtype": "Section Break", - "label": "Accounting Details" - }, - { - "fieldname": "provisional_expense_account", - "fieldtype": "Link", - "hidden": 1, - "label": "Provisional Expense Account", - "options": "Account" - }, - { - "default": "0", - "fieldname": "is_return", - "fieldtype": "Check", - "label": "Is Return", - "no_copy": 1, - "print_hide": 1, - "read_only": 1 - }, - { - "depends_on": "is_return", - "fieldname": "return_against", - "fieldtype": "Link", - "label": "Return Against Subcontracting Receipt", - "no_copy": 1, - "options": "Subcontracting Receipt", - "print_hide": 1, - "read_only": 1 - }, - { - "fieldname": "column_break_39", - "fieldtype": "Column Break" - }, - { - "depends_on": "eval:(!doc.__islocal && doc.is_return==0)", - "fieldname": "per_returned", - "fieldtype": "Percent", - "in_list_view": 1, - "label": "% Returned", - "no_copy": 1, - "print_hide": 1, - "read_only": 1 - }, - { - "fieldname": "section_break_47", - "fieldtype": "Section Break" - } - ], - "is_submittable": 1, - "links": [], - "modified": "2022-04-18 13:15:12.011682", - "modified_by": "Administrator", - "module": "Subcontracting", - "name": "Subcontracting Receipt", - "naming_rule": "By \"Naming Series\" field", - "owner": "Administrator", - "permissions": [ - { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Stock Manager", - "share": 1, - "submit": 1, - "write": 1 - }, - { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Stock User", - "share": 1, - "submit": 1, - "write": 1 - }, - { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Purchase User", - "share": 1, - "submit": 1, - "write": 1 - }, - { - "read": 1, - "report": 1, - "role": "Accounts User" - }, - { - "permlevel": 1, - "read": 1, - "role": "Stock Manager", - "write": 1 - } - ], - "search_fields": "status, posting_date, supplier", - "show_name_in_global_search": 1, - "sort_field": "modified", - "sort_order": "DESC", - "states": [], - "timeline_field": "supplier", - "title_field": "title", - "track_changes": 1 + "actions": [], + "autoname": "naming_series:", + "creation": "2022-04-18 11:20:44.226738", + "doctype": "DocType", + "document_type": "Document", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "title", + "naming_series", + "supplier", + "supplier_name", + "column_break1", + "company", + "posting_date", + "posting_time", + "set_posting_time", + "is_return", + "return_against", + "accounting_dimensions_section", + "cost_center", + "dimension_col_break", + "project", + "section_addresses", + "supplier_address", + "contact_person", + "address_display", + "contact_display", + "contact_mobile", + "contact_email", + "col_break_address", + "shipping_address", + "shipping_address_display", + "billing_address", + "billing_address_display", + "sec_warehouse", + "set_warehouse", + "rejected_warehouse", + "col_break_warehouse", + "supplier_warehouse", + "items_section", + "items", + "section_break0", + "total_qty", + "column_break_27", + "total", + "raw_material_details", + "get_current_stock", + "supplied_items", + "additional_costs_section", + "distribute_additional_costs_based_on", + "additional_costs", + "total_additional_costs", + "section_break_46", + "in_words", + "bill_no", + "bill_date", + "more_info", + "status", + "column_break_39", + "per_returned", + "section_break_47", + "amended_from", + "range", + "column_break4", + "represents_company", + "subscription_detail", + "auto_repeat", + "printing_settings", + "letter_head", + "language", + "instructions", + "column_break_97", + "select_print_heading", + "other_details", + "remarks", + "transporter_info", + "transporter_name", + "column_break5", + "lr_no", + "lr_date" + ], + "fields": [ + { + "allow_on_submit": 1, + "default": "{supplier_name}", + "fieldname": "title", + "fieldtype": "Data", + "hidden": 1, + "label": "Title", + "no_copy": 1, + "print_hide": 1 + }, + { + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Series", + "no_copy": 1, + "options": "MAT-SCR-.YYYY.-\nMAT-SCR-RET-.YYYY.-", + "print_hide": 1, + "reqd": 1, + "set_only_once": 1 + }, + { + "bold": 1, + "fieldname": "supplier", + "fieldtype": "Link", + "in_global_search": 1, + "label": "Supplier", + "options": "Supplier", + "print_hide": 1, + "print_width": "150px", + "reqd": 1, + "search_index": 1, + "width": "150px" + }, + { + "bold": 1, + "depends_on": "supplier", + "fetch_from": "supplier.supplier_name", + "fieldname": "supplier_name", + "fieldtype": "Data", + "in_global_search": 1, + "label": "Supplier Name", + "read_only": 1 + }, + { + "fieldname": "column_break1", + "fieldtype": "Column Break", + "print_width": "50%", + "width": "50%" + }, + { + "default": "Today", + "fieldname": "posting_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Date", + "no_copy": 1, + "print_width": "100px", + "read_only_depends_on": "eval: !doc.set_posting_time", + "reqd": 1, + "search_index": 1, + "width": "100px" + }, + { + "description": "Time at which materials were received", + "fieldname": "posting_time", + "fieldtype": "Time", + "label": "Posting Time", + "no_copy": 1, + "print_hide": 1, + "print_width": "100px", + "read_only_depends_on": "eval: !doc.set_posting_time", + "reqd": 1, + "width": "100px" + }, + { + "fieldname": "company", + "fieldtype": "Link", + "in_standard_filter": 1, + "label": "Company", + "options": "Company", + "print_hide": 1, + "print_width": "150px", + "remember_last_selected_value": 1, + "reqd": 1, + "width": "150px" + }, + { + "collapsible": 1, + "fieldname": "section_addresses", + "fieldtype": "Section Break", + "label": "Address and Contact" + }, + { + "fieldname": "supplier_address", + "fieldtype": "Link", + "label": "Select Supplier Address", + "options": "Address", + "print_hide": 1 + }, + { + "fieldname": "contact_person", + "fieldtype": "Link", + "label": "Contact Person", + "options": "Contact", + "print_hide": 1 + }, + { + "fieldname": "address_display", + "fieldtype": "Small Text", + "label": "Address", + "read_only": 1 + }, + { + "fieldname": "contact_display", + "fieldtype": "Small Text", + "in_global_search": 1, + "label": "Contact", + "read_only": 1 + }, + { + "fieldname": "contact_mobile", + "fieldtype": "Small Text", + "label": "Mobile No", + "read_only": 1 + }, + { + "fieldname": "contact_email", + "fieldtype": "Small Text", + "label": "Contact Email", + "options": "Email", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "col_break_address", + "fieldtype": "Column Break" + }, + { + "fieldname": "shipping_address", + "fieldtype": "Link", + "label": "Select Shipping Address", + "options": "Address", + "print_hide": 1 + }, + { + "fieldname": "shipping_address_display", + "fieldtype": "Small Text", + "label": "Shipping Address", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "sec_warehouse", + "fieldtype": "Section Break" + }, + { + "description": "Sets 'Accepted Warehouse' in each row of the Items table.", + "fieldname": "set_warehouse", + "fieldtype": "Link", + "label": "Accepted Warehouse", + "options": "Warehouse", + "print_hide": 1 + }, + { + "depends_on": "eval: !doc.is_return", + "description": "Sets 'Rejected Warehouse' in each row of the Items table.", + "fieldname": "rejected_warehouse", + "fieldtype": "Link", + "label": "Rejected Warehouse", + "no_copy": 1, + "options": "Warehouse", + "print_hide": 1 + }, + { + "fieldname": "col_break_warehouse", + "fieldtype": "Column Break" + }, + { + "fieldname": "supplier_warehouse", + "fieldtype": "Link", + "label": "Supplier Warehouse", + "no_copy": 1, + "options": "Warehouse", + "print_hide": 1, + "print_width": "50px", + "width": "50px" + }, + { + "fieldname": "items_section", + "fieldtype": "Section Break", + "options": "fa fa-shopping-cart" + }, + { + "allow_bulk_edit": 1, + "fieldname": "items", + "fieldtype": "Table", + "label": "Items", + "options": "Subcontracting Receipt Item", + "reqd": 1 + }, + { + "depends_on": "supplied_items", + "fieldname": "get_current_stock", + "fieldtype": "Button", + "label": "Get Current Stock", + "options": "get_current_stock", + "print_hide": 1 + }, + { + "collapsible": 1, + "collapsible_depends_on": "supplied_items", + "depends_on": "supplied_items", + "fieldname": "raw_material_details", + "fieldtype": "Section Break", + "label": "Raw Materials Consumed", + "options": "fa fa-table", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "supplied_items", + "fieldtype": "Table", + "label": "Consumed Items", + "no_copy": 1, + "options": "Subcontracting Receipt Supplied Item", + "print_hide": 1 + }, + { + "fieldname": "section_break0", + "fieldtype": "Section Break" + }, + { + "fieldname": "total_qty", + "fieldtype": "Float", + "label": "Total Quantity", + "read_only": 1 + }, + { + "fieldname": "column_break_27", + "fieldtype": "Column Break" + }, + { + "fieldname": "total", + "fieldtype": "Currency", + "label": "Total", + "options": "currency", + "read_only": 1 + }, + { + "fieldname": "section_break_46", + "fieldtype": "Section Break" + }, + { + "fieldname": "in_words", + "fieldtype": "Data", + "label": "In Words", + "length": 240, + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "bill_no", + "fieldtype": "Data", + "hidden": 1, + "label": "Bill No", + "print_hide": 1 + }, + { + "fieldname": "bill_date", + "fieldtype": "Date", + "hidden": 1, + "label": "Bill Date", + "print_hide": 1 + }, + { + "collapsible": 1, + "fieldname": "more_info", + "fieldtype": "Section Break", + "label": "More Information", + "options": "fa fa-file-text" + }, + { + "default": "Draft", + "fieldname": "status", + "fieldtype": "Select", + "in_standard_filter": 1, + "label": "Status", + "no_copy": 1, + "options": "\nDraft\nCompleted\nReturn\nReturn Issued\nCancelled\nClosed", + "print_hide": 1, + "print_width": "150px", + "read_only": 1, + "reqd": 1, + "search_index": 1, + "width": "150px" + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "hidden": 1, + "ignore_user_permissions": 1, + "label": "Amended From", + "no_copy": 1, + "options": "Subcontracting Receipt", + "print_hide": 1, + "print_width": "150px", + "read_only": 1, + "width": "150px" + }, + { + "fieldname": "range", + "fieldtype": "Data", + "hidden": 1, + "label": "Range", + "print_hide": 1 + }, + { + "fieldname": "column_break4", + "fieldtype": "Column Break", + "print_hide": 1, + "print_width": "50%", + "width": "50%" + }, + { + "fieldname": "subscription_detail", + "fieldtype": "Section Break", + "label": "Auto Repeat Detail" + }, + { + "fieldname": "auto_repeat", + "fieldtype": "Link", + "label": "Auto Repeat", + "no_copy": 1, + "options": "Auto Repeat", + "print_hide": 1, + "read_only": 1 + }, + { + "collapsible": 1, + "fieldname": "printing_settings", + "fieldtype": "Section Break", + "label": "Printing Settings" + }, + { + "allow_on_submit": 1, + "fieldname": "letter_head", + "fieldtype": "Link", + "label": "Letter Head", + "options": "Letter Head", + "print_hide": 1 + }, + { + "allow_on_submit": 1, + "fieldname": "select_print_heading", + "fieldtype": "Link", + "label": "Print Heading", + "no_copy": 1, + "options": "Print Heading", + "print_hide": 1, + "report_hide": 1 + }, + { + "fieldname": "language", + "fieldtype": "Data", + "label": "Print Language", + "read_only": 1 + }, + { + "fieldname": "column_break_97", + "fieldtype": "Column Break" + }, + { + "fieldname": "other_details", + "fieldtype": "HTML", + "hidden": 1, + "label": "Other Details", + "options": "
Other Details
", + "print_hide": 1, + "print_width": "30%", + "width": "30%" + }, + { + "fieldname": "instructions", + "fieldtype": "Small Text", + "label": "Instructions" + }, + { + "fieldname": "remarks", + "fieldtype": "Small Text", + "label": "Remarks", + "print_hide": 1 + }, + { + "collapsible": 1, + "collapsible_depends_on": "transporter_name", + "fieldname": "transporter_info", + "fieldtype": "Section Break", + "label": "Transporter Details", + "options": "fa fa-truck" + }, + { + "fieldname": "transporter_name", + "fieldtype": "Data", + "label": "Transporter Name" + }, + { + "fieldname": "column_break5", + "fieldtype": "Column Break", + "print_width": "50%", + "width": "50%" + }, + { + "fieldname": "lr_no", + "fieldtype": "Data", + "label": "Vehicle Number", + "no_copy": 1, + "print_width": "100px", + "width": "100px" + }, + { + "fieldname": "lr_date", + "fieldtype": "Date", + "label": "Vehicle Date", + "no_copy": 1, + "print_width": "100px", + "width": "100px" + }, + { + "fieldname": "billing_address", + "fieldtype": "Link", + "label": "Select Billing Address", + "options": "Address" + }, + { + "fieldname": "billing_address_display", + "fieldtype": "Small Text", + "label": "Billing Address", + "read_only": 1 + }, + { + "fetch_from": "supplier.represents_company", + "fieldname": "represents_company", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "label": "Represents Company", + "options": "Company", + "read_only": 1 + }, + { + "default": "0", + "fieldname": "is_return", + "fieldtype": "Check", + "label": "Is Return", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, + { + "depends_on": "is_return", + "fieldname": "return_against", + "fieldtype": "Link", + "label": "Return Against Subcontracting Receipt", + "no_copy": 1, + "options": "Subcontracting Receipt", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "column_break_39", + "fieldtype": "Column Break" + }, + { + "depends_on": "eval:(!doc.__islocal && doc.is_return==0)", + "fieldname": "per_returned", + "fieldtype": "Percent", + "in_list_view": 1, + "label": "% Returned", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "section_break_47", + "fieldtype": "Section Break" + }, + { + "collapsible": 1, + "fieldname": "accounting_dimensions_section", + "fieldtype": "Section Break", + "label": "Accounting Dimensions " + }, + { + "fieldname": "cost_center", + "fieldtype": "Link", + "label": "Cost Center", + "options": "Cost Center" + }, + { + "fieldname": "dimension_col_break", + "fieldtype": "Column Break" + }, + { + "fieldname": "project", + "fieldtype": "Link", + "label": "Project", + "options": "Project" + }, + { + "collapsible": 1, + "collapsible_depends_on": "total_additional_costs", + "depends_on": "eval:(doc.docstatus == 0 || doc.total_additional_costs)", + "fieldname": "additional_costs_section", + "fieldtype": "Section Break", + "label": "Additional Costs" + }, + { + "default": "Qty", + "fieldname": "distribute_additional_costs_based_on", + "fieldtype": "Select", + "label": "Distribute Additional Costs Based On ", + "options": "Qty\nAmount" + }, + { + "fieldname": "additional_costs", + "fieldtype": "Table", + "label": "Additional Costs", + "options": "Landed Cost Taxes and Charges" + }, + { + "fieldname": "total_additional_costs", + "fieldtype": "Currency", + "label": "Total Additional Costs", + "print_hide_if_no_value": 1, + "read_only": 1 + }, + { + "default": "0", + "depends_on": "eval:doc.docstatus==0", + "fieldname": "set_posting_time", + "fieldtype": "Check", + "label": "Edit Posting Date and Time", + "print_hide": 1 + } + ], + "in_create": 1, + "is_submittable": 1, + "links": [], + "modified": "2022-11-16 14:18:57.001239", + "modified_by": "Administrator", + "module": "Subcontracting", + "name": "Subcontracting Receipt", + "naming_rule": "By \"Naming Series\" field", + "owner": "Administrator", + "permissions": [ + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Stock Manager", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Stock User", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Purchase User", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "read": 1, + "report": 1, + "role": "Accounts User" + }, + { + "permlevel": 1, + "read": 1, + "role": "Stock Manager", + "write": 1 + } + ], + "search_fields": "status, posting_date, supplier", + "show_name_in_global_search": 1, + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "timeline_field": "supplier", + "title_field": "title", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index 0c4ec6fb76..f4fd4de169 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -3,8 +3,10 @@ import frappe from frappe import _ -from frappe.utils import cint, getdate, nowdate +from frappe.utils import cint, flt, getdate, nowdate +import erpnext +from erpnext.accounts.utils import get_account_currency from erpnext.controllers.subcontracting_controller import SubcontractingController @@ -55,10 +57,17 @@ class SubcontractingReceipt(SubcontractingController): def before_validate(self): super(SubcontractingReceipt, self).before_validate() + self.validate_items_qty() + self.set_items_bom() self.set_items_cost_center() self.set_items_expense_account() def validate(self): + if ( + frappe.db.get_single_value("Buying Settings", "backflush_raw_materials_of_subcontract_based_on") + == "BOM" + ): + self.supplied_items = [] super(SubcontractingReceipt, self).validate() self.set_missing_values() self.validate_posting_time() @@ -75,6 +84,7 @@ class SubcontractingReceipt(SubcontractingController): self.get_current_stock() def on_submit(self): + self.validate_available_qty_for_consumption() self.update_status_updater_args() self.update_prevdoc_status() self.set_subcontracting_order_status() @@ -103,13 +113,46 @@ class SubcontractingReceipt(SubcontractingController): @frappe.whitelist() def set_missing_values(self): + self.set_missing_values_in_additional_costs() self.set_missing_values_in_supplied_items() self.set_missing_values_in_items() + def set_available_qty_for_consumption(self): + supplied_items_details = {} + + sco_supplied_item = frappe.qb.DocType("Subcontracting Order Supplied Item") + for item in self.get("items"): + supplied_items = ( + frappe.qb.from_(sco_supplied_item) + .select( + sco_supplied_item.rm_item_code, + sco_supplied_item.reference_name, + (sco_supplied_item.total_supplied_qty - sco_supplied_item.consumed_qty).as_("available_qty"), + ) + .where( + (sco_supplied_item.parent == item.subcontracting_order) + & (sco_supplied_item.main_item_code == item.item_code) + & (sco_supplied_item.reference_name == item.subcontracting_order_item) + ) + ).run(as_dict=True) + + if supplied_items: + supplied_items_details[item.name] = {} + + for supplied_item in supplied_items: + supplied_items_details[item.name][supplied_item.rm_item_code] = supplied_item.available_qty + else: + for item in self.get("supplied_items"): + item.available_qty_for_consumption = supplied_items_details.get(item.reference_name, {}).get( + item.rm_item_code, 0 + ) + def set_missing_values_in_supplied_items(self): for item in self.get("supplied_items") or []: item.amount = item.rate * item.consumed_qty + self.set_available_qty_for_consumption() + def set_missing_values_in_items(self): rm_supp_cost = {} for item in self.get("supplied_items") or []: @@ -120,17 +163,17 @@ class SubcontractingReceipt(SubcontractingController): total_qty = total_amount = 0 for item in self.items: - if item.name in rm_supp_cost: + if item.qty and item.name in rm_supp_cost: item.rm_supp_cost = rm_supp_cost[item.name] item.rm_cost_per_qty = item.rm_supp_cost / item.qty rm_supp_cost.pop(item.name) - if self.is_new() and item.rm_supp_cost > 0: + if item.recalculate_rate: item.rate = ( - item.rm_cost_per_qty + (item.service_cost_per_qty or 0) + item.additional_cost_per_qty + flt(item.rm_cost_per_qty) + flt(item.service_cost_per_qty) + flt(item.additional_cost_per_qty) ) - item.received_qty = item.qty + (item.rejected_qty or 0) + item.received_qty = item.qty + flt(item.rejected_qty) item.amount = item.qty * item.rate total_qty += item.qty total_amount += item.amount @@ -146,6 +189,42 @@ class SubcontractingReceipt(SubcontractingController): _("Rejected Warehouse is mandatory against rejected Item {0}").format(item.item_code) ) + def validate_available_qty_for_consumption(self): + for item in self.get("supplied_items"): + if ( + item.available_qty_for_consumption and item.available_qty_for_consumption < item.consumed_qty + ): + frappe.throw( + _( + "Row {0}: Consumed Qty must be less than or equal to Available Qty For Consumption in Consumed Items Table." + ).format(item.idx) + ) + + def validate_items_qty(self): + for item in self.items: + if not (item.qty or item.rejected_qty): + frappe.throw( + _("Row {0}: Accepted Qty and Rejected Qty can't be zero at the same time.").format(item.idx) + ) + + def set_items_bom(self): + if self.is_return: + for item in self.items: + if not item.bom: + item.bom = frappe.db.get_value( + "Subcontracting Receipt Item", + {"name": item.subcontracting_receipt_item, "parent": self.return_against}, + "bom", + ) + else: + for item in self.items: + if not item.bom: + item.bom = frappe.db.get_value( + "Subcontracting Order Item", + {"name": item.subcontracting_order_item, "parent": item.subcontracting_order}, + "bom", + ) + def set_items_cost_center(self): if self.company: cost_center = frappe.get_cached_value("Company", self.company, "cost_center") @@ -180,6 +259,139 @@ class SubcontractingReceipt(SubcontractingController): if status: frappe.db.set_value("Subcontracting Receipt", self.name, "status", status, update_modified) + def get_gl_entries(self, warehouse_account=None): + from erpnext.accounts.general_ledger import process_gl_map + + if not erpnext.is_perpetual_inventory_enabled(self.company): + return [] + + gl_entries = [] + self.make_item_gl_entries(gl_entries, warehouse_account) + + return process_gl_map(gl_entries) + + def make_item_gl_entries(self, gl_entries, warehouse_account=None): + stock_rbnb = self.get_company_default("stock_received_but_not_billed") + expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation") + + warehouse_with_no_account = [] + + for item in self.items: + if flt(item.rate) and flt(item.qty): + if warehouse_account.get(item.warehouse): + stock_value_diff = frappe.db.get_value( + "Stock Ledger Entry", + { + "voucher_type": "Subcontracting Receipt", + "voucher_no": self.name, + "voucher_detail_no": item.name, + "warehouse": item.warehouse, + "is_cancelled": 0, + }, + "stock_value_difference", + ) + + warehouse_account_name = warehouse_account[item.warehouse]["account"] + warehouse_account_currency = warehouse_account[item.warehouse]["account_currency"] + supplier_warehouse_account = warehouse_account.get(self.supplier_warehouse, {}).get("account") + supplier_warehouse_account_currency = warehouse_account.get(self.supplier_warehouse, {}).get( + "account_currency" + ) + remarks = self.get("remarks") or _("Accounting Entry for Stock") + + # FG Warehouse Account (Debit) + self.add_gl_entry( + gl_entries=gl_entries, + account=warehouse_account_name, + cost_center=item.cost_center, + debit=stock_value_diff, + credit=0.0, + remarks=remarks, + against_account=stock_rbnb, + account_currency=warehouse_account_currency, + item=item, + ) + + # Supplier Warehouse Account (Credit) + if flt(item.rm_supp_cost) and warehouse_account.get(self.supplier_warehouse): + self.add_gl_entry( + gl_entries=gl_entries, + account=supplier_warehouse_account, + cost_center=item.cost_center, + debit=0.0, + credit=flt(item.rm_supp_cost), + remarks=remarks, + against_account=warehouse_account_name, + account_currency=supplier_warehouse_account_currency, + item=item, + ) + + # Expense Account (Credit) + if flt(item.service_cost_per_qty): + self.add_gl_entry( + gl_entries=gl_entries, + account=item.expense_account, + cost_center=item.cost_center, + debit=0.0, + credit=flt(item.service_cost_per_qty) * flt(item.qty), + remarks=remarks, + against_account=warehouse_account_name, + account_currency=get_account_currency(item.expense_account), + item=item, + ) + + # Loss Account (Credit) + divisional_loss = flt(item.amount - stock_value_diff, item.precision("amount")) + + if divisional_loss: + if self.is_return: + loss_account = expenses_included_in_valuation + else: + loss_account = item.expense_account + + self.add_gl_entry( + gl_entries=gl_entries, + account=loss_account, + cost_center=item.cost_center, + debit=divisional_loss, + credit=0.0, + remarks=remarks, + against_account=warehouse_account_name, + account_currency=get_account_currency(loss_account), + project=item.project, + item=item, + ) + elif ( + item.warehouse not in warehouse_with_no_account + or item.rejected_warehouse not in warehouse_with_no_account + ): + warehouse_with_no_account.append(item.warehouse) + + # Additional Costs Expense Accounts (Credit) + for row in self.additional_costs: + credit_amount = ( + flt(row.base_amount) + if (row.base_amount or row.account_currency != self.company_currency) + else flt(row.amount) + ) + + self.add_gl_entry( + gl_entries=gl_entries, + account=row.expense_account, + cost_center=self.cost_center or self.get_company_default("cost_center"), + debit=0.0, + credit=credit_amount, + remarks=remarks, + against_account=None, + ) + + if warehouse_with_no_account: + frappe.msgprint( + _("No accounting entries for the following warehouses") + + ": \n" + + "\n".join(warehouse_with_no_account) + ) + @frappe.whitelist() def make_subcontract_return(source_name, target_doc=None): diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt_dashboard.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt_dashboard.py index a9e51937d7..deb8342b83 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt_dashboard.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt_dashboard.py @@ -4,6 +4,9 @@ from frappe import _ def get_data(): return { "fieldname": "subcontracting_receipt_no", + "non_standard_fieldnames": { + "Subcontracting Receipt": "return_against", + }, "internal_links": { "Subcontracting Order": ["items", "subcontracting_order"], "Project": ["items", "project"], @@ -11,5 +14,6 @@ def get_data(): }, "transactions": [ {"label": _("Reference"), "items": ["Subcontracting Order", "Quality Inspection", "Project"]}, + {"label": _("Returns"), "items": ["Subcontracting Receipt"]}, ], } diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py index 763e76882e..72ed4d4e2e 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py @@ -6,8 +6,10 @@ import copy import frappe from frappe.tests.utils import FrappeTestCase -from frappe.utils import flt +from frappe.utils import cint, flt +import erpnext +from erpnext.accounts.doctype.account.test_account import get_inventory_account from erpnext.controllers.sales_and_purchase_return import make_return_doc from erpnext.controllers.tests.test_subcontracting_controller import ( get_rm_items, @@ -22,6 +24,7 @@ from erpnext.controllers.tests.test_subcontracting_controller import ( set_backflush_based_on, ) from erpnext.stock.doctype.item.test_item import make_item +from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import ( make_subcontracting_receipt, @@ -70,6 +73,55 @@ class TestSubcontractingReceipt(FrappeTestCase): rm_supp_cost = sum(item.amount for item in scr.get("supplied_items")) self.assertEqual(scr.get("items")[0].rm_supp_cost, flt(rm_supp_cost)) + def test_available_qty_for_consumption(self): + 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, + ) + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 1", + "qty": 10, + "rate": 100, + "fg_item": "_Test FG Item", + "fg_item_qty": 10, + }, + ] + sco = get_subcontracting_order(service_items=service_items) + rm_items = [ + { + "main_item_code": "_Test FG Item", + "item_code": "_Test Item", + "qty": 5.0, + "rate": 100.0, + "stock_uom": "_Test UOM", + "warehouse": "_Test Warehouse - _TC", + }, + { + "main_item_code": "_Test FG Item", + "item_code": "_Test Item Home Desktop 100", + "qty": 10.0, + "rate": 100.0, + "stock_uom": "_Test UOM", + "warehouse": "_Test Warehouse - _TC", + }, + ] + itemwise_details = make_stock_in_entry(rm_items=rm_items) + make_stock_transfer_entry( + sco_no=sco.name, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) + scr = make_subcontracting_receipt(sco.name) + scr.save() + self.assertRaises(frappe.ValidationError, scr.submit) + def test_subcontracting_gle_fg_item_rate_zero(self): from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries @@ -275,7 +327,7 @@ class TestSubcontractingReceipt(FrappeTestCase): for row in scr.supplied_items: self.assertEqual(transferred_batch.get(row.batch_no), row.consumed_qty) - def test_subcontracting_order_partial_return(self): + def test_subcontracting_receipt_partial_return(self): sco = get_subcontracting_order() rm_items = get_rm_items(sco.supplied_items) itemwise_details = make_stock_in_entry(rm_items=rm_items) @@ -291,15 +343,17 @@ class TestSubcontractingReceipt(FrappeTestCase): scr1_return = make_return_subcontracting_receipt(scr_name=scr1.name, qty=-3) scr1.load_from_db() self.assertEqual(scr1_return.status, "Return") + self.assertIsNotNone(scr1_return.items[0].bom) self.assertEqual(scr1.items[0].returned_qty, 3) scr2_return = make_return_subcontracting_receipt(scr_name=scr1.name, qty=-7) scr1.load_from_db() self.assertEqual(scr2_return.status, "Return") + self.assertIsNotNone(scr2_return.items[0].bom) self.assertEqual(scr1.status, "Return Issued") self.assertEqual(scr1.items[0].returned_qty, 10) - def test_subcontracting_order_over_return(self): + def test_subcontracting_receipt_over_return(self): sco = get_subcontracting_order() rm_items = get_rm_items(sco.supplied_items) itemwise_details = make_stock_in_entry(rm_items=rm_items) @@ -317,6 +371,163 @@ class TestSubcontractingReceipt(FrappeTestCase): args = frappe._dict(scr_name=scr1.name, qty=-15) self.assertRaises(OverAllowanceError, make_return_subcontracting_receipt, **args) + def test_subcontracting_receipt_no_gl_entry(self): + sco = get_subcontracting_order() + rm_items = get_rm_items(sco.supplied_items) + itemwise_details = make_stock_in_entry(rm_items=rm_items) + make_stock_transfer_entry( + sco_no=sco.name, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) + + scr = make_subcontracting_receipt(sco.name) + scr.append( + "additional_costs", + { + "expense_account": "Expenses Included In Valuation - _TC", + "description": "Test Additional Costs", + "amount": 100, + }, + ) + scr.save() + scr.submit() + + stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", + { + "voucher_type": "Subcontracting Receipt", + "voucher_no": scr.name, + "item_code": "Subcontracted Item SA7", + "warehouse": "_Test Warehouse - _TC", + }, + "stock_value_difference", + ) + + # Service Cost(100 * 10) + Raw Materials Cost(100 * 10) + Additional Costs(10 * 10) = 2100 + self.assertEqual(stock_value_difference, 2100) + self.assertFalse(get_gl_entries("Subcontracting Receipt", scr.name)) + + def test_subcontracting_receipt_gl_entry(self): + sco = get_subcontracting_order( + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + supplier_warehouse="Work In Progress - TCP1", + ) + rm_items = get_rm_items(sco.supplied_items) + itemwise_details = make_stock_in_entry(rm_items=rm_items) + make_stock_transfer_entry( + sco_no=sco.name, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) + + scr = make_subcontracting_receipt(sco.name) + additional_costs_expense_account = "Expenses Included In Valuation - TCP1" + scr.append( + "additional_costs", + { + "expense_account": additional_costs_expense_account, + "description": "Test Additional Costs", + "amount": 100, + "base_amount": 100, + }, + ) + scr.save() + scr.submit() + + self.assertEqual(cint(erpnext.is_perpetual_inventory_enabled(scr.company)), 1) + + gl_entries = get_gl_entries("Subcontracting Receipt", scr.name) + + self.assertTrue(gl_entries) + + fg_warehouse_ac = get_inventory_account(scr.company, scr.items[0].warehouse) + supplier_warehouse_ac = get_inventory_account(scr.company, scr.supplier_warehouse) + expense_account = scr.items[0].expense_account + + if fg_warehouse_ac == supplier_warehouse_ac: + expected_values = { + fg_warehouse_ac: [2100.0, 1000.0], # FG Amount (D), RM Cost (C) + expense_account: [0.0, 1000.0], # Service Cost (C) + additional_costs_expense_account: [0.0, 100.0], # Additional Cost (C) + } + else: + expected_values = { + fg_warehouse_ac: [2100.0, 0.0], # FG Amount (D) + supplier_warehouse_ac: [0.0, 1000.0], # RM Cost (C) + expense_account: [0.0, 1000.0], # Service Cost (C) + additional_costs_expense_account: [0.0, 100.0], # Additional Cost (C) + } + + for gle in gl_entries: + self.assertEqual(expected_values[gle.account][0], gle.debit) + self.assertEqual(expected_values[gle.account][1], gle.credit) + + scr.reload() + scr.cancel() + self.assertTrue(get_gl_entries("Subcontracting Receipt", scr.name)) + + def test_supplied_items_consumed_qty(self): + # Set Backflush Based On as "Material Transferred for Subcontracting" to transfer RM's more than the required qty + set_backflush_based_on("Material Transferred for Subcontract") + + # Create Material Receipt for RM's + 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, + ) + + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 1", + "qty": 10, + "rate": 100, + "fg_item": "_Test FG Item", + "fg_item_qty": 10, + }, + ] + + # Create Subcontracting Order + sco = get_subcontracting_order(service_items=service_items) + + # Transfer RM's + rm_items = get_rm_items(sco.supplied_items) + rm_items[0]["qty"] = 20 # Extra 10 Qty + itemwise_details = make_stock_in_entry(rm_items=rm_items) + make_stock_transfer_entry( + sco_no=sco.name, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) + + # Create Subcontracting Receipt + scr = make_subcontracting_receipt(sco.name) + scr.rejected_warehouse = "_Test Warehouse 1 - _TC" + + scr.items[0].qty = 5 # Accepted Qty + scr.items[0].rejected_qty = 3 + scr.save() + + # consumed_qty should be (accepted_qty * (transfered_qty / qty)) = (5 * (20 / 10)) = 10 + self.assertEqual(scr.supplied_items[0].consumed_qty, 10) + + # Set Backflush Based On as "BOM" + set_backflush_based_on("BOM") + + scr.items[0].qty = 6 # Accepted Qty + scr.items[0].rejected_qty = 4 + scr.save() + + # consumed_qty should be (accepted_qty * qty_consumed_per_unit) = (6 * 1) = 6 + self.assertEqual(scr.supplied_items[0].consumed_qty, 6) + def make_return_subcontracting_receipt(**args): args = frappe._dict(args) diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json index e2785ce0cd..4b64e4bafe 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json +++ b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json @@ -1,475 +1,490 @@ { - "actions": [], - "autoname": "hash", - "creation": "2022-04-13 16:05:55.395695", - "doctype": "DocType", - "document_type": "Document", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "item_code", - "column_break_2", - "item_name", - "section_break_4", - "description", - "brand", - "image_column", - "image", - "image_view", - "received_and_accepted", - "received_qty", - "qty", - "rejected_qty", - "returned_qty", - "col_break2", - "stock_uom", - "conversion_factor", - "tracking_section", - "col_break_tracking_section", - "rate_and_amount", - "rate", - "amount", - "column_break_19", - "rm_cost_per_qty", - "service_cost_per_qty", - "additional_cost_per_qty", - "rm_supp_cost", - "warehouse_and_reference", - "warehouse", - "rejected_warehouse", - "subcontracting_order", - "column_break_40", - "schedule_date", - "quality_inspection", - "subcontracting_order_item", - "subcontracting_receipt_item", - "section_break_45", - "bom", - "serial_no", - "col_break5", - "batch_no", - "rejected_serial_no", - "expense_account", - "manufacture_details", - "manufacturer", - "column_break_16", - "manufacturer_part_no", - "accounting_dimensions_section", - "project", - "dimension_col_break", - "cost_center", - "section_break_80", - "page_break" - ], - "fields": [ - { - "bold": 1, - "columns": 3, - "fieldname": "item_code", - "fieldtype": "Link", - "in_global_search": 1, - "in_list_view": 1, - "label": "Item Code", - "options": "Item", - "print_width": "100px", - "reqd": 1, - "search_index": 1, - "width": "100px" - }, - { - "fieldname": "column_break_2", - "fieldtype": "Column Break" - }, - { - "fieldname": "item_name", - "fieldtype": "Data", - "in_global_search": 1, - "label": "Item Name", - "print_hide": 1, - "reqd": 1 - }, - { - "collapsible": 1, - "fieldname": "section_break_4", - "fieldtype": "Section Break", - "label": "Description" - }, - { - "fieldname": "description", - "fieldtype": "Text Editor", - "label": "Description", - "print_width": "300px", - "reqd": 1, - "width": "300px" - }, - { - "fieldname": "image", - "fieldtype": "Attach", - "hidden": 1, - "label": "Image" - }, - { - "fieldname": "image_view", - "fieldtype": "Image", - "label": "Image View", - "options": "image", - "print_hide": 1 - }, - { - "fieldname": "received_and_accepted", - "fieldtype": "Section Break", - "label": "Received and Accepted" - }, - { - "bold": 1, - "default": "0", - "fieldname": "received_qty", - "fieldtype": "Float", - "label": "Received Quantity", - "no_copy": 1, - "print_hide": 1, - "print_width": "100px", - "read_only": 1, - "reqd": 1, - "width": "100px" - }, - { - "columns": 2, - "fieldname": "qty", - "fieldtype": "Float", - "in_list_view": 1, - "label": "Accepted Quantity", - "no_copy": 1, - "print_width": "100px", - "width": "100px" - }, - { - "columns": 1, - "fieldname": "rejected_qty", - "fieldtype": "Float", - "in_list_view": 1, - "label": "Rejected Quantity", - "no_copy": 1, - "print_hide": 1, - "print_width": "100px", - "width": "100px" - }, - { - "fieldname": "col_break2", - "fieldtype": "Column Break", - "print_hide": 1 - }, - { - "fieldname": "stock_uom", - "fieldtype": "Link", - "label": "Stock UOM", - "options": "UOM", - "print_hide": 1, - "print_width": "100px", - "read_only": 1, - "reqd": 1, - "width": "100px" - }, - { - "default": "1", - "fieldname": "conversion_factor", - "fieldtype": "Float", - "hidden": 1, - "label": "Conversion Factor", - "read_only": 1 - }, - { - "fieldname": "rate_and_amount", - "fieldtype": "Section Break", - "label": "Rate and Amount" - }, - { - "bold": 1, - "columns": 2, - "fieldname": "rate", - "fieldtype": "Currency", - "in_list_view": 1, - "label": "Rate", - "options": "currency", - "print_width": "100px", - "width": "100px" - }, - { - "fieldname": "amount", - "fieldtype": "Currency", - "in_list_view": 1, - "label": "Amount", - "options": "currency", - "read_only": 1 - }, - { - "fieldname": "column_break_19", - "fieldtype": "Column Break" - }, - { - "fieldname": "rm_cost_per_qty", - "fieldtype": "Currency", - "label": "Raw Material Cost Per Qty", - "no_copy": 1, - "read_only": 1 - }, - { - "fieldname": "service_cost_per_qty", - "fieldtype": "Currency", - "label": "Service Cost Per Qty", - "read_only": 1, - "reqd": 1 - }, - { - "default": "0", - "fieldname": "additional_cost_per_qty", - "fieldtype": "Currency", - "label": "Additional Cost Per Qty", - "read_only": 1 - }, - { - "fieldname": "warehouse_and_reference", - "fieldtype": "Section Break", - "label": "Warehouse and Reference" - }, - { - "bold": 1, - "fieldname": "warehouse", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Accepted Warehouse", - "options": "Warehouse", - "print_hide": 1, - "print_width": "100px", - "width": "100px" - }, - { - "fieldname": "rejected_warehouse", - "fieldtype": "Link", - "label": "Rejected Warehouse", - "no_copy": 1, - "options": "Warehouse", - "print_hide": 1, - "print_width": "100px", - "width": "100px" - }, - { - "depends_on": "eval:!doc.__islocal", - "fieldname": "quality_inspection", - "fieldtype": "Link", - "label": "Quality Inspection", - "no_copy": 1, - "options": "Quality Inspection", - "print_hide": 1 - }, - { - "fieldname": "column_break_40", - "fieldtype": "Column Break" - }, - { - "fieldname": "subcontracting_order", - "fieldtype": "Link", - "label": "Subcontracting Order", - "no_copy": 1, - "options": "Subcontracting Order", - "print_width": "150px", - "read_only": 1, - "search_index": 1, - "width": "150px" - }, - { - "fieldname": "schedule_date", - "fieldtype": "Date", - "label": "Required By", - "print_hide": 1, - "read_only": 1 - }, - { - "fieldname": "section_break_45", - "fieldtype": "Section Break" - }, - { - "depends_on": "eval:!doc.is_fixed_asset", - "fieldname": "serial_no", - "fieldtype": "Small Text", - "in_list_view": 1, - "label": "Serial No", - "no_copy": 1 - }, - { - "depends_on": "eval:!doc.is_fixed_asset", - "fieldname": "batch_no", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Batch No", - "no_copy": 1, - "options": "Batch", - "print_hide": 1 - }, - { - "depends_on": "eval:!doc.is_fixed_asset", - "fieldname": "rejected_serial_no", - "fieldtype": "Small Text", - "label": "Rejected Serial No", - "no_copy": 1, - "print_hide": 1 - }, - { - "fieldname": "subcontracting_order_item", - "fieldtype": "Data", - "hidden": 1, - "label": "Subcontracting Order Item", - "no_copy": 1, - "print_hide": 1, - "print_width": "150px", - "read_only": 1, - "search_index": 1, - "width": "150px" - }, - { - "fieldname": "col_break5", - "fieldtype": "Column Break" - }, - { - "fieldname": "bom", - "fieldtype": "Link", - "label": "BOM", - "no_copy": 1, - "options": "BOM", - "print_hide": 1 - }, - { - "fetch_from": "item_code.brand", - "fieldname": "brand", - "fieldtype": "Link", - "hidden": 1, - "label": "Brand", - "options": "Brand", - "print_hide": 1, - "read_only": 1 - }, - { - "fieldname": "rm_supp_cost", - "fieldtype": "Currency", - "hidden": 1, - "label": "Raw Materials Supplied Cost", - "no_copy": 1, - "options": "Company:company:default_currency", - "print_hide": 1, - "print_width": "150px", - "read_only": 1, - "width": "150px" - }, - { - "fieldname": "expense_account", - "fieldtype": "Link", - "hidden": 1, - "label": "Expense Account", - "options": "Account", - "read_only": 1 - }, - { - "collapsible": 1, - "fieldname": "manufacture_details", - "fieldtype": "Section Break", - "label": "Manufacture" - }, - { - "fieldname": "manufacturer", - "fieldtype": "Link", - "label": "Manufacturer", - "options": "Manufacturer" - }, - { - "fieldname": "column_break_16", - "fieldtype": "Column Break" - }, - { - "fieldname": "manufacturer_part_no", - "fieldtype": "Data", - "label": "Manufacturer Part Number" - }, - { - "fieldname": "subcontracting_receipt_item", - "fieldtype": "Data", - "hidden": 1, - "label": "Subcontracting Receipt Item", - "no_copy": 1, - "print_hide": 1, - "read_only": 1 - }, - { - "collapsible": 1, - "fieldname": "image_column", - "fieldtype": "Column Break" - }, - { - "fieldname": "tracking_section", - "fieldtype": "Section Break" - }, - { - "fieldname": "col_break_tracking_section", - "fieldtype": "Column Break" - }, - { - "fieldname": "accounting_dimensions_section", - "fieldtype": "Section Break", - "label": "Accounting Dimensions" - }, - { - "fieldname": "project", - "fieldtype": "Link", - "label": "Project", - "options": "Project", - "print_hide": 1 - }, - { - "fieldname": "dimension_col_break", - "fieldtype": "Column Break" - }, - { - "default": ":Company", - "depends_on": "eval:cint(erpnext.is_perpetual_inventory_enabled(parent.company))", - "fieldname": "cost_center", - "fieldtype": "Link", - "label": "Cost Center", - "options": "Cost Center", - "print_hide": 1 - }, - { - "fieldname": "section_break_80", - "fieldtype": "Section Break" - }, - { - "allow_on_submit": 1, - "default": "0", - "fieldname": "page_break", - "fieldtype": "Check", - "label": "Page Break", - "print_hide": 1 - }, - { - "depends_on": "returned_qty", - "fieldname": "returned_qty", - "fieldtype": "Float", - "label": "Returned Qty", - "no_copy": 1, - "print_hide": 1, - "read_only": 1 - } - ], - "idx": 1, - "istable": 1, - "links": [], - "modified": "2022-04-21 12:07:55.899701", - "modified_by": "Administrator", - "module": "Subcontracting", - "name": "Subcontracting Receipt Item", - "naming_rule": "Random", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "sort_field": "modified", - "sort_order": "DESC", - "states": [] + "actions": [], + "autoname": "hash", + "creation": "2022-04-13 16:05:55.395695", + "doctype": "DocType", + "document_type": "Document", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "item_code", + "column_break_2", + "item_name", + "section_break_4", + "description", + "brand", + "image_column", + "image", + "image_view", + "received_and_accepted", + "received_qty", + "qty", + "rejected_qty", + "returned_qty", + "col_break2", + "stock_uom", + "conversion_factor", + "tracking_section", + "col_break_tracking_section", + "rate_and_amount", + "rate", + "amount", + "recalculate_rate", + "column_break_19", + "rm_cost_per_qty", + "service_cost_per_qty", + "additional_cost_per_qty", + "rm_supp_cost", + "warehouse_and_reference", + "warehouse", + "rejected_warehouse", + "subcontracting_order", + "column_break_40", + "schedule_date", + "quality_inspection", + "subcontracting_order_item", + "subcontracting_receipt_item", + "section_break_45", + "bom", + "serial_no", + "col_break5", + "batch_no", + "rejected_serial_no", + "manufacture_details", + "manufacturer", + "column_break_16", + "manufacturer_part_no", + "accounting_details_section", + "expense_account", + "accounting_dimensions_section", + "cost_center", + "dimension_col_break", + "project", + "section_break_80", + "page_break" + ], + "fields": [ + { + "bold": 1, + "columns": 3, + "fieldname": "item_code", + "fieldtype": "Link", + "in_global_search": 1, + "in_list_view": 1, + "label": "Item Code", + "options": "Item", + "print_width": "100px", + "reqd": 1, + "search_index": 1, + "width": "100px" + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, + { + "fieldname": "item_name", + "fieldtype": "Data", + "in_global_search": 1, + "label": "Item Name", + "print_hide": 1, + "reqd": 1 + }, + { + "collapsible": 1, + "fieldname": "section_break_4", + "fieldtype": "Section Break", + "label": "Description" + }, + { + "fieldname": "description", + "fieldtype": "Text Editor", + "label": "Description", + "print_width": "300px", + "reqd": 1, + "width": "300px" + }, + { + "fieldname": "image", + "fieldtype": "Attach", + "hidden": 1, + "label": "Image" + }, + { + "fieldname": "image_view", + "fieldtype": "Image", + "label": "Image View", + "options": "image", + "print_hide": 1 + }, + { + "fieldname": "received_and_accepted", + "fieldtype": "Section Break", + "label": "Received and Accepted" + }, + { + "bold": 1, + "default": "0", + "fieldname": "received_qty", + "fieldtype": "Float", + "label": "Received Quantity", + "no_copy": 1, + "print_hide": 1, + "print_width": "100px", + "read_only": 1, + "reqd": 1, + "width": "100px" + }, + { + "columns": 2, + "fieldname": "qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Accepted Quantity", + "no_copy": 1, + "print_width": "100px", + "width": "100px" + }, + { + "columns": 1, + "depends_on": "eval: !parent.is_return", + "fieldname": "rejected_qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Rejected Quantity", + "no_copy": 1, + "print_hide": 1, + "print_width": "100px", + "width": "100px" + }, + { + "fieldname": "col_break2", + "fieldtype": "Column Break", + "print_hide": 1 + }, + { + "fieldname": "stock_uom", + "fieldtype": "Link", + "label": "Stock UOM", + "options": "UOM", + "print_hide": 1, + "print_width": "100px", + "read_only": 1, + "reqd": 1, + "width": "100px" + }, + { + "default": "1", + "fieldname": "conversion_factor", + "fieldtype": "Float", + "hidden": 1, + "label": "Conversion Factor", + "read_only": 1 + }, + { + "fieldname": "rate_and_amount", + "fieldtype": "Section Break", + "label": "Rate and Amount" + }, + { + "bold": 1, + "columns": 2, + "fieldname": "rate", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Rate", + "options": "currency", + "print_width": "100px", + "read_only": 1, + "read_only_depends_on": "eval: doc.recalculate_rate", + "width": "100px" + }, + { + "fieldname": "amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Amount", + "options": "currency", + "read_only": 1 + }, + { + "fieldname": "column_break_19", + "fieldtype": "Column Break" + }, + { + "fieldname": "rm_cost_per_qty", + "fieldtype": "Currency", + "label": "Raw Material Cost Per Qty", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "service_cost_per_qty", + "fieldtype": "Currency", + "label": "Service Cost Per Qty", + "read_only": 1, + "reqd": 1 + }, + { + "default": "0", + "fieldname": "additional_cost_per_qty", + "fieldtype": "Currency", + "label": "Additional Cost Per Qty", + "read_only": 1 + }, + { + "fieldname": "warehouse_and_reference", + "fieldtype": "Section Break", + "label": "Warehouse and Reference" + }, + { + "bold": 1, + "fieldname": "warehouse", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Accepted Warehouse", + "options": "Warehouse", + "print_hide": 1, + "print_width": "100px", + "width": "100px" + }, + { + "depends_on": "eval: !parent.is_return", + "fieldname": "rejected_warehouse", + "fieldtype": "Link", + "label": "Rejected Warehouse", + "no_copy": 1, + "options": "Warehouse", + "print_hide": 1, + "print_width": "100px", + "width": "100px" + }, + { + "depends_on": "eval:!doc.__islocal", + "fieldname": "quality_inspection", + "fieldtype": "Link", + "label": "Quality Inspection", + "no_copy": 1, + "options": "Quality Inspection", + "print_hide": 1 + }, + { + "fieldname": "column_break_40", + "fieldtype": "Column Break" + }, + { + "fieldname": "subcontracting_order", + "fieldtype": "Link", + "label": "Subcontracting Order", + "no_copy": 1, + "options": "Subcontracting Order", + "print_width": "150px", + "read_only": 1, + "search_index": 1, + "width": "150px" + }, + { + "fieldname": "schedule_date", + "fieldtype": "Date", + "label": "Required By", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "section_break_45", + "fieldtype": "Section Break" + }, + { + "depends_on": "eval:!doc.is_fixed_asset", + "fieldname": "serial_no", + "fieldtype": "Small Text", + "in_list_view": 1, + "label": "Serial No", + "no_copy": 1 + }, + { + "depends_on": "eval:!doc.is_fixed_asset", + "fieldname": "batch_no", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Batch No", + "no_copy": 1, + "options": "Batch", + "print_hide": 1 + }, + { + "depends_on": "eval: !parent.is_return", + "fieldname": "rejected_serial_no", + "fieldtype": "Small Text", + "label": "Rejected Serial No", + "no_copy": 1, + "print_hide": 1 + }, + { + "fieldname": "subcontracting_order_item", + "fieldtype": "Data", + "hidden": 1, + "label": "Subcontracting Order Item", + "no_copy": 1, + "print_hide": 1, + "print_width": "150px", + "read_only": 1, + "search_index": 1, + "width": "150px" + }, + { + "fieldname": "col_break5", + "fieldtype": "Column Break" + }, + { + "fieldname": "bom", + "fieldtype": "Link", + "label": "BOM", + "no_copy": 1, + "options": "BOM", + "print_hide": 1 + }, + { + "fetch_from": "item_code.brand", + "fieldname": "brand", + "fieldtype": "Link", + "hidden": 1, + "label": "Brand", + "options": "Brand", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "rm_supp_cost", + "fieldtype": "Currency", + "hidden": 1, + "label": "Raw Materials Supplied Cost", + "no_copy": 1, + "options": "Company:company:default_currency", + "print_hide": 1, + "print_width": "150px", + "read_only": 1, + "width": "150px" + }, + { + "fieldname": "expense_account", + "fieldtype": "Link", + "label": "Expense Account", + "options": "Account" + }, + { + "collapsible": 1, + "fieldname": "manufacture_details", + "fieldtype": "Section Break", + "label": "Manufacture" + }, + { + "fieldname": "manufacturer", + "fieldtype": "Link", + "label": "Manufacturer", + "options": "Manufacturer" + }, + { + "fieldname": "column_break_16", + "fieldtype": "Column Break" + }, + { + "fieldname": "manufacturer_part_no", + "fieldtype": "Data", + "label": "Manufacturer Part Number" + }, + { + "fieldname": "subcontracting_receipt_item", + "fieldtype": "Data", + "hidden": 1, + "label": "Subcontracting Receipt Item", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, + { + "collapsible": 1, + "fieldname": "image_column", + "fieldtype": "Column Break" + }, + { + "fieldname": "tracking_section", + "fieldtype": "Section Break" + }, + { + "fieldname": "col_break_tracking_section", + "fieldtype": "Column Break" + }, + { + "fieldname": "accounting_dimensions_section", + "fieldtype": "Section Break", + "label": "Accounting Dimensions" + }, + { + "fieldname": "project", + "fieldtype": "Link", + "label": "Project", + "options": "Project", + "print_hide": 1 + }, + { + "fieldname": "dimension_col_break", + "fieldtype": "Column Break" + }, + { + "default": ":Company", + "depends_on": "eval:cint(erpnext.is_perpetual_inventory_enabled(parent.company))", + "fieldname": "cost_center", + "fieldtype": "Link", + "label": "Cost Center", + "options": "Cost Center", + "print_hide": 1 + }, + { + "fieldname": "section_break_80", + "fieldtype": "Section Break" + }, + { + "allow_on_submit": 1, + "default": "0", + "fieldname": "page_break", + "fieldtype": "Check", + "label": "Page Break", + "print_hide": 1 + }, + { + "depends_on": "returned_qty", + "fieldname": "returned_qty", + "fieldtype": "Float", + "label": "Returned Qty", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "accounting_details_section", + "fieldtype": "Section Break", + "label": "Accounting Details" + }, + { + "default": "1", + "fieldname": "recalculate_rate", + "fieldtype": "Check", + "label": "Recalculate Rate" + } + ], + "idx": 1, + "istable": 1, + "links": [], + "modified": "2022-11-16 14:21:26.125815", + "modified_by": "Administrator", + "module": "Subcontracting", + "name": "Subcontracting Receipt Item", + "naming_rule": "Random", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "states": [] } \ No newline at end of file diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json b/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json index 100a8060e8..d21bc22ad7 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json +++ b/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json @@ -1,198 +1,208 @@ { - "actions": [], - "creation": "2022-04-18 10:45:16.538479", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "main_item_code", - "rm_item_code", - "item_name", - "bom_detail_no", - "col_break1", - "description", - "stock_uom", - "conversion_factor", - "reference_name", - "secbreak_1", - "rate", - "col_break2", - "amount", - "secbreak_2", - "required_qty", - "col_break3", - "consumed_qty", - "current_stock", - "secbreak_3", - "batch_no", - "col_break4", - "serial_no", - "subcontracting_order" - ], - "fields": [ - { - "fieldname": "main_item_code", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Item Code", - "options": "Item", - "read_only": 1 - }, - { - "fieldname": "rm_item_code", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Raw Material Item Code", - "options": "Item", - "read_only": 1 - }, - { - "fieldname": "description", - "fieldtype": "Text Editor", - "in_global_search": 1, - "label": "Description", - "print_width": "300px", - "read_only": 1, - "width": "300px" - }, - { - "fieldname": "batch_no", - "fieldtype": "Link", - "label": "Batch No", - "no_copy": 1, - "options": "Batch" - }, - { - "fieldname": "serial_no", - "fieldtype": "Text", - "label": "Serial No", - "no_copy": 1 - }, - { - "fieldname": "col_break1", - "fieldtype": "Column Break" - }, - { - "fieldname": "required_qty", - "fieldtype": "Float", - "in_list_view": 1, - "label": "Available Qty For Consumption", - "print_hide": 1, - "read_only": 1 - }, - { - "columns": 2, - "fieldname": "consumed_qty", - "fieldtype": "Float", - "in_list_view": 1, - "label": "Qty to be Consumed", - "reqd": 1 - }, - { - "fieldname": "stock_uom", - "fieldtype": "Link", - "label": "Stock Uom", - "options": "UOM", - "read_only": 1 - }, - { - "fieldname": "rate", - "fieldtype": "Currency", - "label": "Rate", - "options": "Company:company:default_currency", - "read_only": 1 - }, - { - "fieldname": "amount", - "fieldtype": "Currency", - "label": "Amount", - "options": "Company:company:default_currency", - "read_only": 1 - }, - { - "default": "1", - "fieldname": "conversion_factor", - "fieldtype": "Float", - "hidden": 1, - "label": "Conversion Factor", - "read_only": 1 - }, - { - "fieldname": "current_stock", - "fieldtype": "Float", - "in_list_view": 1, - "label": "Current Stock", - "read_only": 1 - }, - { - "fieldname": "reference_name", - "fieldtype": "Data", - "hidden": 1, - "in_list_view": 1, - "label": "Reference Name", - "read_only": 1 - }, - { - "fieldname": "bom_detail_no", - "fieldtype": "Data", - "hidden": 1, - "in_list_view": 1, - "label": "BOM Detail No", - "read_only": 1 - }, - { - "fieldname": "secbreak_1", - "fieldtype": "Section Break" - }, - { - "fieldname": "col_break2", - "fieldtype": "Column Break" - }, - { - "fieldname": "secbreak_2", - "fieldtype": "Section Break" - }, - { - "fieldname": "col_break3", - "fieldtype": "Column Break" - }, - { - "fieldname": "secbreak_3", - "fieldtype": "Section Break" - }, - { - "fieldname": "col_break4", - "fieldtype": "Column Break" - }, - { - "fieldname": "item_name", - "fieldtype": "Data", - "label": "Item Name", - "read_only": 1 - }, - { - "fieldname": "subcontracting_order", - "fieldtype": "Link", - "hidden": 1, - "label": "Subcontracting Order", - "no_copy": 1, - "options": "Subcontracting Order", - "print_hide": 1, - "read_only": 1 - } - ], - "idx": 1, - "istable": 1, - "links": [], - "modified": "2022-04-18 10:45:16.538479", - "modified_by": "Administrator", - "module": "Subcontracting", - "name": "Subcontracting Receipt Supplied Item", - "naming_rule": "Autoincrement", - "owner": "Administrator", - "permissions": [], - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "states": [] + "actions": [], + "creation": "2022-04-18 10:45:16.538479", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "main_item_code", + "rm_item_code", + "item_name", + "bom_detail_no", + "col_break1", + "description", + "stock_uom", + "conversion_factor", + "reference_name", + "secbreak_1", + "rate", + "col_break2", + "amount", + "secbreak_2", + "available_qty_for_consumption", + "required_qty", + "col_break3", + "consumed_qty", + "current_stock", + "secbreak_3", + "batch_no", + "col_break4", + "serial_no", + "subcontracting_order" + ], + "fields": [ + { + "fieldname": "main_item_code", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Item Code", + "options": "Item", + "read_only": 1 + }, + { + "fieldname": "rm_item_code", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Raw Material Item Code", + "options": "Item", + "read_only": 1 + }, + { + "fieldname": "description", + "fieldtype": "Text Editor", + "in_global_search": 1, + "label": "Description", + "print_width": "300px", + "read_only": 1, + "width": "300px" + }, + { + "fieldname": "batch_no", + "fieldtype": "Link", + "label": "Batch No", + "no_copy": 1, + "options": "Batch" + }, + { + "fieldname": "serial_no", + "fieldtype": "Text", + "label": "Serial No", + "no_copy": 1 + }, + { + "fieldname": "col_break1", + "fieldtype": "Column Break" + }, + { + "fieldname": "required_qty", + "fieldtype": "Float", + "label": "Required Qty", + "print_hide": 1, + "read_only": 1 + }, + { + "columns": 2, + "fieldname": "consumed_qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Consumed Qty", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "stock_uom", + "fieldtype": "Link", + "label": "Stock Uom", + "options": "UOM", + "read_only": 1 + }, + { + "fieldname": "rate", + "fieldtype": "Currency", + "label": "Rate", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "amount", + "fieldtype": "Currency", + "label": "Amount", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "default": "1", + "fieldname": "conversion_factor", + "fieldtype": "Float", + "hidden": 1, + "label": "Conversion Factor", + "read_only": 1 + }, + { + "fieldname": "current_stock", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Current Stock", + "read_only": 1 + }, + { + "fieldname": "reference_name", + "fieldtype": "Data", + "hidden": 1, + "in_list_view": 1, + "label": "Reference Name", + "read_only": 1 + }, + { + "fieldname": "bom_detail_no", + "fieldtype": "Data", + "hidden": 1, + "in_list_view": 1, + "label": "BOM Detail No", + "read_only": 1 + }, + { + "fieldname": "secbreak_1", + "fieldtype": "Section Break" + }, + { + "fieldname": "col_break2", + "fieldtype": "Column Break" + }, + { + "fieldname": "secbreak_2", + "fieldtype": "Section Break" + }, + { + "fieldname": "col_break3", + "fieldtype": "Column Break" + }, + { + "fieldname": "secbreak_3", + "fieldtype": "Section Break" + }, + { + "fieldname": "col_break4", + "fieldtype": "Column Break" + }, + { + "fieldname": "item_name", + "fieldtype": "Data", + "label": "Item Name", + "read_only": 1 + }, + { + "fieldname": "subcontracting_order", + "fieldtype": "Link", + "hidden": 1, + "label": "Subcontracting Order", + "no_copy": 1, + "options": "Subcontracting Order", + "print_hide": 1, + "read_only": 1 + }, + { + "default": "0", + "fieldname": "available_qty_for_consumption", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Available Qty For Consumption", + "print_hide": 1, + "read_only": 1 + } + ], + "idx": 1, + "istable": 1, + "links": [], + "modified": "2022-11-07 17:17:21.670761", + "modified_by": "Administrator", + "module": "Subcontracting", + "name": "Subcontracting Receipt Supplied Item", + "naming_rule": "Autoincrement", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py index 7f3e0cf4c2..f23419e98e 100644 --- a/erpnext/support/doctype/issue/issue.py +++ b/erpnext/support/doctype/issue/issue.py @@ -18,9 +18,6 @@ from frappe.utils.user import is_website_user class Issue(Document): - def get_feed(self): - return "{0}: {1}".format(_(self.status), self.subject) - def validate(self): if self.is_new() and self.via_customer_portal: self.flags.create_communication = True 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 e49f212f10..7f4e9efa94 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -776,6 +776,9 @@ def on_communication_update(doc, status): if not parent.meta.has_field("service_level_agreement"): return + if not parent.get("service_level_agreement"): + return + if ( doc.sent_or_received == "Received" # a reply is received and parent.get("status") == "Open" # issue status is set as open from communication.py 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 4e00138906..472f6bc24e 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 @@ -15,8 +15,30 @@ from erpnext.support.doctype.service_level_agreement.service_level_agreement imp class TestServiceLevelAgreement(unittest.TestCase): def setUp(self): + self.create_company() frappe.db.set_value("Support Settings", None, "track_service_level_agreement", 1) - frappe.db.sql("delete from `tabLead`") + lead = frappe.qb.DocType("Lead") + frappe.qb.from_(lead).delete().where(lead.company == self.company).run() + + def create_company(self): + name = "_Test Support SLA" + company = None + if frappe.db.exists("Company", name): + company = frappe.get_doc("Company", name) + else: + company = frappe.get_doc( + { + "doctype": "Company", + "company_name": name, + "country": "India", + "default_currency": "INR", + "create_chart_of_accounts_based_on": "Standard Template", + "chart_of_accounts": "Standard", + } + ) + company = company.save() + + self.company = company.name def test_service_level_agreement(self): # Default Service Level Agreement @@ -205,7 +227,7 @@ class TestServiceLevelAgreement(unittest.TestCase): # make lead with default SLA creation = datetime.datetime(2019, 3, 4, 12, 0) - lead = make_lead(creation=creation, index=1) + lead = make_lead(creation=creation, index=1, company=self.company) self.assertEqual(lead.service_level_agreement, lead_sla.name) self.assertEqual(lead.response_by, datetime.datetime(2019, 3, 4, 16, 0)) @@ -233,7 +255,7 @@ class TestServiceLevelAgreement(unittest.TestCase): ) creation = datetime.datetime(2020, 3, 4, 4, 0) - lead = make_lead(creation, index=2) + lead = make_lead(creation, index=2, company=self.company) frappe.flags.current_time = datetime.datetime(2020, 3, 4, 4, 15) lead.reload() @@ -267,7 +289,7 @@ class TestServiceLevelAgreement(unittest.TestCase): ) creation = datetime.datetime(2019, 3, 4, 12, 0) - lead = make_lead(creation=creation, index=1) + lead = make_lead(creation=creation, index=1, company=self.company) self.assertEqual(lead.response_by, datetime.datetime(2019, 3, 4, 16, 0)) # failed with response time only @@ -294,7 +316,7 @@ class TestServiceLevelAgreement(unittest.TestCase): # fulfilled with response time only creation = datetime.datetime(2019, 3, 4, 12, 0) - lead = make_lead(creation=creation, index=2) + lead = make_lead(creation=creation, index=2, company=self.company) self.assertEqual(lead.service_level_agreement, lead_sla.name) self.assertEqual(lead.response_by, datetime.datetime(2019, 3, 4, 16, 0)) @@ -321,7 +343,7 @@ class TestServiceLevelAgreement(unittest.TestCase): apply_sla_for_resolution=0, ) creation = datetime.datetime(2019, 3, 4, 12, 0) - lead = make_lead(creation=creation, index=4) + lead = make_lead(creation=creation, index=4, company=self.company) applied_sla = frappe.db.get_value("Lead", lead.name, "service_level_agreement") self.assertFalse(applied_sla) @@ -611,7 +633,7 @@ def create_custom_doctype(): return frappe.get_doc("DocType", "Test SLA on Custom Dt") -def make_lead(creation=None, index=0): +def make_lead(creation=None, index=0, company=None): return frappe.get_doc( { "doctype": "Lead", @@ -621,5 +643,6 @@ def make_lead(creation=None, index=0): "creation": creation, "service_level_agreement_creation": creation, "priority": "Medium", + "company": company, } ).insert(ignore_permissions=True) diff --git a/erpnext/support/doctype/warranty_claim/warranty_claim.py b/erpnext/support/doctype/warranty_claim/warranty_claim.py index 5e2ea067a8..ff63b77f9e 100644 --- a/erpnext/support/doctype/warranty_claim/warranty_claim.py +++ b/erpnext/support/doctype/warranty_claim/warranty_claim.py @@ -10,9 +10,6 @@ from erpnext.utilities.transaction_base import TransactionBase class WarrantyClaim(TransactionBase): - def get_feed(self): - return _("{0}: From {1}").format(self.status, self.customer_name) - def validate(self): if session["user"] != "Guest" and not self.customer: frappe.throw(_("Customer is required")) @@ -35,7 +32,7 @@ class WarrantyClaim(TransactionBase): lst1 = ",".join(x[0] for x in lst) frappe.throw(_("Cancel Material Visit {0} before cancelling this Warranty Claim").format(lst1)) else: - frappe.db.set(self, "status", "Cancelled") + self.db_set("status", "Cancelled") def on_update(self): pass diff --git a/erpnext/templates/generators/item/item.html b/erpnext/templates/generators/item/item.html index 4070d40d47..358c1c52e5 100644 --- a/erpnext/templates/generators/item/item.html +++ b/erpnext/templates/generators/item/item.html @@ -32,7 +32,7 @@
-
+
{% if show_tabs and tabs %}
diff --git a/erpnext/templates/includes/cart/cart_address.html b/erpnext/templates/includes/cart/cart_address.html index cf60017373..a8188ec825 100644 --- a/erpnext/templates/includes/cart/cart_address.html +++ b/erpnext/templates/includes/cart/cart_address.html @@ -55,6 +55,7 @@ {% endif %} -{% endblock %} - -{% block style %} - -{% endblock %} - -{% block title %} -{{ _("ERPNext Demo") }} -{% endblock %} - -{% block page_content %} -
- -
- - {{ _("ERPNext Demo") }} -
- -

Some functionality is disabled for the demo and the data will be cleared regularly.

-
-
- - -

Start a free 14-day trial -

- -{% endblock %} diff --git a/erpnext/templates/pages/home.html b/erpnext/templates/pages/home.html index 4c69b8388d..27d966ad42 100644 --- a/erpnext/templates/pages/home.html +++ b/erpnext/templates/pages/home.html @@ -37,7 +37,7 @@ {% for item in homepage.products %}
-
+ {{ item.item_name }}
{{ item.item_name }}
{{ _('More details') }} diff --git a/erpnext/templates/pages/order.html b/erpnext/templates/pages/order.html index a10870db27..bc34ad5ac5 100644 --- a/erpnext/templates/pages/order.html +++ b/erpnext/templates/pages/order.html @@ -5,149 +5,161 @@ {% include "templates/includes/breadcrumbs.html" %} {% endblock %} -{% block title %}{{ doc.name }}{% endblock %} +{% block title %} + {{ doc.name }} +{% endblock %} {% block header %} -

{{ doc.name }}

+

{{ doc.name }}

{% endblock %} {% block header_actions %} - {% endif %} -
{% if doc.terms %}
-

{{ doc.terms }}

+
+

{{ doc.terms }}

{% endif %} {% endblock %} {% block script %} - + -{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/erpnext/templates/pages/order.py b/erpnext/templates/pages/order.py index 3e6d57a02b..13772d3129 100644 --- a/erpnext/templates/pages/order.py +++ b/erpnext/templates/pages/order.py @@ -38,19 +38,28 @@ def get_context(context): if not frappe.has_website_permission(context.doc): frappe.throw(_("Not Permitted"), frappe.PermissionError) - # check for the loyalty program of the customer - customer_loyalty_program = frappe.db.get_value( - "Customer", context.doc.customer, "loyalty_program" - ) - if customer_loyalty_program: - from erpnext.accounts.doctype.loyalty_program.loyalty_program import ( - get_loyalty_program_details_with_points, + context.available_loyalty_points = 0.0 + if context.doc.get("customer"): + # check for the loyalty program of the customer + customer_loyalty_program = frappe.db.get_value( + "Customer", context.doc.customer, "loyalty_program" ) - loyalty_program_details = get_loyalty_program_details_with_points( - context.doc.customer, customer_loyalty_program - ) - context.available_loyalty_points = int(loyalty_program_details.get("loyalty_points")) + if customer_loyalty_program: + from erpnext.accounts.doctype.loyalty_program.loyalty_program import ( + get_loyalty_program_details_with_points, + ) + + loyalty_program_details = get_loyalty_program_details_with_points( + context.doc.customer, customer_loyalty_program + ) + context.available_loyalty_points = int(loyalty_program_details.get("loyalty_points")) + + context.show_pay_button = frappe.db.get_single_value("Buying Settings", "show_pay_button") + context.show_make_pi_button = False + if context.doc.get("supplier"): + # show Make Purchase Invoice button based on permission + context.show_make_pi_button = frappe.has_permission("Purchase Invoice", "create") def get_attachments(dt, dn): diff --git a/erpnext/templates/pages/product_search.py b/erpnext/templates/pages/product_search.py index 0768cc3fa6..f40fd479f4 100644 --- a/erpnext/templates/pages/product_search.py +++ b/erpnext/templates/pages/product_search.py @@ -5,14 +5,13 @@ import json import frappe from frappe.utils import cint, cstr -from redisearch import AutoCompleter, Client, Query +from redis.commands.search.query import Query from erpnext.e_commerce.redisearch_utils import ( WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE, WEBSITE_ITEM_INDEX, WEBSITE_ITEM_NAME_AUTOCOMPLETE, is_redisearch_enabled, - make_key, ) from erpnext.e_commerce.shopping_cart.product_info import set_product_info_for_website from erpnext.setup.doctype.item_group.item_group import get_item_for_list_in_html @@ -88,15 +87,17 @@ def product_search(query, limit=10, fuzzy_search=True): if not query: return search_results - red = frappe.cache() + redis = frappe.cache() query = clean_up_query(query) # TODO: Check perf/correctness with Suggestions & Query vs only Query # TODO: Use Levenshtein Distance in Query (max=3) - ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=red) - client = Client(make_key(WEBSITE_ITEM_INDEX), conn=red) - suggestions = ac.get_suggestions( - query, num=limit, fuzzy=fuzzy_search and len(query) > 3 # Fuzzy on length < 3 can be real slow + redisearch = redis.ft(WEBSITE_ITEM_INDEX) + suggestions = redisearch.sugget( + WEBSITE_ITEM_NAME_AUTOCOMPLETE, + query, + num=limit, + fuzzy=fuzzy_search and len(query) > 3, ) # Build a query @@ -106,8 +107,8 @@ def product_search(query, limit=10, fuzzy_search=True): query_string += f"|('{clean_up_query(s.string)}')" q = Query(query_string) + results = redisearch.search(q) - results = client.search(q) search_results["results"] = list(map(convert_to_dict, results.docs)) search_results["results"] = sorted( search_results["results"], key=lambda k: frappe.utils.cint(k["ranking"]), reverse=True @@ -141,8 +142,8 @@ def get_category_suggestions(query): if not query: return search_results - ac = AutoCompleter(make_key(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE), conn=frappe.cache()) - suggestions = ac.get_suggestions(query, num=10, with_payloads=True) + ac = frappe.cache().ft() + suggestions = ac.sugget(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE, query, num=10, with_payloads=True) results = [json.loads(s.payload) for s in suggestions] diff --git a/erpnext/templates/pages/projects.html b/erpnext/templates/pages/projects.html index 6d366c5ffe..9fe4338477 100644 --- a/erpnext/templates/pages/projects.html +++ b/erpnext/templates/pages/projects.html @@ -9,7 +9,7 @@ {% endblock %} {% block header %} -

{{ doc.project_name }}

+ {% endblock %} {% block style %} @@ -21,61 +21,62 @@ {% endblock %} {% block page_content %} - - {{ progress_bar(doc.percent_complete) }} - -
-

Status:

-

Progress: - {{ doc.percent_complete }} - % -

-

Hours Spent: - {{ doc.actual_time }} -

+
+
+
Status: {{ doc.status }}
+
Progress: {{ doc.percent_complete }}%
+
Hours Spent: {{ doc.actual_time | round }}
+
{{ progress_bar(doc.percent_complete) }} - {% if doc.tasks %} -
-
-
-
-

Tasks

-

Status

-

End Date

-

Assigned To

- -
-
- {% include "erpnext/templates/includes/projects/project_tasks.html" %} -
+
+ +
+ + +
+ {% if doc.tasks %} +
+
+
+
+
Tasks
+
Status
+
End Date
+
Assignment
+
Modified On
+
+
+ {% include "erpnext/templates/includes/projects/project_tasks.html" %} +
+
{% else %} -

{{ _("No Tasks") }}

+ {{ empty_state('Task')}} {% endif %} + {% if doc.timesheets %}
-
-

Timesheets

-

Status

-

From

-

To

-

Modified By

-

Modified On

+
+
Timesheet
+
Status
+
From
+
To
+
Modified By
+
Modified On
- {% include "erpnext/templates/includes/projects/project_timesheets.html" %} + {% include "erpnext/templates/includes/projects/project_timesheets.html" %}
{% else %} -

{{ _("No Timesheets") }}

+ {{ empty_state('Timesheet')}} {% endif %} {% if doc.attachments %} @@ -104,70 +105,37 @@
+ { % include "frappe/public/js/frappe/provide.js" % } + { % include "frappe/public/js/frappe/form/formatters.js" % } + {% endblock %} {% macro progress_bar(percent_complete) %} {% if percent_complete %} -
-
+ Project Progress: +
+
+
-{% else %} -
{% endif %} {% endmacro %} -{% macro task_row(task, indent) %} -
-
- - {% if task.parent_task %} - - - - {% endif %} - {{ task.subject }} + +{% macro empty_state(section_name) %} +
+
+
+
+
+ Generic Empty State +
+

You haven't created a {{ section_name }} yet

+
+
-
{{ task.status }}
-
- {% if task.exp_end_date %} - {{ task.exp_end_date }} - {% else %} - -- - {% endif %}
-
- {% if task["_assign"] %} - {% set assigned_users = json.loads(task["_assign"])%} - {% for user in assigned_users %} - {% set user_details = frappe.db.get_value("User", user, - ["full_name", "user_image"], - as_dict = True)%} - {% if user_details.user_image %} - - - - {% else %} - -
- {{ frappe.utils.get_abbr(user_details.full_name) }} -
-
- {% endif %} - {% endfor %} - {% endif %} -
-
- {{ frappe.utils.pretty_date(task.modified) }} -
-
-{% if task.children %} - {% for child in task.children %} - {{ task_row(child, indent + 30) }} - {% endfor %} -{% endif %} -{% endmacro %} +{% endmacro %} \ No newline at end of file diff --git a/erpnext/translations/af.csv b/erpnext/translations/af.csv index 45435d8600..265e85c0f1 100644 --- a/erpnext/translations/af.csv +++ b/erpnext/translations/af.csv @@ -3537,7 +3537,7 @@ Quality Feedback Template,Kwaliteit-terugvoersjabloon, Rules for applying different promotional schemes.,Reëls vir die toepassing van verskillende promosieskemas., Shift,verskuiwing, Show {0},Wys {0}, -"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series","Spesiale karakters behalwe "-", "#", ".", "/", "{" En "}" word nie toegelaat in die naamreekse nie", +"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}","Spesiale karakters behalwe "-", "#", ".", "/", "{{" En "}}" word nie toegelaat in die naamreekse nie {0}", Target Details,Teikenbesonderhede, {0} already has a Parent Procedure {1}.,{0} het reeds 'n ouerprosedure {1}., API,API, diff --git a/erpnext/translations/am.csv b/erpnext/translations/am.csv index 554b0a54f1..d131404821 100644 --- a/erpnext/translations/am.csv +++ b/erpnext/translations/am.csv @@ -3537,7 +3537,7 @@ Quality Feedback Template,የጥራት ግብረ መልስ አብነት።, Rules for applying different promotional schemes.,የተለያዩ የማስተዋወቂያ ዘዴዎችን ለመተግበር ህጎች።, Shift,ቀይር, Show {0},አሳይ {0}, -"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series",ከ "-" ፣ "#" ፣ "፣" ፣ "/" ፣ "{" እና "}" በስተቀር ልዩ ቁምፊዎች ከመለያ መሰየሚያ አይፈቀድም, +"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}",ከ "-" ፣ "#" ፣ "፣" ፣ "/" ፣ "{{" እና "}}" በስተቀር ልዩ ቁምፊዎች ከመለያ መሰየሚያ አይፈቀድም {0}, Target Details,የ Detailsላማ ዝርዝሮች።, {0} already has a Parent Procedure {1}.,{0} ቀድሞውኑ የወላጅ አሰራር ሂደት አለው {1}።, API,ኤ ፒ አይ, diff --git a/erpnext/translations/ar.csv b/erpnext/translations/ar.csv index e62f61a4f5..c0da1c4d76 100644 --- a/erpnext/translations/ar.csv +++ b/erpnext/translations/ar.csv @@ -3537,7 +3537,7 @@ Quality Feedback Template,قالب ملاحظات الجودة, Rules for applying different promotional schemes.,قواعد تطبيق المخططات الترويجية المختلفة., Shift,تحول, Show {0},عرض {0}, -"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series",الأحرف الخاصة باستثناء "-" ، "#" ، "." ، "/" ، "{" و "}" غير مسموح في سلسلة التسمية, +"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}",{0} الأحرف الخاصة باستثناء "-" ، "#" ، "." ، "/" ، "{{" و "}}" غير مسموح في سلسلة التسمية, Target Details,تفاصيل الهدف, {0} already has a Parent Procedure {1}.,{0} يحتوي بالفعل على إجراء الأصل {1}., API,API, diff --git a/erpnext/translations/bg.csv b/erpnext/translations/bg.csv index 15278a6a40..ac6dc7851b 100644 --- a/erpnext/translations/bg.csv +++ b/erpnext/translations/bg.csv @@ -3537,7 +3537,7 @@ Quality Feedback Template,Качествен обратен шаблон, Rules for applying different promotional schemes.,Правила за прилагане на различни промоционални схеми., Shift,изместване, Show {0},Показване на {0}, -"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series","Специални символи, с изключение на "-", "#", ".", "/", "{" И "}" не са позволени в именуването на серии", +"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}","Специални символи, с изключение на "-", "#", ".", "/", "{{" И "}}" не са позволени в именуването на серии {0}", Target Details,Детайли за целта, {0} already has a Parent Procedure {1}.,{0} вече има родителска процедура {1}., API,API, diff --git a/erpnext/translations/bn.csv b/erpnext/translations/bn.csv index cf0971667b..52f7b1c593 100644 --- a/erpnext/translations/bn.csv +++ b/erpnext/translations/bn.csv @@ -3537,7 +3537,7 @@ Quality Feedback Template,গুণমান প্রতিক্রিয় Rules for applying different promotional schemes.,বিভিন্ন প্রচারমূলক স্কিম প্রয়োগ করার নিয়ম।, Shift,পরিবর্তন, Show {0},{0} দেখান, -"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series","নামকরণ সিরিজে "-", "#", "।", "/", "{" এবং "}" ব্যতীত বিশেষ অক্ষর অনুমোদিত নয়", +"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}","নামকরণ সিরিজে "-", "#", "।", "/", "{{" এবং "}}" ব্যতীত বিশেষ অক্ষর অনুমোদিত নয় {0}", Target Details,টার্গেটের বিশদ, {0} already has a Parent Procedure {1}.,{0} ইতিমধ্যে একটি মূল পদ্ধতি আছে {1}।, API,এপিআই, diff --git a/erpnext/translations/bs.csv b/erpnext/translations/bs.csv index 6ef445a1af..267434f480 100644 --- a/erpnext/translations/bs.csv +++ b/erpnext/translations/bs.csv @@ -3537,7 +3537,7 @@ Quality Feedback Template,Predložak kvalitetne povratne informacije, Rules for applying different promotional schemes.,Pravila za primjenu različitih promotivnih shema., Shift,Shift, Show {0},Prikaži {0}, -"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series","Posebni znakovi osim "-", "#", ".", "/", "{" I "}" nisu dozvoljeni u imenovanju serija", +"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}","Posebni znakovi osim "-", "#", ".", "/", "{{" I "}}" nisu dozvoljeni u imenovanju serija {0}", Target Details,Detalji cilja, {0} already has a Parent Procedure {1}.,{0} već ima roditeljsku proceduru {1}., API,API, diff --git a/erpnext/translations/ca.csv b/erpnext/translations/ca.csv index 18fa52a2b7..d8c2ef68e6 100644 --- a/erpnext/translations/ca.csv +++ b/erpnext/translations/ca.csv @@ -3537,7 +3537,7 @@ Quality Feedback Template,Plantilla de comentaris de qualitat, Rules for applying different promotional schemes.,Normes per aplicar diferents règims promocionals., Shift,Majúscules, Show {0},Mostra {0}, -"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series","Caràcters especials, excepte "-", "#", ".", "/", "{" I "}" no estan permesos en nomenar sèries", +"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}","Caràcters especials, excepte "-", "#", ".", "/", "{{" I "}}" no estan permesos en nomenar sèries {0}", Target Details,Detalls de l'objectiu, {0} already has a Parent Procedure {1}.,{0} ja té un procediment progenitor {1}., API,API, diff --git a/erpnext/translations/cs.csv b/erpnext/translations/cs.csv index 705e471d27..7d570bbdd0 100644 --- a/erpnext/translations/cs.csv +++ b/erpnext/translations/cs.csv @@ -3537,7 +3537,7 @@ Quality Feedback Template,Šablona zpětné vazby kvality, Rules for applying different promotional schemes.,Pravidla pro uplatňování různých propagačních programů., Shift,Posun, Show {0},Zobrazit {0}, -"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series","Zvláštní znaky kromě "-", "#", ".", "/", "{" A "}" nejsou v názvových řadách povoleny", +"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}","Zvláštní znaky kromě "-", "#", ".", "/", "{{" A "}}" nejsou v názvových řadách povoleny {0}", Target Details,Podrobnosti o cíli, {0} already has a Parent Procedure {1}.,{0} již má rodičovský postup {1}., API,API, diff --git a/erpnext/translations/da.csv b/erpnext/translations/da.csv index c0d0146694..16b2e878e0 100644 --- a/erpnext/translations/da.csv +++ b/erpnext/translations/da.csv @@ -3537,7 +3537,7 @@ Quality Feedback Template,Kvalitetsfeedback-skabelon, Rules for applying different promotional schemes.,Regler for anvendelse af forskellige salgsfremmende ordninger., Shift,Flytte, Show {0},Vis {0}, -"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series","Specialtegn undtagen "-", "#", ".", "/", "{" Og "}" er ikke tilladt i navngivningsserier", +"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}","Specialtegn undtagen "-", "#", ".", "/", "{{" Og "}}" er ikke tilladt i navngivningsserier {0}", Target Details,Måldetaljer, {0} already has a Parent Procedure {1}.,{0} har allerede en overordnet procedure {1}., API,API, diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv index 01795af23e..5a0a863a47 100644 --- a/erpnext/translations/de.csv +++ b/erpnext/translations/de.csv @@ -196,7 +196,7 @@ All other ITC,Alle anderen ITC, All the mandatory Task for employee creation hasn't been done yet.,Alle obligatorischen Aufgaben zur Mitarbeitererstellung wurden noch nicht erledigt., Allocate Payment Amount,Zahlungsbetrag zuweisen, Allocated Amount,Zugewiesene Menge, -Allocated Leaves,Zugewiesene Blätter, +Allocated Leaves,Zugewiesene Urlaubstage, Allocating leaves...,Blätter zuordnen..., Already record exists for the item {0},Es existiert bereits ein Datensatz für den Artikel {0}, "Already set default in pos profile {0} for user {1}, kindly disabled default","Im Standardprofil {0} für den Benutzer {1} ist der Standard bereits festgelegt, standardmäßig deaktiviert", @@ -1849,6 +1849,8 @@ Outstanding Amt,Offener Betrag, Outstanding Cheques and Deposits to clear,Ausstehende Schecks und Anzahlungen zum verbuchen, Outstanding for {0} cannot be less than zero ({1}),Ausstände für {0} können nicht kleiner als Null sein ({1}), Outward taxable supplies(zero rated),Steuerpflichtige Lieferungen aus dem Ausland (null bewertet), +Over Receipt/Delivery of {0} {1} ignored for item {2} because you have {3} role.,"Überhöhte Annahme bzw. Lieferung von Artikel {2} mit {0} {1} wurde ignoriert, weil Sie die Rolle {3} haben." +Overbilling of {0} {1} ignored for item {2} because you have {3} role.,"Überhöhte Abrechnung von Artikel {2} mit {0} {1} wurde ignoriert, weil Sie die Rolle {3} haben." Overdue,Überfällig, Overlap in scoring between {0} and {1},Überlappung beim Scoring zwischen {0} und {1}, Overlapping conditions found between:,Überlagernde Bedingungen gefunden zwischen:, @@ -3544,7 +3546,7 @@ Quality Feedback Template,Qualitäts-Feedback-Vorlage, Rules for applying different promotional schemes.,Regeln für die Anwendung verschiedener Werbemaßnahmen., Shift,Verschiebung, Show {0},{0} anzeigen, -"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series","Sonderzeichen außer "-", "#", ".", "/", "{" Und "}" sind bei der Benennung von Serien nicht zulässig", +"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}","Sonderzeichen außer "-", "#", ".", "/", "{{" Und "}}" sind bei der Benennung von Serien nicht zulässig {0}", Target Details,Zieldetails, {0} already has a Parent Procedure {1}.,{0} hat bereits eine übergeordnete Prozedur {1}., API,API, @@ -4051,7 +4053,7 @@ Server Error,Serverfehler, Service Level Agreement has been changed to {0}.,Service Level Agreement wurde in {0} geändert., Service Level Agreement was reset.,Service Level Agreement wurde zurückgesetzt., Service Level Agreement with Entity Type {0} and Entity {1} already exists.,Service Level Agreement mit Entitätstyp {0} und Entität {1} ist bereits vorhanden., -Set,Menge, +Set Loyalty Program,Treueprogramm eintragen, Set Meta Tags,Festlegen von Meta-Tags, Set {0} in company {1},{0} in Firma {1} festlegen, Setup,Einstellungen, @@ -4231,10 +4233,8 @@ To date cannot be before From date,Bis-Datum kann nicht vor Von-Datum liegen, Write Off,Abschreiben, {0} Created,{0} Erstellt, Email Id,E-Mail-ID, -No,Kein, Reference Doctype,Referenz-DocType, User Id,Benutzeridentifikation, -Yes,Ja, Actual ,Tatsächlich, Add to cart,In den Warenkorb legen, Budget,Budget, @@ -8625,8 +8625,8 @@ Material Request Warehouse,Materialanforderungslager, Select warehouse for material requests,Wählen Sie Lager für Materialanfragen, Transfer Materials For Warehouse {0},Material für Lager übertragen {0}, Production Plan Material Request Warehouse,Produktionsplan Materialanforderungslager, -Sets 'Source Warehouse' in each row of the items table.,Legt 'Source Warehouse' in jeder Zeile der Artikeltabelle fest., -Sets 'Target Warehouse' in each row of the items table.,"Füllt das Feld ""Ziel Lager"" in allen Positionen der folgenden Tabelle.", +Sets 'Source Warehouse' in each row of the items table.,Legt in jeder Zeile der Artikeltabelle das „Ausgangslager“ fest., +Sets 'Target Warehouse' in each row of the items table.,Legt in jeder Zeile der Artikeltabelle das „Eingangslager“ fest., Show Cancelled Entries,Abgebrochene Einträge anzeigen, Backdated Stock Entry,Backdated Stock Entry, Row #{}: Currency of {} - {} doesn't matches company currency.,Zeile # {}: Die Währung von {} - {} stimmt nicht mit der Firmenwährung überein., @@ -9873,3 +9873,46 @@ Leave Type Allocation,Zuordnung Abwesenheitsarten, From Lead,Aus Lead, From Opportunity,Aus Chance, Publish in Website,Auf Webseite veröffentlichen, +Total Allocated Leave(s),Gesamte zugewiesene Urlaubstage, +Expired Leave(s),Verfallene Urlaubstage, +Used Leave(s),Verbrauchte Urlaubstage, +Leave(s) Pending Approval,Urlaubstage zur Genehmigung ausstehend, +Available Leave(s),Verfügbare Urlaubstage, +Party Specific Item,Parteispezifischer Artikel, +Active Customers,Aktive Kunden, +Annual Sales,Jährlicher Umsatz, +Total Outgoing Bills,Ausgangsrechnungen insgesamt, +Total Incoming Bills,Eingangsrechnungen insgesamt, +Total Incoming Payment,Zahlungseingang insgesamt, +Total Outgoing Payment,Zahlungsausgang insgesamt, +Incoming Bills (Purchase Invoice),Eingehende Rechnungen (Eingangsrechnung), +Outgoing Bills (Sales Invoice),Ausgehende Rechnungen (Ausgangsrechnung), +Accounts Receivable Ageing,Fälligkeit Forderungen, +Accounts Payable Ageing,Fälligkeit Verbindlichkeiten, +Budget Variance,Budgetabweichung, +Based On Value,Basierend auf Wert, +Restrict Items Based On,Artikel einschränken auf Basis von, +Earnings & Deductions,Erträge & Abzüge, +Is Process Loss,Ist Prozessverlust, +Is Finished Item,Ist fertiger Artikel, +Is Scrap Item,Ist Schrott, +Issue a debit note with 0 qty against an existing Sales Invoice,Lastschrift mit Menge 0 gegen eine bestehende Ausgangsrechnung ausstellen, +Show Remarks,Bemerkungen anzeigen, +Website Item,Webseiten-Artikel, +Update Property,Eigenschaft aktualisieren, +Recurring Sales Invoice,Wiederkehrende Ausgangsrechnung, +Total Asset,Aktiva, +Total Liability,Verbindlichkeiten, +Total Equity,Eigenkapital, +Warehouse wise Stock Value,Warenwert nach Lager, +Ex Works,Ab Werk, +Free Carrier,Frei Frachtführer, +Free Alongside Ship,Frei Längsseite Schiff, +Free on Board,Frei an Bord, +Carriage Paid To,Frachtfrei, +Carriage and Insurance Paid to,Frachtfrei versichert, +Cost and Freight,Kosten und Fracht, +"Cost, Insurance and Freight","Kosten, Versicherung und Fracht", +Delivered at Place,Geliefert benannter Ort, +Delivered at Place Unloaded,Geliefert benannter Ort entladen, +Delivered Duty Paid,Geliefert verzollt, diff --git a/erpnext/translations/el.csv b/erpnext/translations/el.csv index acf5db5d46..06b80603e6 100644 --- a/erpnext/translations/el.csv +++ b/erpnext/translations/el.csv @@ -3537,7 +3537,7 @@ Quality Feedback Template,Πρότυπο σχολιασμού ποιότητας Rules for applying different promotional schemes.,Κανόνες εφαρμογής διαφορετικών προγραμμάτων προώθησης., Shift,Βάρδια, Show {0},Εμφάνιση {0}, -"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series","Ειδικοί χαρακτήρες εκτός από "-", "#", ".", "/", "" Και "}" δεν επιτρέπονται στη σειρά ονομασίας", +"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}","Ειδικοί χαρακτήρες εκτός από "-", "#", ".", "/", "{" Και "}}" δεν επιτρέπονται στη σειρά ονομασίας {0}", Target Details,Στοιχεία στόχου, {0} already has a Parent Procedure {1}.,{0} έχει ήδη μια διαδικασία γονέα {1}., API,API, diff --git a/erpnext/translations/es.csv b/erpnext/translations/es.csv index 0f2259db21..b216b868bb 100644 --- a/erpnext/translations/es.csv +++ b/erpnext/translations/es.csv @@ -3537,7 +3537,7 @@ Quality Feedback Template,Plantilla de comentarios de calidad, Rules for applying different promotional schemes.,Reglas para aplicar diferentes esquemas promocionales., Shift,Cambio, Show {0},Mostrar {0}, -"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series","Caracteres especiales excepto "-", "#", ".", "/", "{" Y "}" no están permitidos en las series de nombres", +"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}","Caracteres especiales excepto "-", "#", ".", "/", "{{" Y "}}" no están permitidos en las series de nombres {0}", Target Details,Detalles del objetivo, {0} already has a Parent Procedure {1}.,{0} ya tiene un Procedimiento principal {1}., API,API, diff --git a/erpnext/translations/et.csv b/erpnext/translations/et.csv index ba32187c68..5d67d81b79 100644 --- a/erpnext/translations/et.csv +++ b/erpnext/translations/et.csv @@ -3537,7 +3537,7 @@ Quality Feedback Template,Kvaliteetse tagasiside mall, Rules for applying different promotional schemes.,Erinevate reklaamiskeemide rakenduseeskirjad., Shift,Vahetus, Show {0},Kuva {0}, -"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series","Erimärgid, välja arvatud "-", "#", ".", "/", "{" Ja "}" pole sarjade nimetamisel lubatud", +"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}","Erimärgid, välja arvatud "-", "#", ".", "/", "{{" Ja "}}" pole sarjade nimetamisel lubatud {0}", Target Details,Sihtkoha üksikasjad, {0} already has a Parent Procedure {1}.,{0} juba on vanemamenetlus {1}., API,API, diff --git a/erpnext/translations/fa.csv b/erpnext/translations/fa.csv index 4a7c979499..040034d09e 100644 --- a/erpnext/translations/fa.csv +++ b/erpnext/translations/fa.csv @@ -3537,7 +3537,7 @@ Quality Feedback Template,الگوی بازخورد کیفیت, Rules for applying different promotional schemes.,قوانین استفاده از طرح های تبلیغاتی مختلف., Shift,تغییر مکان, Show {0},نمایش {0}, -"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series",کاراکترهای خاص به جز "-" ، "#" ، "." ، "/" ، "{" و "}" در سریال نامگذاری مجاز نیستند, +"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}",{0} کاراکترهای خاص به جز "-" ، "#" ، "." ، "/" ، "{{" و "}}" در سریال نامگذاری مجاز نیستند, Target Details,جزئیات هدف, {0} already has a Parent Procedure {1}.,{0} در حال حاضر یک روش والدین {1} دارد., API,API, diff --git a/erpnext/translations/fi.csv b/erpnext/translations/fi.csv index 29eb56702d..27ea3b8dd4 100644 --- a/erpnext/translations/fi.csv +++ b/erpnext/translations/fi.csv @@ -3537,7 +3537,7 @@ Quality Feedback Template,Laadun palautteen malli, Rules for applying different promotional schemes.,Säännöt erilaisten myynninedistämisjärjestelmien soveltamisesta., Shift,Siirtää, Show {0},Näytä {0}, -"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series","Erikoismerkit paitsi "-", "#", ".", "/", "{" Ja "}" eivät ole sallittuja nimeämissarjoissa", +"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}","Erikoismerkit paitsi "-", "#", ".", "/", "{{" Ja "}}" eivät ole sallittuja nimeämissarjoissa {0}", Target Details,Kohteen yksityiskohdat, {0} already has a Parent Procedure {1}.,{0}: llä on jo vanhempainmenettely {1}., API,API, diff --git a/erpnext/translations/fr.csv b/erpnext/translations/fr.csv index dbc319476b..8367afd331 100644 --- a/erpnext/translations/fr.csv +++ b/erpnext/translations/fr.csv @@ -329,11 +329,11 @@ Average Rate,Prix moyen, Avg Daily Outgoing,Moy Quotidienne Sortante, Avg. Buying Price List Rate,Moyenne de la liste de prix d'achat, Avg. Selling Price List Rate,Prix moyen de la liste de prix de vente, -Avg. Selling Rate,Moy. Taux de vente, +Avg. Selling Rate,Moy. prix de vente, BOM,Nomenclature, BOM Browser,Explorateur Nomenclature, BOM No,N° Nomenclature, -BOM Rate,Valeur nomenclature, +BOM Rate,Cout nomenclature, BOM Stock Report,Rapport de Stock des nomenclatures, BOM and Manufacturing Quantity are required,Nomenclature et quantité de production sont nécessaires, BOM does not contain any stock item,Nomenclature ne contient aucun article en stock, @@ -561,9 +561,9 @@ Colour,Couleur, Combined invoice portion must equal 100%,La portion combinée de la facture doit être égale à 100%, Commercial,Commercial, Commission,Commission, -Commission Rate %,Taux de commission%, +Commission Rate %,Pourcentage de commission, Commission on Sales,Commission sur les ventes, -Commission rate cannot be greater than 100,Taux de commission ne peut pas être supérieure à 100, +Commission rate cannot be greater than 100,Pourcentage de commission ne peut pas être supérieure à 100, Community Forum,Forum de la communauté, Company (not Customer or Supplier) master.,Données de base de la Société (ni les Clients ni les Fournisseurs), Company Abbreviation,Abréviation de la Société, @@ -658,7 +658,7 @@ Create Invoice,Créer une facture, Create Invoices,Créer des factures, Create Job Card,Créer une carte de travail, Create Journal Entry,Créer une entrée de journal, -Create Lead,Créer une piste, +Create Lead,Créer un Prospect, Create Leads,Créer des Prospects, Create Maintenance Visit,Créer une visite de maintenance, Create Material Request,Créer une demande de matériel, @@ -785,7 +785,7 @@ Default BOM for {0} not found,Nomenclature par défaut {0} introuvable, Default BOM not found for Item {0} and Project {1},La nomenclature par défaut n'a pas été trouvée pour l'Article {0} et le Projet {1}, Default Letter Head,En-Tête de Courrier par Défaut, Default Tax Template,Modèle de Taxes par Défaut, -Default Unit of Measure for Item {0} cannot be changed directly because you have already made some transaction(s) with another UOM. You will need to create a new Item to use a different Default UOM.,L’Unité de Mesure par Défaut pour l’Article {0} ne peut pas être modifiée directement parce que vous avez déjà fait une (des) transaction (s) avec une autre unité de mesure. Vous devez créer un nouvel article pour utiliser une UDM par défaut différente., +Default Unit of Measure for Item {0} cannot be changed directly because you have already made some transaction(s) with another UOM. You will need to create a new Item to use a different Default UOM.,L’Unité de Mesure par Défaut pour l’Article {0} ne peut pas être modifiée directement parce que vous avez déjà fait une (des) transaction (s) avec une autre unité de mesure. Vous devez créer un nouvel article pour utiliser une UdM par défaut différente., Default Unit of Measure for Variant '{0}' must be same as in Template '{1}',L’Unité de mesure par défaut pour la variante '{0}' doit être la même que dans le Modèle '{1}', Default settings for buying transactions.,Paramètres par défaut pour les transactions d'achat., Default settings for selling transactions.,Paramètres par défaut pour les transactions de vente., @@ -838,7 +838,7 @@ Difference Account,Compte d’Écart, "Difference Account must be a Asset/Liability type account, since this Stock Reconciliation is an Opening Entry","Le Compte d’Écart doit être un compte de type Actif / Passif, puisque cette Réconciliation de Stock est une écriture d'à-nouveau", Difference Amount,Écart de Montant, Difference Amount must be zero,L’Écart de Montant doit être égal à zéro, -Different UOM for items will lead to incorrect (Total) Net Weight value. Make sure that Net Weight of each item is in the same UOM.,Différentes UDM pour les articles conduira à un Poids Net (Total) incorrect . Assurez-vous que le Poids Net de chaque article a la même unité de mesure ., +Different UOM for items will lead to incorrect (Total) Net Weight value. Make sure that Net Weight of each item is in the same UOM.,Différentes UdM pour les articles conduira à un Poids Net (Total) incorrect . Assurez-vous que le Poids Net de chaque article a la même unité de mesure ., Direct Expenses,Charges Directes, Direct Income,Revenu direct, Disable,Désactiver, @@ -1072,7 +1072,7 @@ For Warehouse is required before Submit,Pour l’Entrepôt est requis avant de V "For an item {0}, quantity must be negative number","Pour l'article {0}, la quantité doit être un nombre négatif", "For an item {0}, quantity must be positive number","Pour un article {0}, la quantité doit être un nombre positif", "For job card {0}, you can only make the 'Material Transfer for Manufacture' type stock entry","Pour la carte de travail {0}, vous pouvez uniquement saisir une entrée de stock de type "Transfert d'article pour fabrication".", -"For row {0} in {1}. To include {2} in Item rate, rows {3} must also be included","Pour la ligne {0} dans {1}. Pour inclure {2} dans le prix de l'Article, les lignes {3} doivent également être incluses", +"For row {0} in {1}. To include {2} in Item rate, rows {3} must also be included","Pour la ligne {0} dans {1}. Pour inclure {2} dans le prix de l'article, les lignes {3} doivent également être incluses", For row {0}: Enter Planned Qty,Pour la ligne {0}: entrez la quantité planifiée, "For {0}, only credit accounts can be linked against another debit entry","Pour {0}, seuls les comptes de crédit peuvent être liés avec une autre écriture de débit", "For {0}, only debit accounts can be linked against another credit entry","Pour {0}, seuls les comptes de débit peuvent être liés avec une autre écriture de crédit", @@ -1235,7 +1235,7 @@ ITC Reversed,CTI inversé, Identifying Decision Makers,Identifier les décideurs, "If Auto Opt In is checked, then the customers will be automatically linked with the concerned Loyalty Program (on save)","Si l'option adhésion automatique est cochée, les clients seront automatiquement liés au programme de fidélité concerné (après l'enregistrement)", "If multiple Pricing Rules continue to prevail, users are asked to set Priority manually to resolve conflict.","Si plusieurs Règles de Prix continuent de prévaloir, les utilisateurs sont invités à définir manuellement la priorité pour résoudre les conflits.", -"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.","Si la règle de tarification sélectionnée est définie pour le «Prix Unitaire», elle écrase la liste de prix. Le prix unitaire de la règle de tarification est le prix unitaire final, donc aucune autre réduction supplémentaire ne doit être appliquée. Par conséquent, dans les transactions telles que la commande client, la commande d'achat, etc., elle sera récupérée dans le champ ""Prix Unitaire"", plutôt que dans le champ ""Tarif de la liste de prix"".", +"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.","Si la règle de tarification sélectionnée est définie pour le 'Prix Unitaire', elle écrase la liste de prix. Le prix unitaire de la règle de tarification est le prix unitaire final, donc aucune autre réduction supplémentaire ne doit être appliquée. Par conséquent, dans les transactions telles que la commande client, la commande d'achat, etc., elle sera récupérée dans le champ 'Prix Unitaire', plutôt que dans le champ 'Tarif de la liste de prix'.", "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.","Si deux Règles de Prix ou plus sont trouvées sur la base des conditions ci-dessus, une Priorité est appliquée. La Priorité est un nombre compris entre 0 et 20 avec une valeur par défaut de zéro (vide). Les nombres les plus élévés sont prioritaires s'il y a plusieurs Règles de Prix avec mêmes conditions.", "If unlimited expiry for the Loyalty Points, keep the Expiry Duration empty or 0.","Si vous souhaitez ne pas mettre de date d'expiration pour les points de fidélité, laissez la durée d'expiration vide ou mettez 0.", "If you have any questions, please get back to us.","Si vous avez des questions, veuillez revenir vers nous.", @@ -1269,7 +1269,7 @@ Income,Revenus, Income Account,Compte de Produits, Income Tax,Impôt sur le revenu, Incoming,Entrant, -Incoming Rate,Taux d'Entrée, +Incoming Rate,Prix d'Entrée, Incorrect number of General Ledger Entries found. You might have selected a wrong Account in the transaction.,Nombre incorrect d'Écritures Grand Livre trouvées. Vous avez peut-être choisi le mauvais Compte dans la transaction., Increment cannot be 0,Incrément ne peut pas être 0, Increment for Attribute {0} cannot be 0,Incrément pour l'Attribut {0} ne peut pas être 0, @@ -1365,7 +1365,7 @@ Item Variants,Variantes de l'Article, Item Variants updated,Variantes d'article mises à jour, Item has variants.,L'article a des variantes., Item must be added using 'Get Items from Purchase Receipts' button,L'article doit être ajouté à l'aide du bouton 'Obtenir des éléments de Reçus d'Achat', -Item valuation rate is recalculated considering landed cost voucher amount,Le taux d'évaluation de l'article est recalculé compte tenu du montant du bon de prix au débarquement, +Item valuation rate is recalculated considering landed cost voucher amount,Le taux de valorisation de l'article est recalculé compte tenu du montant du bon de prix au débarquement, Item variant {0} exists with same attributes,La variante de l'article {0} existe avec les mêmes caractéristiques, Item {0} does not exist,Article {0} n'existe pas, Item {0} does not exist in the system or has expired,L'article {0} n'existe pas dans le système ou a expiré, @@ -1409,7 +1409,7 @@ Lab Test,Test de laboratoire, Lab Test Report,Rapport de test de laboratoire, Lab Test Sample,Échantillon de test de laboratoire, Lab Test Template,Modèle de test de laboratoire, -Lab Test UOM,UDM de test de laboratoire, +Lab Test UOM,UdM de test de laboratoire, Lab Tests and Vital Signs,Tests de laboratoire et signes vitaux, Lab result datetime cannot be before testing datetime,La date et l'heure du résultat de laboratoire ne peuvent pas être avant la date et l'heure du test, Lab testing datetime cannot be before collection datetime,La date et l'heure du test de laboratoire ne peuvent pas être avant la date et l'heure de collecte, @@ -2147,7 +2147,7 @@ Previous Financial Year is not closed,L’Exercice Financier Précédent n’est Price,Prix, Price List,Liste de prix, Price List Currency not selected,Devise de la Liste de Prix non sélectionnée, -Price List Rate,Taux de la Liste des Prix, +Price List Rate,Prix de la Liste des Prix, Price List master.,Données de Base des Listes de Prix, Price List must be applicable for Buying or Selling,La Liste de Prix doit être applicable pour les Achats et les Ventes, Price List {0} is disabled or does not exist,Liste des Prix {0} est désactivée ou n'existe pas, @@ -2288,8 +2288,8 @@ Quotations: ,Devis :, Quotes to Leads or Customers.,Devis de Prospects ou Clients., RFQs are not allowed for {0} due to a scorecard standing of {1},Les Appels d'Offres ne sont pas autorisés pour {0} en raison d'une note de {1} sur la fiche d'évaluation, Range,Plage, -Rate,Taux, -Rate:,Taux:, +Rate,Prix, +Rate:,Prix:, Rating,Évaluation, Raw Material,Matières Premières, Raw Materials,Matières premières, @@ -2426,7 +2426,7 @@ Route,Route, Row # {0}: ,Ligne # {0} :, Row # {0}: Batch No must be same as {1} {2},Ligne # {0} : Le N° de Lot doit être le même que {1} {2}, Row # {0}: Cannot return more than {1} for Item {2},Ligne # {0} : Vous ne pouvez pas retourner plus de {1} pour l’Article {2}, -Row # {0}: Rate cannot be greater than the rate used in {1} {2},Ligne # {0}: Le Taux ne peut pas être supérieur au taux utilisé dans {1} {2}, +Row # {0}: Rate cannot be greater than the rate used in {1} {2},Ligne # {0}: Le prix ne peut pas être supérieur au prix utilisé dans {1} {2}, Row # {0}: Serial No is mandatory,Ligne # {0} : N° de série est obligatoire, Row # {0}: Serial No {1} does not match with {2} {3},Ligne # {0} : N° de série {1} ne correspond pas à {2} {3}, Row #{0} (Payment Table): Amount must be negative,Row # {0} (Table de paiement): le montant doit être négatif, @@ -2434,7 +2434,7 @@ Row #{0} (Payment Table): Amount must be positive,Ligne #{0} (Table de paiement) Row #{0}: Account {1} does not belong to company {2},Ligne # {0}: le compte {1} n'appartient pas à la société {2}, Row #{0}: Allocated Amount cannot be greater than outstanding amount.,Ligne # {0}: montant attribué ne peut pas être supérieur au montant en souffrance., "Row #{0}: Asset {1} cannot be submitted, it is already {2}","Ligne #{0} : L’Actif {1} ne peut pas être soumis, il est déjà {2}", -Row #{0}: Cannot set Rate if amount is greater than billed amount for Item {1}.,Ligne n ° {0}: impossible de définir le tarif si le montant est supérieur au montant facturé pour l'élément {1}., +Row #{0}: Cannot set Rate if amount is greater than billed amount for Item {1}.,Ligne n ° {0}: impossible de définir le prix si le montant est supérieur au montant facturé pour l'élément {1}., Row #{0}: Clearance date {1} cannot be before Cheque Date {2},Ligne #{0} : Date de compensation {1} ne peut pas être antérieure à la Date du Chèque {2}, Row #{0}: Duplicate entry in References {1} {2},Ligne # {0}: entrée en double dans les références {1} {2}, Row #{0}: Expected Delivery Date cannot be before Purchase Order Date,Ligne {0}: la date de livraison prévue ne peut pas être avant la date de commande, @@ -2806,7 +2806,7 @@ Stock Received But Not Billed,Stock Reçus Mais Non Facturés, Stock Reports,Rapports de stock, Stock Summary,Résumé du Stock, Stock Transactions,Transactions du Stock, -Stock UOM,UDM du Stock, +Stock UOM,UdM du Stock, Stock Value,Valeur du Stock, Stock balance in Batch {0} will become negative {1} for Item {2} at Warehouse {3},Solde du stock dans le Lot {0} deviendra négatif {1} pour l'Article {2} à l'Entrepôt {3}, Stock cannot be updated against Delivery Note {0},Stock ne peut pas être mis à jour pour le Bon de Livraison {0}, @@ -2898,7 +2898,7 @@ Sync has been temporarily disabled because maximum retries have been exceeded,La Syntax error in condition: {0},Erreur de syntaxe dans la condition: {0}, Syntax error in formula or condition: {0},Erreur de syntaxe dans la formule ou condition : {0}, System Manager,Responsable Système, -TDS Rate %,Taux de TDS%, +TDS Rate %,Pourcentage de TDS, Tap items to add them here,Choisissez des articles pour les ajouter ici, Target,Cible, Target ({}),Cible ({}), @@ -3161,9 +3161,9 @@ Trial Period End Date Cannot be before Trial Period Start Date,La date de fin de Trialling,Essai, Type of Business,Type de commerce, Types of activities for Time Logs,Types d'activités pour Journaux de Temps, -UOM,UDM, -UOM Conversion factor is required in row {0},Facteur de conversion de l'UDM est obligatoire dans la ligne {0}, -UOM coversion factor required for UOM: {0} in Item: {1},Facteur de coversion UDM requis pour l'UDM : {0} dans l'Article : {1}, +UOM,UdM, +UOM Conversion factor is required in row {0},Facteur de conversion de l'UdM est obligatoire dans la ligne {0}, +UOM coversion factor required for UOM: {0} in Item: {1},Facteur de coversion UdM requis pour l'UdM : {0} dans l'Article : {1}, URL,URL, Unable to find DocType {0},Impossible de trouver le DocType {0}, Unable to find exchange rate for {0} to {1} for key date {2}. Please create a Currency Exchange record manually,Impossible de trouver le taux de change pour {0} à {1} pour la date clé {2}. Veuillez créer une entrée de taux de change manuellement, @@ -3190,7 +3190,7 @@ Update Print Format,Mettre à Jour le Format d'Impression, Update Response,Mettre à jour la réponse, Update bank payment dates with journals.,Mettre à jour les dates de paiement bancaires avec les journaux., Update in progress. It might take a while.,Mise à jour en cours. Ça peut prendre un moment., -Update rate as per last purchase,Taux de mise à jour selon le dernier achat, +Update rate as per last purchase,Mettre à jour les prix selon le dernier prix achat, Update stock must be enable for the purchase invoice {0},La mise à jour du stock doit être activée pour la facture d'achat {0}, Updating Variants...,Mise à jour des variantes ..., Upload your letter head and logo. (you can edit them later).,Charger votre en-tête et logo. (vous pouvez les modifier ultérieurement)., @@ -3294,7 +3294,7 @@ Wednesday,Mercredi, Week,Semaine, Weekdays,Jours de la semaine, Weekly,Hebdomadaire, -"Weight is mentioned,\nPlease mention ""Weight UOM"" too","Poids est mentionné,\nVeuillez aussi mentionner ""UDM de Poids""", +"Weight is mentioned,\nPlease mention ""Weight UOM"" too","Poids est mentionné,\nVeuillez aussi mentionner ""UdM de Poids""", Welcome email sent,Email de bienvenue envoyé, Welcome to ERPNext,Bienvenue sur ERPNext, What do you need help with?,Avec quoi avez vous besoin d'aide ?, @@ -3326,7 +3326,6 @@ You are not authorized to add or update entries before {0},Vous n'êtes pas auto You are not authorized to approve leaves on Block Dates,Vous n'êtes pas autorisé à approuver les congés sur les Dates Bloquées, You are not authorized to set Frozen value,Vous n'êtes pas autorisé à définir des valeurs gelées, You are not present all day(s) between compensatory leave request days,Vous n'êtes pas présent(e) tous les jours vos demandes de congé compensatoire, -You can not change rate if BOM mentioned agianst any item,Vous ne pouvez pas modifier le taux si la nomenclature est mentionnée pour un article, You can not enter current voucher in 'Against Journal Entry' column,Vous ne pouvez pas entrer le bon actuel dans la colonne 'Pour l'Écriture de Journal', You can only have Plans with the same billing cycle in a Subscription,Vous ne pouvez avoir que des plans ayant le même cycle de facturation dans le même abonnement, You can only redeem max {0} points in this order.,Vous pouvez uniquement échanger un maximum de {0} points dans cet commande., @@ -3538,7 +3537,7 @@ Quality Feedback Template,Modèle de commentaires sur la qualité, Rules for applying different promotional schemes.,Règles d'application de différents programmes promotionnels., Shift,Décalage, Show {0},Montrer {0}, -"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series","Caractères spéciaux sauf "-", "#", ".", "/", "{" Et "}" non autorisés dans les séries de nommage", +"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}","Caractères spéciaux sauf "-", "#", ".", "/", "{{" Et "}}" non autorisés dans les séries de nommage {0}", Target Details,Détails de la cible, {0} already has a Parent Procedure {1}.,{0} a déjà une procédure parent {1}., API,API, @@ -3882,7 +3881,7 @@ Only expired allocation can be cancelled,Seule l'allocation expirée peut être Only users with the {0} role can create backdated leave applications,Seuls les utilisateurs avec le rôle {0} peuvent créer des demandes de congé antidatées, Open,Ouvert, Open Contact,Contact ouvert, -Open Lead,Ouvrir le fil, +Open Lead,Ouvrir le Prospect, Opening and Closing,Ouverture et fermeture, Operating Cost as per Work Order / BOM,Coût d'exploitation selon l'ordre de fabrication / nomenclature, Order Amount,Montant de la commande, @@ -3971,7 +3970,7 @@ Queued,File d'Attente, Quick Entry,Écriture Rapide, Quiz {0} does not exist,Le questionnaire {0} n'existe pas, Quotation Amount,Montant du devis, -Rate or Discount is required for the price discount.,Le taux ou la remise est requis pour la remise de prix., +Rate or Discount is required for the price discount.,Le prix ou la remise est requis pour la remise., Reason,Raison, Reconcile Entries,Réconcilier les entrées, Reconcile this account,Réconcilier ce compte, @@ -4348,7 +4347,7 @@ Valid Upto date cannot be before Valid From date,La date de validité valide ne Valid From date not in Fiscal Year {0},Date de début de validité non comprise dans l'exercice {0}, Valid Upto date not in Fiscal Year {0},Valable jusqu'à la date hors exercice {0}, Group Roll No,Groupe Roll Non, -Maintain Same Rate Throughout Sales Cycle,Maintenir le Même Taux Durant le Cycle de Vente, +Maintain Same Rate Throughout Sales Cycle,Maintenir le même prix Durant le Cycle de Vente, "Row {1}: Quantity ({0}) cannot be a fraction. To allow this, disable '{2}' in UOM {3}.","Ligne {1}: la quantité ({0}) ne peut pas être une fraction. Pour autoriser cela, désactivez «{2}» dans UdM {3}.", Must be Whole Number,Doit être un Nombre Entier, Please setup Razorpay Plan ID,Veuillez configurer l'ID du plan Razorpay, @@ -4939,12 +4938,15 @@ Is Cumulative,Est cumulatif, Coupon Code Based,Code de coupon basé, Discount on Other Item,Remise sur un autre article, Apply Rule On Other,Appliquer la règle sur autre, -Party Information,Informations sur la fête, +Party Information,Informations sur le tier, Quantity and Amount,Quantité et montant, Min Qty,Qté Min, Max Qty,Qté Max, -Min Amt,Min Amt, -Max Amt,Max Amt, +Min Amt,Montant Min, +Max Amt,Montant Max, +"If rate is zero them item will be treated as ""Free Item""",Si le prix est à 0 alors l'article sera traité comme article gratuit +Is Recursive,Est récursif +"Discounts to be applied in sequential ranges like buy 1 get 1, buy 2 get 2, buy 3 get 3 and so on","La remise sera appliquée séquentiellement telque : acheter 1 => recupérer 1, acheter 2 => recupérer 2, acheter 3 => recupérer 3, etc..." Period Settings,Paramètres de période, Margin,Marge, Margin Type,Type de Marge, @@ -4961,7 +4963,7 @@ Threshold for Suggestion,Seuil de suggestion, System will notify to increase or decrease quantity or amount ,Le système notifiera d'augmenter ou de diminuer la quantité ou le montant, "Higher the number, higher the priority","Plus le nombre est grand, plus la priorité est haute", Apply Multiple Pricing Rules,Appliquer plusieurs règles de tarification, -Apply Discount on Rate,Appliquer une réduction sur le taux, +Apply Discount on Rate,Appliquer une réduction sur le prix, Validate Applied Rule,Valider la règle appliquée, Rule Description,Description de la règle, Pricing Rule Help,Aide pour les Règles de Tarification, @@ -5050,19 +5052,18 @@ End date of current invoice's period,Date de fin de la période de facturation e Update Auto Repeat Reference,Mettre à jour la référence de répétition automatique, Purchase Invoice Advance,Avance sur Facture d’Achat, Purchase Invoice Item,Article de la Facture d'Achat, -Quantity and Rate,Quantité et Taux, +Quantity and Rate,Quantité et Prix, Received Qty,Qté Reçue, Accepted Qty,Quantité acceptée, Rejected Qty,Qté Rejetée, -UOM Conversion Factor,Facteur de Conversion de l'UDM, +UOM Conversion Factor,Facteur de Conversion de l'UdM, Discount on Price List Rate (%),Remise sur la Liste des Prix (%), Price List Rate (Company Currency),Taux de la Liste de Prix (Devise Société), -Rate ,Taux, Rate (Company Currency),Prix (Devise Société), Amount (Company Currency),Montant (Devise de la Société), Is Free Item,Est un article gratuit, -Net Rate,Taux Net, -Net Rate (Company Currency),Taux Net (Devise Société), +Net Rate,Prix Net, +Net Rate (Company Currency),Prix Net (Devise Société), Net Amount (Company Currency),Montant Net (Devise Société), Item Tax Amount Included in Value,Montant de la taxe incluse dans la valeur, Landed Cost Voucher Amount,Montant de la Référence de Coût au Débarquement, @@ -5080,14 +5081,14 @@ Enable Deferred Expense,Activer les frais reportés, Service Start Date,Date de début du service, Service End Date,Date de fin du service, Allow Zero Valuation Rate,Autoriser un Taux de Valorisation Égal à Zéro, -Item Tax Rate,Taux de la Taxe sur l'Article, +Item Tax Rate,Prix de la Taxe sur l'Article, Tax detail table fetched from item master as a string and stored in this field.\nUsed for Taxes and Charges,La table de détails de taxe est récupérée depuis les données de base de l'article comme une chaîne de caractères et stockée dans ce champ. Elle est utilisée pour les Taxes et Frais., Purchase Order Item,Article de la Commande d'Achat, Purchase Receipt Detail,Détail du reçu d'achat, Item Weight Details,Détails du poids de l'article, Weight Per Unit,Poids par unité, Total Weight,Poids total, -Weight UOM,UDM de Poids, +Weight UOM,UdM de Poids, Page Break,Saut de Page, Consider Tax or Charge for,Tenir Compte de la Taxe et des Frais pour, Valuation and Total,Valorisation et Total, @@ -5098,8 +5099,8 @@ On Previous Row Amount,Le Montant de la Rangée Précédente, On Previous Row Total,Le Total de la Rangée Précédente, On Item Quantity,Sur quantité d'article, Reference Row #,Ligne de Référence #, -Is this Tax included in Basic Rate?,Cette Taxe est-elle incluse dans le Taux de Base ?, -"If checked, the tax amount will be considered as already included in the Print Rate / Print Amount","Si cochée, le montant de la taxe sera considéré comme déjà inclus dans le Taux d'Impression / Prix d'Impression", +Is this Tax included in Basic Rate?,Cette Taxe est-elle incluse dans le Prix de Base ?, +"If checked, the tax amount will be considered as already included in the Print Rate / Print Amount","Si cochée, le montant de la taxe sera considéré comme déjà inclus dans le Taux / Prix des documents (PDF, impressions)", Account Head,Compte Principal, Tax Amount After Discount Amount,Montant de la Taxe après Remise, Item Wise Tax Detail ,Détail de la taxe de l'article Wise, @@ -5147,7 +5148,7 @@ Accounting Details,Détails Comptabilité, Debit To,Débit Pour, Is Opening Entry,Est Écriture Ouverte, C-Form Applicable,Formulaire-C Applicable, -Commission Rate (%),Taux de Commission (%), +Commission Rate (%),Pourcentage de Commission, Sales Team1,Équipe des Ventes 1, Against Income Account,Pour le Compte de Produits, Sales Invoice Advance,Avance sur Facture de Vente, @@ -5155,11 +5156,11 @@ Advance amount,Montant de l'Avance, Sales Invoice Item,Article de la Facture de Vente, Customer's Item Code,Code de l'Article du Client, Brand Name,Nom de la Marque, -Qty as per Stock UOM,Qté par UDM du Stock, +Qty as per Stock UOM,Qté par UdM du Stock, Discount and Margin,Remise et Marge, -Rate With Margin,Tarif Avec Marge, -Discount (%) on Price List Rate with Margin,Remise (%) sur le Tarif de la Liste de Prix avec la Marge, -Rate With Margin (Company Currency),Taux avec marge (devise de l'entreprise), +Rate With Margin,Prix Avec Marge, +Discount (%) on Price List Rate with Margin,Remise (%) sur le prix de la Liste de Prix avec la Marge, +Rate With Margin (Company Currency),Prix avec marge (devise de l'entreprise), Delivered By Supplier,Livré par le Fournisseur, Deferred Revenue,Produits comptabilisés d'avance, Deferred Revenue Account,Compte de produits comptabilisés d'avance, @@ -5503,7 +5504,7 @@ Blanket Order Rate,Prix unitaire de commande avec limites, Returned Qty,Qté Retournée, Purchase Order Item Supplied,Article Fourni depuis la Commande d'Achat, BOM Detail No,N° de Détail de la nomenclature, -Stock Uom,UDM du Stock, +Stock Uom,UdM du Stock, Raw Material Item Code,Code d’Article de Matière Première, Supplied Qty,Qté Fournie, Purchase Receipt Item Supplied,Articles Fournis du Reçus d’Achat, @@ -5721,7 +5722,7 @@ Contact Mobile No,N° de Portable du Contact, Enter name of campaign if source of enquiry is campaign,Entrez le nom de la campagne si la source de l'enquête est une campagne, Opportunity Date,Date d'Opportunité, Opportunity Item,Article de l'Opportunité, -Basic Rate,Taux de Base, +Basic Rate,Prix de Base, Stage Name,Nom de scène, Social Media Post,Publication sur les réseaux sociaux, Post Status,Statut du message, @@ -6151,7 +6152,7 @@ Drug Name / Description,Nom / description du médicament, Dosage,Dosage, Dosage by Time Interval,Dosage par intervalle de temps, Interval,Intervalle, -Interval UOM,UDM d'Intervalle, +Interval UOM,UdM d'Intervalle, Hour,Heure, Update Schedule,Mettre à Jour le Calendrier, Exercise,Exercice, @@ -7025,7 +7026,7 @@ Petrol,Essence, Diesel,Diesel, Natural Gas,Gaz Naturel, Electric,Électrique, -Fuel UOM,UDM Carburant, +Fuel UOM,UdM Carburant, Last Carbon Check,Dernière Vérification Carbone, Wheels,Roues, Doors,Portes, @@ -7184,7 +7185,7 @@ Item to be manufactured or repacked,Article à produire ou à réemballer, Quantity of item obtained after manufacturing / repacking from given quantities of raw materials,Quantité d'article obtenue après production / reconditionnement des quantités données de matières premières, Set rate of sub-assembly item based on BOM,Définir le prix des articles de sous-assemblage en fonction de la nomenclature, Allow Alternative Item,Autoriser un article alternatif, -Item UOM,UDM de l'Article, +Item UOM,UdM de l'Article, Conversion Rate,Taux de Conversion, Rate Of Materials Based On,Prix des Matériaux Basé sur, With Operations,Avec des Opérations, @@ -7218,8 +7219,8 @@ Qty Consumed Per Unit,Qté Consommée Par Unité, Include Item In Manufacturing,Inclure l'article dans la fabrication, BOM Item,Article de la nomenclature, Item operation,Opération de l'article, -Rate & Amount,Taux et Montant, -Basic Rate (Company Currency),Taux de Base (Devise de la Société ), +Rate & Amount,Prix et Montant, +Basic Rate (Company Currency),Prix de Base (Devise de la Société ), Scrap %,% de Rebut, Original Item,Article original, BOM Operation,Opération de la nomenclature (gamme), @@ -7464,8 +7465,8 @@ Website Attribute,Attribut de site Web, Attribute,Attribut, Website Filter Field,Champ de filtrage de site Web, Activity Cost,Coût de l'Activité, -Billing Rate,Taux de Facturation, -Costing Rate,Taux des Coûts, +Billing Rate,Prix de Facturation, +Costing Rate,Tarifs des Coûts, title,Titre, Projects User,Utilisateur/Intervenant Projets, Default Costing Rate,Coût de Revient par Défaut, @@ -7928,7 +7929,7 @@ Territory Manager,Responsable Régional, For reference,Pour référence, Territory Targets,Objectifs Régionaux, Set Item Group-wise budgets on this Territory. You can also include seasonality by setting the Distribution.,Définir des budgets par Groupes d'Articles sur ce Territoire. Vous pouvez également inclure de la saisonnalité en définissant la Répartition., -UOM Name,Nom UDM, +UOM Name,Nom UdM, Check this to disallow fractions. (for Nos),Cochez cette case pour interdire les fractions. (Pour les numéros), Website Item Group,Groupe d'Articles du Site Web, Cross Listing of Item in multiple groups,Liste Croisée d'Articles dans plusieurs groupes, @@ -7963,7 +7964,7 @@ Reserved Quantity,Quantité Réservée, Actual Quantity,Quantité Réelle, Requested Quantity,Quantité Demandée, Reserved Qty for sub contract,Qté réservée pour le sous-contrat, -Moving Average Rate,Taux Mobile Moyen, +Moving Average Rate,Prix moyen pondéré, FCFS Rate,Montant PAPS, Customs Tariff Number,Tarifs Personnalisés, Tariff Number,Tarif, @@ -8200,10 +8201,10 @@ To Package No.,Au N° de Paquet, If more than one package of the same type (for print),Si plus d'un paquet du même type (pour l'impression), Package Weight Details,Détails du Poids du Paquet, The net weight of this package. (calculated automatically as sum of net weight of items),Le poids net de ce paquet. (Calculé automatiquement comme la somme du poids net des articles), -Net Weight UOM,UDM Poids Net, +Net Weight UOM,UdM Poids Net, Gross Weight,Poids Brut, The gross weight of the package. Usually net weight + packaging material weight. (for print),Le poids brut du colis. Habituellement poids net + poids du matériau d'emballage. (Pour l'impression), -Gross Weight UOM,UDM du Poids Brut, +Gross Weight UOM,UdM du Poids Brut, Packing Slip Item,Article Emballé, DN Detail,Détail du Bon de Livraison, STO-PICK-.YYYY.-,STO-PICK-.YYYY.-, @@ -8217,7 +8218,7 @@ Pick List Item,Élément de la liste de choix, Picked Qty,Quantité choisie, Price List Master,Données de Base des Listes de Prix, Price List Name,Nom de la Liste de Prix, -Price Not UOM Dependent,Prix non dépendant de l'UOM, +Price Not UOM Dependent,Prix non dépendant de l'UdM, Applicable for Countries,Applicable pour les Pays, Price List Country,Pays de la Liste des Prix, MAT-PRE-.YYYY.-,MAT-PRE-YYYY.-, @@ -8296,7 +8297,7 @@ Purchase Receipt No,N° du Reçu d'Achat, Inspection Required,Inspection obligatoire, From BOM,Depuis la nomenclature, For Quantity,Pour la Quantité, -As per Stock UOM,Selon UDM du Stock, +As per Stock UOM,Selon UdM du Stock, Including items for sub assemblies,Incluant les articles pour des sous-ensembles, Default Source Warehouse,Entrepôt Source par Défaut, Source Warehouse Address,Adresse de l'entrepôt source, @@ -8311,7 +8312,7 @@ Total Additional Costs,Total des Coûts Additionnels, Customer or Supplier Details,Détails du Client ou du Fournisseur, Per Transferred,Par transféré, Stock Entry Detail,Détails de l'Écriture de Stock, -Basic Rate (as per Stock UOM),Taux de base (comme l’UDM du Stock), +Basic Rate (as per Stock UOM),Prix de base (comme l’UdM du Stock), Basic Amount,Montant de Base, Additional Cost,Frais Supplémentaire, Serial No / Batch,N° de Série / Lot, @@ -8323,7 +8324,7 @@ Stock Entry Child,Entrée de stock enfant, PO Supplied Item,PO article fourni, Reference Purchase Receipt,Reçu d'achat de référence, Stock Ledger Entry,Écriture du Livre d'Inventaire, -Outgoing Rate,Taux Sortant, +Outgoing Rate,Prix Sortant, Actual Qty After Transaction,Qté Réelle Après Transaction, Stock Value Difference,Différence de Valeur du Sock, Stock Queue (FIFO),File d'Attente du Stock (FIFO), @@ -8341,7 +8342,7 @@ Quantity Difference,Différence de Quantité, Amount Difference,Différence de Montant, Item Naming By,Nomenclature d'Article Par, Default Item Group,Groupe d'Éléments par Défaut, -Default Stock UOM,UDM par Défaut des Articles, +Default Stock UOM,UdM par Défaut des Articles, Sample Retention Warehouse,Entrepôt de stockage des échantillons, Default Valuation Method,Méthode de Valorisation par Défaut, Show Barcode Field,Afficher Champ Code Barre, @@ -8355,8 +8356,8 @@ Stock Frozen Upto,Stock Gelé Jusqu'au, Batch Identification,Identification par lots, Use Naming Series,Utiliser la série de noms, Naming Series Prefix,Préfix du nom de série, -UOM Category,Catégorie d'unité de mesure (UDM), -UOM Conversion Detail,Détails de Conversion de l'UDM, +UOM Category,Catégorie d'unité de mesure (UdM), +UOM Conversion Detail,Détails de Conversion de l'UdM, Variant Field,Champ de Variante, A logical Warehouse against which stock entries are made.,Un Entrepôt logique dans lequel les entrées en stock sont faites., Warehouse Detail,Détail de l'Entrepôt, @@ -8509,7 +8510,7 @@ Item Price Stock,Stock et prix de l'article, Item Prices,Prix des Articles, Item Shortage Report,Rapport de Rupture de Stock d'Article, Item Variant Details,Détails de la variante de l'article, -Item-wise Price List Rate,Taux de la Liste des Prix par Article, +Item-wise Price List Rate,Prix de la Liste des Prix par Article, Item-wise Purchase History,Historique d'Achats par Article, Item-wise Purchase Register,Registre des Achats par Article, Item-wise Sales History,Historique des Ventes par Article, @@ -8561,7 +8562,7 @@ Sales Partner Target Variance based on Item Group,Variance cible du partenaire c Sales Partner Transaction Summary,Récapitulatif des transactions du partenaire commercial, Sales Partners Commission,Commission des Partenaires de Vente, Invoiced Amount (Exclusive Tax),Montant facturé (taxe exclusive), -Average Commission Rate,Taux Moyen de la Commission, +Average Commission Rate,Coût Moyen de la Commission, Sales Payment Summary,Résumé du paiement des ventes, Sales Person Commission Summary,Récapitulatif de la commission des ventes, Sales Person Target Variance Based On Item Group,Écart cible du commercial basé sur le groupe de postes, @@ -8815,7 +8816,7 @@ Generate New Invoices Past Due Date,Générer de nouvelles factures en retard, New invoices will be generated as per schedule even if current invoices are unpaid or past due date,"De nouvelles factures seront générées selon le calendrier, même si les factures actuelles sont impayées ou en retard", Document Type ,Type de document, Subscription Price Based On,Prix d'abonnement basé sur, -Fixed Rate,Taux fixe, +Fixed Rate,Tarif fixe, Based On Price List,Basé sur la liste de prix, Monthly Rate,Tarif mensuel, Cancel Subscription After Grace Period,Annuler l'abonnement après la période de grâce, @@ -8886,10 +8887,6 @@ Practitioner Name,Nom du praticien, Enter a name for the Clinical Procedure Template,Entrez un nom pour le modèle de procédure clinique, Set the Item Code which will be used for billing the Clinical Procedure.,Définissez le code article qui sera utilisé pour facturer la procédure clinique., Select an Item Group for the Clinical Procedure Item.,Sélectionnez un groupe d'articles pour l'article de procédure clinique., -Clinical Procedure Rate,Taux de procédure clinique, -Check this if the Clinical Procedure is billable and also set the rate.,Cochez cette case si la procédure clinique est facturable et définissez également le tarif., -Check this if the Clinical Procedure utilises consumables. Click ,Vérifiez ceci si la procédure clinique utilise des consommables. Cliquez sur, - to know more,en savoir plus, "You can also set the Medical Department for the template. After saving the document, an Item will automatically be created for billing this Clinical Procedure. You can then use this template while creating Clinical Procedures for Patients. Templates save you from filling up redundant data every single time. You can also create templates for other operations like Lab Tests, Therapy Sessions, etc.","Vous pouvez également définir le service médical du modèle. Après avoir enregistré le document, un élément sera automatiquement créé pour facturer cette procédure clinique. Vous pouvez ensuite utiliser ce modèle lors de la création de procédures cliniques pour les patients. Les modèles vous évitent de remplir des données redondantes à chaque fois. Vous pouvez également créer des modèles pour d'autres opérations telles que des tests de laboratoire, des séances de thérapie, etc.", Descriptive Test Result,Résultat du test descriptif, Allow Blank,Autoriser le blanc, @@ -9033,7 +9030,7 @@ Work In Progress Warehouse,Entrepôt de travaux en cours, This Warehouse will be auto-updated in the Work In Progress Warehouse field of Work Orders.,Cet entrepôt sera mis à jour automatiquement dans le champ Entrepôt de travaux en cours des bons de travail., Finished Goods Warehouse,Entrepôt de produits finis, This Warehouse will be auto-updated in the Target Warehouse field of Work Order.,Cet entrepôt sera mis à jour automatiquement dans le champ Entrepôt cible de l'ordre de fabrication. -"If ticked, the BOM cost will be automatically updated based on Valuation Rate / Price List Rate / last purchase rate of raw materials.","Si coché, le coût de la nomenclature sera automatiquement mis à jour en fonction du taux de valorisation / tarif tarifaire / dernier taux d'achat des matières premières.", +"If ticked, the BOM cost will be automatically updated based on Valuation Rate / Price List Rate / last purchase rate of raw materials.","Si coché, le coût de la nomenclature sera automatiquement mis à jour en fonction du taux de valorisation / prix de la liste prix / dernier prix d'achat des matières premières.", Source Warehouses (Optional),Entrepôts d'origine (facultatif), "System will pickup the materials from the selected warehouses. If not specified, system will create material request for purchase.","Le système ramassera les matériaux dans les entrepôts sélectionnés. S'il n'est pas spécifié, le système créera une demande de matériel pour l'achat.", Lead Time,Délai de mise en œuvre, @@ -9107,7 +9104,7 @@ MAT-PR-RET-.YYYY.-,MAT-PR-RET-.YYYY.-, Track this Purchase Receipt against any Project,Suivre ce reçu d'achat par rapport à n'importe quel projet, Please Select a Supplier,Veuillez sélectionner un fournisseur, Add to Transit,Ajouter à Transit, -Set Basic Rate Manually,Définir manuellement le taux de base, +Set Basic Rate Manually,Définir manuellement le prix de base, "By default, the Item Name is set as per the Item Code entered. If you want Items to be named by a ","Par défaut, le nom de l'article est défini selon le code d'article entré. Si vous souhaitez que les éléments soient nommés par un", Set a Default Warehouse for Inventory Transactions. This will be fetched into the Default Warehouse in the Item master.,Définissez un entrepôt par défaut pour les mouvements de stock. Ce sera récupéré dans l'entrepôt par défaut dans la base d'articles., "This will allow stock items to be displayed in negative values. Using this option depends on your use case. With this option unchecked, the system warns before obstructing a transaction that is causing negative stock.","Cela permettra aux articles en stock d'être affichés avec des valeurs négatives. L'utilisation de cette option dépend de votre cas d'utilisation. Lorsque cette option n'est pas cochée, le système avertit avant d'entraver une transaction entraînant un stock négatif.", @@ -9495,7 +9492,7 @@ Normal Range: ,Plage normale:, Row #{0}: Check Out datetime cannot be less than Check In datetime,Ligne n ° {0}: la date de sortie ne peut pas être inférieure à la date de sortie, "Missing required details, did not create Inpatient Record","Détails requis manquants, n'a pas créé de dossier d'hospitalisation", Unbilled Invoices,Factures non facturées, -Standard Selling Rate should be greater than zero.,Le taux de vente standard doit être supérieur à zéro., +Standard Selling Rate should be greater than zero.,Le prix de vente standard doit être supérieur à zéro., Conversion Factor is mandatory,Le facteur de conversion est obligatoire, Row #{0}: Conversion Factor is mandatory,Ligne n ° {0}: le facteur de conversion est obligatoire, Sample Quantity cannot be negative or 0,La quantité d'échantillon ne peut pas être négative ou 0, @@ -9559,7 +9556,7 @@ The Request for Quotation can be accessed by clicking on the following button,La Regards,Cordialement, Please click on the following button to set your new password,Veuillez cliquer sur le bouton suivant pour définir votre nouveau mot de passe, Update Password,Mettre à jour le mot de passe, -Row #{}: Selling rate for item {} is lower than its {}. Selling {} should be atleast {},Ligne n ° {}: le taux de vente de l'article {} est inférieur à son {}. La vente {} doit être au moins {}, +Row #{}: Selling rate for item {} is lower than its {}. Selling {} should be atleast {},Ligne n ° {}: le prix de vente de l'article {} est inférieur à son {}. La vente {} doit être au moins {}, You can alternatively disable selling price validation in {} to bypass this validation.,Vous pouvez également désactiver la validation du prix de vente dans {} pour contourner cette validation., Invalid Selling Price,Prix de vente invalide, Address needs to be linked to a Company. Please add a row for Company in the Links table.,L'adresse doit être liée à une entreprise. Veuillez ajouter une ligne pour Entreprise dans le tableau Liens., @@ -9581,7 +9578,7 @@ Only select this if you have set up the Cash Flow Mapper documents,Sélectionnez Payment Channel,Canal de paiement, Is Purchase Order Required for Purchase Invoice & Receipt Creation?,Une Commande d'Achat est-il requis pour la création de factures d'achat et de reçus?, Is Purchase Receipt Required for Purchase Invoice Creation?,Un reçu d'achat est-il requis pour la création d'une facture d'achat?, -Maintain Same Rate Throughout the Purchase Cycle,Maintenir le même taux tout au long du cycle d'achat, +Maintain Same Rate Throughout the Purchase Cycle,Maintenir les même prix tout au long du cycle d'achat, Allow Item To Be Added Multiple Times in a Transaction,Autoriser l'ajout d'un article plusieurs fois dans une transaction, Suppliers,Fournisseurs, Send Emails to Suppliers,Envoyer des e-mails aux fournisseurs, @@ -9633,7 +9630,7 @@ Plan operations X days in advance,Planifier les opérations X jours à l'avance, Time Between Operations (Mins),Temps entre les opérations (minutes), Default: 10 mins,Par défaut: 10 minutes, Overproduction for Sales and Work Order,Surproduction pour les ventes et les bons de travail, -"Update BOM cost automatically via scheduler, based on the latest Valuation Rate/Price List Rate/Last Purchase Rate of raw materials","Mettre à jour automatiquement le coût de la nomenclature via le planificateur, en fonction du dernier taux de valorisation / tarif tarifaire / dernier taux d'achat de matières premières", +"Update BOM cost automatically via scheduler, based on the latest Valuation Rate/Price List Rate/Last Purchase Rate of raw materials","Mettre à jour automatiquement le coût de la nomenclature via le planificateur, en fonction du dernier taux de valorisation / prix de la liste de prix / dernier prix d'achat de matières premières", Purchase Order already created for all Sales Order items,Commande d'Achat déjà créé pour tous les articles de commande client, Select Items,Sélectionner des éléments, Against Default Supplier,Contre le fournisseur par défaut, @@ -9641,14 +9638,14 @@ Auto close Opportunity after the no. of days mentioned above,Opportunité de fer Is Sales Order Required for Sales Invoice & Delivery Note Creation?,Une commande client est-elle requise pour la création de factures clients et de bons de livraison?, Is Delivery Note Required for Sales Invoice Creation?,Un bon de livraison est-il nécessaire pour la création de factures de vente?, How often should Project and Company be updated based on Sales Transactions?,À quelle fréquence le projet et l'entreprise doivent-ils être mis à jour en fonction des transactions de vente?, -Allow User to Edit Price List Rate in Transactions,Autoriser l'utilisateur à modifier le tarif tarifaire dans les transactions, +Allow User to Edit Price List Rate in Transactions,Autoriser l'utilisateur à modifier le prix de la liste prix dans les transactions, Allow Item to Be Added Multiple Times in a Transaction,Autoriser l'ajout d'un article plusieurs fois dans une transaction, Allow Multiple Sales Orders Against a Customer's Purchase Order,Autoriser plusieurs commandes client par rapport à la commande d'achat d'un client, -Validate Selling Price for Item Against Purchase Rate or Valuation Rate,Valider le prix de vente de l'article par rapport au taux d'achat ou au taux de valorisation, +Validate Selling Price for Item Against Purchase Rate or Valuation Rate,Valider le prix de vente de l'article par rapport au prix d'achat ou au taux de valorisation, Hide Customer's Tax ID from Sales Transactions,Masquer le numéro d'identification fiscale du client dans les transactions de vente, "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.","Le pourcentage que vous êtes autorisé à recevoir ou à livrer plus par rapport à la quantité commandée. Par exemple, si vous avez commandé 100 unités et que votre allocation est de 10%, vous êtes autorisé à recevoir 110 unités.", Action If Quality Inspection Is Not Submitted,Action si l'inspection qualité n'est pas soumise, -Auto Insert Price List Rate If Missing,Taux de liste de prix d'insertion automatique s'il est manquant, +Auto Insert Price List Rate If Missing,Insérer automatiquement le prix dand liste de prix s'il est manquant, Automatically Set Serial Nos Based on FIFO,Définir automatiquement les numéros de série en fonction de FIFO, Set Qty in Transactions Based on Serial No Input,Définir la quantité dans les transactions en fonction du numéro de série, Raise Material Request When Stock Reaches Re-order Level,Augmenter la demande d'article lorsque le stock atteint le niveau de commande, @@ -9722,7 +9719,7 @@ No Inpatient Record found against patient {0},Aucun dossier d'hospitalisation tr An Inpatient Medication Order {0} against Patient Encounter {1} already exists.,Une ordonnance de médicament pour patients hospitalisés {0} contre rencontre avec un patient {1} existe déjà., Allow In Returns,Autoriser les retours, Hide Unavailable Items,Masquer les éléments non disponibles, -Apply Discount on Discounted Rate,Appliquer une remise sur un tarif réduit, +Apply Discount on Discounted Rate,Appliquer une remise sur un prix réduit, Therapy Plan Template,Modèle de plan de thérapie, Fetching Template Details,Récupération des détails du modèle, Linked Item Details,Détails de l'élément lié, @@ -9875,8 +9872,8 @@ Allowed Items,Articles autorisés Party Specific Item,Restriction d'article disponible Restrict Items Based On,Type de critére de restriction Based On Value,critére de restriction -Unit of Measure (UOM),Unité de mesure (UDM), -Unit Of Measure (UOM),Unité de mesure (UDM), +Unit of Measure (UOM),Unité de mesure (UdM), +Unit Of Measure (UOM),Unité de mesure (UdM), CRM Settings,Paramètres CRM Do Not Explode,Ne pas décomposer Quick Access, Accés rapides @@ -9927,4 +9924,3 @@ Enable Reviews and Ratings,Activer les avis et notes Enable Recommendations,Activer les recommendations Item Search Settings,Paramétrage de la recherche d'article Purchase demande,Demande de materiel -Calendar,Calendier diff --git a/erpnext/translations/gu.csv b/erpnext/translations/gu.csv index 5c2b520d9e..97adac94db 100644 --- a/erpnext/translations/gu.csv +++ b/erpnext/translations/gu.csv @@ -3537,7 +3537,7 @@ Quality Feedback Template,ગુણવત્તા પ્રતિસાદ Temp Rules for applying different promotional schemes.,વિવિધ પ્રમોશનલ યોજનાઓ લાગુ કરવાના નિયમો., Shift,પાળી, Show {0},બતાવો {0}, -"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series",""-", "#", ".", "/", "{" અને "}" સિવાયના વિશેષ અક્ષરો નામકરણ શ્રેણીમાં મંજૂરી નથી", +"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}",""-", "#", ".", "/", "{{" અને "}}" સિવાયના વિશેષ અક્ષરો નામકરણ શ્રેણીમાં મંજૂરી નથી {0}", Target Details,લક્ષ્યાંક વિગતો, {0} already has a Parent Procedure {1}.,{0} પાસે પહેલેથી જ પિતૃ કાર્યવાહી છે {1}., API,API, diff --git a/erpnext/translations/he.csv b/erpnext/translations/he.csv index 29e6f6afc3..22b252261f 100644 --- a/erpnext/translations/he.csv +++ b/erpnext/translations/he.csv @@ -3537,7 +3537,7 @@ Quality Feedback Template,תבנית משוב איכותית, Rules for applying different promotional schemes.,כללים ליישום תוכניות קידום מכירות שונות., Shift,מִשׁמֶרֶת, Show {0},הצג את {0}, -"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series","תווים מיוחדים למעט "-", "#", ".", "/", "{" ו- "}" אינם מורשים בסדרות שמות", +"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}","{0} תווים מיוחדים למעט "-", "#", ".", "/", "{{" ו- "}}" אינם מורשים בסדרות שמות", Target Details,פרטי יעד, {0} already has a Parent Procedure {1}.,{0} כבר יש נוהל הורים {1}., API,ממשק API, diff --git a/erpnext/translations/hi.csv b/erpnext/translations/hi.csv index c385fc6ef6..ca41cf3d9f 100644 --- a/erpnext/translations/hi.csv +++ b/erpnext/translations/hi.csv @@ -3537,7 +3537,7 @@ Quality Feedback Template,गुणवत्ता प्रतिक्रि Rules for applying different promotional schemes.,विभिन्न प्रचार योजनाओं को लागू करने के लिए नियम।, Shift,खिसक जाना, Show {0},{0} दिखाएं, -"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series",""-", "#", "।", "/", "{" और "}" को छोड़कर विशेष वर्ण श्रृंखला में अनुमति नहीं है", +"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}",""-", "#", "।", "/", "{{" और "}}" को छोड़कर विशेष वर्ण श्रृंखला {0} में अनुमति नहीं है", Target Details,लक्ष्य विवरण, {0} already has a Parent Procedure {1}.,{0} पहले से ही एक पेरेंट प्रोसीजर {1} है।, API,एपीआई, diff --git a/erpnext/translations/hr.csv b/erpnext/translations/hr.csv index a544e98868..319b80b7c4 100644 --- a/erpnext/translations/hr.csv +++ b/erpnext/translations/hr.csv @@ -3537,7 +3537,7 @@ Quality Feedback Template,Predložak povratne informacije o kvaliteti, Rules for applying different promotional schemes.,Pravila za primjenu različitih promotivnih shema., Shift,smjena, Show {0},Prikaži {0}, -"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series","Posebni znakovi osim "-", "#", ".", "/", "{" I "}" nisu dopušteni u imenovanju serija", +"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}","Posebni znakovi osim "-", "#", ".", "/", "{{" I "}}" nisu dopušteni u imenovanju serija {0}", Target Details,Pojedinosti cilja, {0} already has a Parent Procedure {1}.,{0} već ima roditeljski postupak {1}., API,API, diff --git a/erpnext/translations/hu.csv b/erpnext/translations/hu.csv index 29f347ecbc..06647281cd 100644 --- a/erpnext/translations/hu.csv +++ b/erpnext/translations/hu.csv @@ -3537,7 +3537,7 @@ Quality Feedback Template,Minőségi visszajelző sablon, Rules for applying different promotional schemes.,Különböző promóciós rendszerek alkalmazásának szabályai., Shift,Váltás, Show {0},Mutasd {0}, -"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series","Speciális karakterek, kivéve "-", "#", ".", "/", "{" És "}", a sorozatok elnevezése nem megengedett", +"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}","Speciális karakterek, kivéve "-", "#", ".", "/", "{{" És "}}", a sorozatok elnevezése nem megengedett {0}", Target Details,Cél részletei, {0} already has a Parent Procedure {1}.,A (z) {0} már rendelkezik szülői eljárással {1}., API,API, diff --git a/erpnext/translations/id.csv b/erpnext/translations/id.csv index 7175ad2fa7..1e50747023 100644 --- a/erpnext/translations/id.csv +++ b/erpnext/translations/id.csv @@ -3537,7 +3537,7 @@ Quality Feedback Template,Template Umpan Balik Kualitas, Rules for applying different promotional schemes.,Aturan untuk menerapkan berbagai skema promosi., Shift,Bergeser, Show {0},Tampilkan {0}, -"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series","Karakter Khusus kecuali "-", "#", ".", "/", "{" Dan "}" tidak diizinkan dalam rangkaian penamaan", +"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}","Karakter Khusus kecuali "-", "#", ".", "/", "{{" Dan "}}" tidak diizinkan dalam rangkaian penamaan {0}", Target Details,Detail Target, {0} already has a Parent Procedure {1}.,{0} sudah memiliki Prosedur Induk {1}., API,API, diff --git a/erpnext/translations/is.csv b/erpnext/translations/is.csv index 5f56aff3dc..c20c21eef8 100644 --- a/erpnext/translations/is.csv +++ b/erpnext/translations/is.csv @@ -3537,7 +3537,7 @@ Quality Feedback Template,Sniðmát fyrir gæði gæða, Rules for applying different promotional schemes.,Reglur um beitingu mismunandi kynningarkerfa., Shift,Vakt, Show {0},Sýna {0}, -"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series","Sérstafir nema "-", "#", ".", "/", "{" Og "}" ekki leyfðar í nafngiftiröð", +"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}","Sérstafir nema "-", "#", ".", "/", "{{" Og "}}" ekki leyfðar í nafngiftiröð {0}", Target Details,Upplýsingar um markmið, {0} already has a Parent Procedure {1}.,{0} er þegar með foreldraferli {1}., API,API, diff --git a/erpnext/translations/it.csv b/erpnext/translations/it.csv index 3a1d73f344..3d15d5594d 100644 --- a/erpnext/translations/it.csv +++ b/erpnext/translations/it.csv @@ -3537,7 +3537,7 @@ Quality Feedback Template,Modello di feedback sulla qualità, Rules for applying different promotional schemes.,Regole per l'applicazione di diversi schemi promozionali., Shift,Cambio, Show {0},Mostra {0}, -"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series","Caratteri speciali tranne "-", "#", ".", "/", "{" E "}" non consentiti nelle serie di nomi", +"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}","Caratteri speciali tranne "-", "#", ".", "/", "{{" E "}}" non consentiti nelle serie di nomi {0}", Target Details,Dettagli target, {0} already has a Parent Procedure {1}.,{0} ha già una procedura padre {1}., API,API, diff --git a/erpnext/translations/ja.csv b/erpnext/translations/ja.csv index 6e2eaae4a4..a11a9a126a 100644 --- a/erpnext/translations/ja.csv +++ b/erpnext/translations/ja.csv @@ -3537,7 +3537,7 @@ Quality Feedback Template,品質フィードバックテンプレート, Rules for applying different promotional schemes.,さまざまなプロモーションスキームを適用するための規則。, Shift,シフト, Show {0},{0}を表示, -"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series"," - "、 "#"、 "。"、 "/"、 "{"、および "}"以外の特殊文字は、一連の名前付けでは使用できません, +"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}"," - "、 "#"、 "。"、 "/"、 "{{"、および "}}"以外の特殊文字は、一連の名前付けでは使用できません {0}, Target Details,ターゲット詳細, {0} already has a Parent Procedure {1}.,{0}にはすでに親プロシージャー{1}があります。, API,API, diff --git a/erpnext/translations/km.csv b/erpnext/translations/km.csv index e2a528cea2..bd70595a7b 100644 --- a/erpnext/translations/km.csv +++ b/erpnext/translations/km.csv @@ -3537,7 +3537,7 @@ Quality Feedback Template,គំរូមតិយោបល់គុណភាព Rules for applying different promotional schemes.,វិធានសម្រាប់អនុវត្តគម្រោងផ្សព្វផ្សាយផ្សេងៗគ្នា។, Shift,ប្តូរ។, Show {0},បង្ហាញ {0}, -"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series","តួអក្សរពិសេសលើកលែងតែ "-", "#", "។ ", "/", "{" និង "}" មិនត្រូវបានអនុញ្ញាតក្នុងស៊េរីដាក់ឈ្មោះ", +"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}","តួអក្សរពិសេសលើកលែងតែ "-", "#", "។ ", "/", "{{" និង "}}" មិនត្រូវបានអនុញ្ញាតក្នុងស៊េរីដាក់ឈ្មោះ {0}", Target Details,ព័ត៌មានលម្អិតគោលដៅ។, {0} already has a Parent Procedure {1}.,{0} មាននីតិវិធីឪពុកម្តាយរួចហើយ {1} ។, API,API, diff --git a/erpnext/translations/kn.csv b/erpnext/translations/kn.csv index 4a9173d4a0..7572a09a08 100644 --- a/erpnext/translations/kn.csv +++ b/erpnext/translations/kn.csv @@ -3537,7 +3537,7 @@ Quality Feedback Template,ಗುಣಮಟ್ಟದ ಪ್ರತಿಕ್ರಿ Rules for applying different promotional schemes.,ವಿಭಿನ್ನ ಪ್ರಚಾರ ಯೋಜನೆಗಳನ್ನು ಅನ್ವಯಿಸುವ ನಿಯಮಗಳು., Shift,ಶಿಫ್ಟ್, Show {0},{0} ತೋರಿಸು, -"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series",""-", "#", ".", "/", "{" ಮತ್ತು "}" ಹೊರತುಪಡಿಸಿ ವಿಶೇಷ ಅಕ್ಷರಗಳನ್ನು ಹೆಸರಿಸುವ ಸರಣಿಯಲ್ಲಿ ಅನುಮತಿಸಲಾಗುವುದಿಲ್ಲ", +"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}",""-", "#", ".", "/", "{{" ಮತ್ತು "}}" ಹೊರತುಪಡಿಸಿ ವಿಶೇಷ ಅಕ್ಷರಗಳನ್ನು ಹೆಸರಿಸುವ ಸರಣಿಯಲ್ಲಿ ಅನುಮತಿಸಲಾಗುವುದಿಲ್ಲ {0}", Target Details,ಗುರಿ ವಿವರಗಳು, {0} already has a Parent Procedure {1}.,{0} ಈಗಾಗಲೇ ಪೋಷಕ ವಿಧಾನವನ್ನು ಹೊಂದಿದೆ {1}., API,API, diff --git a/erpnext/translations/ko.csv b/erpnext/translations/ko.csv index c051b07bea..b873b73b9c 100644 --- a/erpnext/translations/ko.csv +++ b/erpnext/translations/ko.csv @@ -3537,7 +3537,7 @@ Quality Feedback Template,품질 피드백 템플릿, Rules for applying different promotional schemes.,다양한 홍보 계획을 적용하기위한 규칙., Shift,시프트, Show {0},{0} 표시, -"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series","이름 계열에 허용되지 않는 "-", "#", ".", "/", "{"및 "}"을 제외한 특수 문자", +"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}","이름 계열에 허용되지 않는 "-", "#", ".", "/", "{{"및 "}}"을 제외한 특수 문자 {0}", Target Details,대상 세부 정보, {0} already has a Parent Procedure {1}.,{0}에 이미 상위 절차 {1}이 있습니다., API,API, diff --git a/erpnext/translations/ku.csv b/erpnext/translations/ku.csv index 6962ea1ef1..89e12c070b 100644 --- a/erpnext/translations/ku.csv +++ b/erpnext/translations/ku.csv @@ -3537,7 +3537,7 @@ Quality Feedback Template,Feedablonê nerazîbûna kalîteyê, Rules for applying different promotional schemes.,Qanûnên ji bo bicihanîna nexşeyên cûda yên danasînê, Shift,Tarloqî, Show {0},Show {0}, -"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series","Di tîpa navnasî de ji bilî "-", "#", ".", "/", "{" Û "}" tîpên Taybet", +"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}","Di tîpa navnasî de ji bilî "-", "#", ".", "/", "{{" Û "}}" tîpên Taybet {0}", Target Details,Hûrgulên armancê, {0} already has a Parent Procedure {1}.,{0} ji berê ve heye Parent Procedure {1}., API,API, diff --git a/erpnext/translations/lo.csv b/erpnext/translations/lo.csv index b61476cf17..778a59b3c2 100644 --- a/erpnext/translations/lo.csv +++ b/erpnext/translations/lo.csv @@ -3537,7 +3537,7 @@ Quality Feedback Template,ແມ່ແບບ ຄຳ ຕິຊົມຄຸນນ Rules for applying different promotional schemes.,ກົດລະບຽບໃນການ ນຳ ໃຊ້ແຜນການໂຄສະນາທີ່ແຕກຕ່າງກັນ., Shift,ປ່ຽນ, Show {0},ສະແດງ {0}, -"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series","ຕົວລະຄອນພິເສດຍົກເວັ້ນ "-", "#", ".", "/", "{" ແລະ "}" ບໍ່ໄດ້ຖືກອະນຸຍາດໃນຊຸດຊື່", +"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}","ຕົວລະຄອນພິເສດຍົກເວັ້ນ "-", "#", ".", "/", "{{" ແລະ "}}" ບໍ່ໄດ້ຖືກອະນຸຍາດໃນຊຸດຊື່ {0}", Target Details,ລາຍລະອຽດເປົ້າ ໝາຍ, {0} already has a Parent Procedure {1}.,{0} ມີຂັ້ນຕອນການເປັນພໍ່ແມ່ {1} ແລ້ວ., API,API, diff --git a/erpnext/translations/lt.csv b/erpnext/translations/lt.csv index 78571f9624..4721ce43ff 100644 --- a/erpnext/translations/lt.csv +++ b/erpnext/translations/lt.csv @@ -3537,7 +3537,7 @@ Quality Feedback Template,Kokybės atsiliepimų šablonas, Rules for applying different promotional schemes.,Skirtingų reklamos schemų taikymo taisyklės., Shift,Pamaina, Show {0},Rodyti {0}, -"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series","Specialieji simboliai, išskyrus „-“, „#“, „.“, „/“, „{“ Ir „}“, neleidžiami įvardyti serijomis", +"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}","Specialieji simboliai, išskyrus "-", "#", "।", "/", "{{" Ir "}}", neleidžiami įvardyti serijomis {0}", Target Details,Tikslinė informacija, {0} already has a Parent Procedure {1}.,{0} jau turi tėvų procedūrą {1}., API,API, diff --git a/erpnext/translations/lv.csv b/erpnext/translations/lv.csv index cbf04855d0..b8499b28c1 100644 --- a/erpnext/translations/lv.csv +++ b/erpnext/translations/lv.csv @@ -3537,7 +3537,7 @@ Quality Feedback Template,Kvalitatīvas atsauksmes veidne, Rules for applying different promotional schemes.,Noteikumi dažādu reklāmas shēmu piemērošanai., Shift,Maiņa, Show {0},Rādīt {0}, -"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series","Speciālās rakstzīmes, izņemot "-", "#", ".", "/", "{" Un "}", kas nav atļautas nosaukuma sērijās", +"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}","Speciālās rakstzīmes, izņemot "-", "#", ".", "/", "{{" Un "}}", kas nav atļautas nosaukuma sērijās {0}", Target Details,Mērķa informācija, {0} already has a Parent Procedure {1}.,{0} jau ir vecāku procedūra {1}., API,API, diff --git a/erpnext/translations/mk.csv b/erpnext/translations/mk.csv index 7008025534..8ecae03adc 100644 --- a/erpnext/translations/mk.csv +++ b/erpnext/translations/mk.csv @@ -3537,7 +3537,7 @@ Quality Feedback Template,Шаблон за повратни информаци Rules for applying different promotional schemes.,Правила за примена на различни промотивни шеми., Shift,Смена, Show {0},Покажи {0}, -"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series","Не се дозволени специјални карактери освен "-", "#", ".", "/", "{" И "}" во сериите за именување", +"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}","Не се дозволени специјални карактери освен "-", "#", ".", "/", "{{" И "}}" во сериите за именување {0}", Target Details,Цели детали, {0} already has a Parent Procedure {1}.,{0} веќе има Матична постапка {1}., API,API, diff --git a/erpnext/translations/ml.csv b/erpnext/translations/ml.csv index f917969345..f649e6c83b 100644 --- a/erpnext/translations/ml.csv +++ b/erpnext/translations/ml.csv @@ -3537,7 +3537,7 @@ Quality Feedback Template,ഗുണനിലവാരമുള്ള ഫീഡ Rules for applying different promotional schemes.,വ്യത്യസ്ത പ്രമോഷണൽ സ്കീമുകൾ പ്രയോഗിക്കുന്നതിനുള്ള നിയമങ്ങൾ., Shift,ഷിഫ്റ്റ്, Show {0},{0} കാണിക്കുക, -"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series",""-", "#", ".", "/", "{", "}" എന്നിവ ഒഴികെയുള്ള പ്രത്യേക പ്രതീകങ്ങൾ നാമകരണ ശ്രേണിയിൽ അനുവദനീയമല്ല", +"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}",""-", "#", ".", "/", "{{", "}}" എന്നിവ ഒഴികെയുള്ള പ്രത്യേക പ്രതീകങ്ങൾ നാമകരണ ശ്രേണിയിൽ അനുവദനീയമല്ല {0}", Target Details,ടാർഗെറ്റ് വിശദാംശങ്ങൾ, {0} already has a Parent Procedure {1}.,{0} ന് ഇതിനകം ഒരു രക്ഷാകർതൃ നടപടിക്രമം ഉണ്ട് {1}., API,API, diff --git a/erpnext/translations/mr.csv b/erpnext/translations/mr.csv index 9c41ce6f33..38effc1b15 100644 --- a/erpnext/translations/mr.csv +++ b/erpnext/translations/mr.csv @@ -3537,7 +3537,7 @@ Quality Feedback Template,गुणवत्ता अभिप्राय ट Rules for applying different promotional schemes.,वेगवेगळ्या जाहिरात योजना लागू करण्याचे नियम., Shift,शिफ्ट, Show {0},दर्शवा {0}, -"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series",""-", "#", ".", "/", "{" आणि "}" वगळता विशिष्ट वर्णांना नामांकन मालिकेमध्ये परवानगी नाही", +"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}",""-", "#", ".", "/", "{{" आणि "}}" वगळता विशिष्ट वर्णांना नामांकन मालिकेमध्ये परवानगी नाही {0}", Target Details,लक्ष्य तपशील, {0} already has a Parent Procedure {1}.,{0} कडे आधीपासूनच पालक प्रक्रिया आहे {1}., API,API, diff --git a/erpnext/translations/ms.csv b/erpnext/translations/ms.csv index 1483844990..4ee650b104 100644 --- a/erpnext/translations/ms.csv +++ b/erpnext/translations/ms.csv @@ -3537,7 +3537,7 @@ Quality Feedback Template,Template Maklum Balas Kualiti, Rules for applying different promotional schemes.,Kaedah untuk memohon skim promosi yang berbeza., Shift,Shift, Show {0},Tunjukkan {0}, -"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series","Watak Khas kecuali "-", "#", ".", "/", "{" Dan "}" tidak dibenarkan dalam siri penamaan", +"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}","Watak Khas kecuali "-", "#", ".", "/", "{{" Dan "}}" tidak dibenarkan dalam siri penamaan {0}", Target Details,Butiran Sasaran, {0} already has a Parent Procedure {1}.,{0} sudah mempunyai Tatacara Ibu Bapa {1}., API,API, diff --git a/erpnext/translations/my.csv b/erpnext/translations/my.csv index d15ec1ec71..f0d216bbc7 100644 --- a/erpnext/translations/my.csv +++ b/erpnext/translations/my.csv @@ -3537,7 +3537,7 @@ Quality Feedback Template,အရည်အသွေးတုံ့ပြန်ခ Rules for applying different promotional schemes.,ကွဲပြားခြားနားသောပရိုမိုးရှင်းအစီအစဉ်များလျှောက်ထားမှုအတွက်စည်းကမ်းများ။, Shift,အဆိုင်း, Show {0},Show ကို {0}, -"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series","မှလွဲ. အထူးဇာတ်ကောင် "-" "။ ", "#", "/", "{" နှင့် "}" စီးရီးနာမည်အတွက်ခွင့်မပြု", +"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}","မှလွဲ. အထူးဇာတ်ကောင် "-" "။ ", "#", "/", "{{" နှင့် "}}" စီးရီးနာမည်အတွက်ခွင့်မပြု {0}", Target Details,ပစ်မှတ်အသေးစိတ်, {0} already has a Parent Procedure {1}.,{0} ပြီးသားမိဘလုပ်ထုံးလုပ်နည်း {1} ရှိပါတယ်။, API,API ကို, diff --git a/erpnext/translations/nl.csv b/erpnext/translations/nl.csv index fbadc02327..6ec43a0592 100644 --- a/erpnext/translations/nl.csv +++ b/erpnext/translations/nl.csv @@ -3537,7 +3537,7 @@ Quality Feedback Template,Kwaliteitsfeedbacksjabloon, Rules for applying different promotional schemes.,Regels voor het toepassen van verschillende promotieregelingen., Shift,Verschuiving, Show {0},Toon {0}, -"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series","Speciale tekens behalve "-", "#", ".", "/", "{" En "}" niet toegestaan in naamgevingsreeks", +"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}","Speciale tekens behalve "-", "#", ".", "/", "{{" En "}}" niet toegestaan in naamgevingsreeks {0}", Target Details,Doelgegevens, {0} already has a Parent Procedure {1}.,{0} heeft al een ouderprocedure {1}., API,API, diff --git a/erpnext/translations/no.csv b/erpnext/translations/no.csv index 150e5ca4a2..df87e81db9 100644 --- a/erpnext/translations/no.csv +++ b/erpnext/translations/no.csv @@ -3537,7 +3537,7 @@ Quality Feedback Template,Kvalitet Tilbakemelding Mal, Rules for applying different promotional schemes.,Regler for anvendelse av forskjellige kampanjer., Shift,Skifte, Show {0},Vis {0}, -"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series","Spesialtegn unntatt "-", "#", ".", "/", "{" Og "}" ikke tillatt i navneserier", +"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}","Spesialtegn unntatt "-", "#", ".", "/", "{{" Og "}}" ikke tillatt i navneserier {0}", Target Details,Måldetaljer, {0} already has a Parent Procedure {1}.,{0} har allerede en foreldreprosedyre {1}., API,API, diff --git a/erpnext/translations/pl.csv b/erpnext/translations/pl.csv index 8340b7272f..be81e29d7f 100644 --- a/erpnext/translations/pl.csv +++ b/erpnext/translations/pl.csv @@ -3537,7 +3537,7 @@ Quality Feedback Template,Szablon opinii o jakości, Rules for applying different promotional schemes.,Zasady stosowania różnych programów promocyjnych., Shift,Przesunięcie, Show {0},Pokaż {0}, -"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series","Znaki specjalne z wyjątkiem „-”, „#”, „.”, „/”, „{” I „}” niedozwolone w serii nazw", +"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}","Znaki specjalne z wyjątkiem "-", "#", "।", "/", "{{" I "}}" niedozwolone w serii nazw {0}", Target Details,Szczegóły celu, {0} already has a Parent Procedure {1}.,{0} ma już procedurę nadrzędną {1}., API,API, diff --git a/erpnext/translations/ps.csv b/erpnext/translations/ps.csv index 1dcaf48d79..68add60cc9 100644 --- a/erpnext/translations/ps.csv +++ b/erpnext/translations/ps.csv @@ -3537,7 +3537,7 @@ Quality Feedback Template,د کیفیت فیډبیک ټیمپلیټ, Rules for applying different promotional schemes.,د مختلف پروموشنل سکیمونو پلي کولو قواعد., Shift,شفټ, Show {0},ښودل {0}, -"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series",ځانګړي نومونه د "-" ، "#" ، "." ، "/" ، "{" او "}" نوم لیکلو کې اجازه نه لري, +"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}",{0} ځانګړي نومونه د "-" ، "#" ، "." ، "/" ، "{{" او "}}" نوم لیکلو کې اجازه نه لري, Target Details,د هدف توضیحات, {0} already has a Parent Procedure {1}.,{0} د مخه د والدین پروسیجر {1} لري., API,API, diff --git a/erpnext/translations/pt-BR.csv b/erpnext/translations/pt-BR.csv index 957cb75752..cc1c6af913 100644 --- a/erpnext/translations/pt-BR.csv +++ b/erpnext/translations/pt-BR.csv @@ -3537,7 +3537,7 @@ Quality Feedback Template,Modelo de Feedback de Qualidade, Rules for applying different promotional schemes.,Regras para aplicar diferentes esquemas promocionais., Shift,Mudança, Show {0},Mostrar {0}, -"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series","Caracteres especiais, exceto "-", "#", ".", "/", "{" e "}" não permitidos na série de nomenclatura", +"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}","Caracteres especiais, exceto "-", "#", ".", "/", "{{" e "}}" não permitidos na série de nomenclatura {0}", Target Details,Detalhes do Alvo, {0} already has a Parent Procedure {1}.,{0} já tem um procedimento pai {1}., API,API, diff --git a/erpnext/translations/pt.csv b/erpnext/translations/pt.csv index 3b8a0a0f43..43bf227e67 100644 --- a/erpnext/translations/pt.csv +++ b/erpnext/translations/pt.csv @@ -3537,7 +3537,7 @@ Quality Feedback Template,Modelo de Feedback de Qualidade, Rules for applying different promotional schemes.,Regras para aplicar diferentes esquemas promocionais., Shift,Mudança, Show {0},Mostrar {0}, -"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series","Caracteres especiais, exceto "-", "#", ".", "/", "{" E "}" não permitidos na série de nomenclatura", +"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}","Caracteres especiais, exceto "-", "#", ".", "/", "{{" E "}}" não permitidos na série de nomenclatura {0}", Target Details,Detalhes do Alvo, {0} already has a Parent Procedure {1}.,{0} já tem um procedimento pai {1}., API,API, diff --git a/erpnext/translations/ro.csv b/erpnext/translations/ro.csv index 643b8c5c3e..3aab431cdd 100644 --- a/erpnext/translations/ro.csv +++ b/erpnext/translations/ro.csv @@ -3537,7 +3537,7 @@ Quality Feedback Template,Șablon de feedback de calitate, Rules for applying different promotional schemes.,Reguli pentru aplicarea diferitelor scheme promoționale., Shift,Schimb, Show {0},Afișați {0}, -"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series","Caractere speciale, cu excepția "-", "#", ".", "/", "{" Și "}" nu sunt permise în numirea seriei", +"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}","Caractere speciale, cu excepția "-", "#", ".", "/", "{{" Și "}}" nu sunt permise în numirea seriei {0}", Target Details,Detalii despre țintă, {0} already has a Parent Procedure {1}.,{0} are deja o procedură părinte {1}., API,API-ul, diff --git a/erpnext/translations/ru.csv b/erpnext/translations/ru.csv index 5f3af770cf..662346f2e5 100644 --- a/erpnext/translations/ru.csv +++ b/erpnext/translations/ru.csv @@ -3535,7 +3535,7 @@ Quality Feedback Template,Шаблон обратной связи по каче Rules for applying different promotional schemes.,Правила применения разных рекламных схем., Shift,Сдвиг, Show {0},Показать {0}, -"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series","Специальные символы, кроме ""-"", ""#"", ""."", ""/"", ""{"" и ""}"", не допускаются в серийных номерах", +"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}","Специальные символы, кроме "-", "#", "।", "/", "{{" и "}}", не допускаются в серийных номерах {0}", Target Details,Детали цели, {0} already has a Parent Procedure {1}.,{0} уже имеет родительскую процедуру {1}., API,API, diff --git a/erpnext/translations/rw.csv b/erpnext/translations/rw.csv index 64591399af..6c2b5ddb10 100644 --- a/erpnext/translations/rw.csv +++ b/erpnext/translations/rw.csv @@ -3537,7 +3537,7 @@ Quality Feedback Template,Inyandikorugero nziza, Rules for applying different promotional schemes.,Amategeko yo gukoresha gahunda zitandukanye zo kwamamaza., Shift,Shift, Show {0},Erekana {0}, -"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series","Inyuguti zidasanzwe usibye "-", "#", ".", "/", "{" Na "}" ntibyemewe mu ruhererekane rwo kwita izina", +"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}","Inyuguti zidasanzwe usibye "-", "#", ".", "/", "{{" Na "}}" ntibyemewe mu ruhererekane rwo kwita izina {0}", Target Details,Intego Ibisobanuro, {0} already has a Parent Procedure {1}.,{0} isanzwe ifite uburyo bwababyeyi {1}., API,API, diff --git a/erpnext/translations/si.csv b/erpnext/translations/si.csv index 690c47332d..5b7823589a 100644 --- a/erpnext/translations/si.csv +++ b/erpnext/translations/si.csv @@ -3537,7 +3537,7 @@ Quality Feedback Template,ගුණාත්මක ප්‍රතිපෝෂ Rules for applying different promotional schemes.,විවිධ ප්‍රවර්ධන යෝජනා ක්‍රම යෙදීම සඳහා නීති., Shift,මාරුව, Show {0},{0 Show පෙන්වන්න, -"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series",""-", "#", ".", "/", "{" සහ "}" හැර විශේෂ අක්ෂර නම් කිරීමේ ශ්‍රේණියේ අවසර නැත", +"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}",""-", "#", ".", "/", "{{" සහ "}}" හැර විශේෂ අක්ෂර නම් කිරීමේ ශ්‍රේණියේ අවසර නැත {0}", Target Details,ඉලක්ක විස්තර, {0} already has a Parent Procedure {1}.,{0} දැනටමත් දෙමාපිය ක්‍රියා පටිපාටියක් ඇත {1}., API,API, diff --git a/erpnext/translations/sk.csv b/erpnext/translations/sk.csv index cb4a7fed4b..446e0be58b 100644 --- a/erpnext/translations/sk.csv +++ b/erpnext/translations/sk.csv @@ -3537,7 +3537,7 @@ Quality Feedback Template,Šablóna spätnej väzby kvality, Rules for applying different promotional schemes.,Pravidlá uplatňovania rôznych propagačných programov., Shift,smena, Show {0},Zobraziť {0}, -"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series","Špeciálne znaky s výnimkou „-“, „#“, „.“, „/“, „{“ A „}“ nie sú v názvových sériách povolené.", +"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}","Špeciálne znaky s výnimkou "-", "#", "।", "/", "{{" A "}}" nie sú v názvových sériách povolené {0}.", Target Details,Podrobnosti o cieli, {0} already has a Parent Procedure {1}.,{0} už má rodičovský postup {1}., API,API, diff --git a/erpnext/translations/sl.csv b/erpnext/translations/sl.csv index 8beec6be16..8b8ed01114 100644 --- a/erpnext/translations/sl.csv +++ b/erpnext/translations/sl.csv @@ -3537,7 +3537,7 @@ Quality Feedback Template,Predloga za povratne informacije o kakovosti, Rules for applying different promotional schemes.,Pravila za uporabo različnih promocijskih shem., Shift,Shift, Show {0},Prikaži {0}, -"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series","Posebni znaki, razen "-", "#", ".", "/", "{" In "}" v poimenovanju ni dovoljen", +"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}","Posebni znaki, razen "-", "#", ".", "/", "{{" In "}}" v poimenovanju ni dovoljen {0}", Target Details,Podrobnosti cilja, {0} already has a Parent Procedure {1}.,{0} že ima nadrejeni postopek {1}., API,API, diff --git a/erpnext/translations/sq.csv b/erpnext/translations/sq.csv index 05aefa3602..6f4f8e0135 100644 --- a/erpnext/translations/sq.csv +++ b/erpnext/translations/sq.csv @@ -3537,7 +3537,7 @@ Quality Feedback Template,Modeli i reagimit të cilësisë, Rules for applying different promotional schemes.,Rregulla për aplikimin e skemave të ndryshme promovuese., Shift,ndryshim, Show {0},Trego {0}, -"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series","Karaktere speciale përveç "-", "#", ".", "/", "{" Dhe "}" nuk lejohen në seritë emërtuese", +"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}","Karaktere speciale përveç "-", "#", ".", "/", "{{" Dhe "}}" nuk lejohen në seritë emërtuese {0}", Target Details,Detaje të synuara, {0} already has a Parent Procedure {1}.,{0} tashmë ka një procedurë prindërore {1}., API,API, diff --git a/erpnext/translations/sr.csv b/erpnext/translations/sr.csv index b507f74f09..853c6f3ccc 100644 --- a/erpnext/translations/sr.csv +++ b/erpnext/translations/sr.csv @@ -3537,7 +3537,7 @@ Quality Feedback Template,Квалитетни образац за поврат Rules for applying different promotional schemes.,Правила за примену различитих промотивних шема., Shift,Смена, Show {0},Прикажи {0}, -"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series","Посебни знакови осим "-", "#", ".", "/", "{" И "}" нису дозвољени у именовању серија", +"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}","Посебни знакови осим "-", "#", ".", "/", "{{" И "}}" нису дозвољени у именовању серија {0}", Target Details,Детаљи циља, {0} already has a Parent Procedure {1}.,{0} већ има родитељску процедуру {1}., API,АПИ, diff --git a/erpnext/translations/sv.csv b/erpnext/translations/sv.csv index 57e02792f4..2a4d6b1fbd 100644 --- a/erpnext/translations/sv.csv +++ b/erpnext/translations/sv.csv @@ -3537,7 +3537,7 @@ Quality Feedback Template,Kvalitetsåterkopplingsmall, Rules for applying different promotional schemes.,Regler för tillämpning av olika kampanjprogram., Shift,Flytta, Show {0},Visa {0}, -"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series","Specialtecken utom "-", "#", ".", "/", "{" Och "}" är inte tillåtna i namnserien", +"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}","Specialtecken utom "-", "#", ".", "/", "{{" Och "}}" är inte tillåtna i namnserien {0}", Target Details,Måldetaljer, {0} already has a Parent Procedure {1}.,{0} har redan en överordnad procedur {1}., API,API, diff --git a/erpnext/translations/sw.csv b/erpnext/translations/sw.csv index 3595727666..234d33e106 100644 --- a/erpnext/translations/sw.csv +++ b/erpnext/translations/sw.csv @@ -3537,7 +3537,7 @@ Quality Feedback Template,Kiolezo cha Maoni ya Ubora, Rules for applying different promotional schemes.,Sheria za kutumia miradi tofauti ya uendelezaji., Shift,Shift, Show {0},Onyesha {0}, -"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series","Tabia maalum isipokuwa "-", "#", ".", "/", "{" Na "}" hairuhusiwi katika kutaja mfululizo", +"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}","Tabia maalum isipokuwa "-", "#", ".", "/", "{{" Na "}}" hairuhusiwi katika kutaja mfululizo {0}", Target Details,Maelezo ya Lengo, {0} already has a Parent Procedure {1}.,{0} tayari ina Utaratibu wa Mzazi {1}., API,API, diff --git a/erpnext/translations/ta.csv b/erpnext/translations/ta.csv index 100f0e9300..e7384b36ef 100644 --- a/erpnext/translations/ta.csv +++ b/erpnext/translations/ta.csv @@ -3537,7 +3537,7 @@ Quality Feedback Template,தரமான கருத்து வார்ப Rules for applying different promotional schemes.,வெவ்வேறு விளம்பர திட்டங்களைப் பயன்படுத்துவதற்கான விதிகள்., Shift,ஷிப்ட், Show {0},{0 Show ஐக் காட்டு, -"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series",""-", "#", ".", "/", "{" மற்றும் "}" தவிர சிறப்பு எழுத்துக்கள் பெயரிடும் தொடரில் அனுமதிக்கப்படவில்லை", +"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}",""-", "#", ".", "/", "{{" மற்றும் "}}" தவிர சிறப்பு எழுத்துக்கள் பெயரிடும் தொடரில் அனுமதிக்கப்படவில்லை {0}", Target Details,இலக்கு விவரங்கள், {0} already has a Parent Procedure {1}.,{0} ஏற்கனவே பெற்றோர் நடைமுறை {1 has ஐக் கொண்டுள்ளது., API,ஏபிஐ, diff --git a/erpnext/translations/te.csv b/erpnext/translations/te.csv index 047d07731e..cd14a77310 100644 --- a/erpnext/translations/te.csv +++ b/erpnext/translations/te.csv @@ -3537,7 +3537,7 @@ Quality Feedback Template,నాణ్యమైన అభిప్రాయ మ Rules for applying different promotional schemes.,విభిన్న ప్రచార పథకాలను వర్తింపజేయడానికి నియమాలు., Shift,మార్పు, Show {0},{0 Show చూపించు, -"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series",""-", "#", ".", "/", "{" మరియు "}" మినహా ప్రత్యేక అక్షరాలు పేరు పెట్టే సిరీస్‌లో అనుమతించబడవు", +"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}",""-", "#", ".", "/", "{{" మరియు "}}" మినహా ప్రత్యేక అక్షరాలు పేరు పెట్టే సిరీస్‌లో అనుమతించబడవు {0}", Target Details,లక్ష్య వివరాలు, {0} already has a Parent Procedure {1}.,{0} ఇప్పటికే తల్లిదండ్రుల విధానం {1 has ను కలిగి ఉంది., API,API, diff --git a/erpnext/translations/th.csv b/erpnext/translations/th.csv index 71233ec8fa..4ab59bc8e1 100644 --- a/erpnext/translations/th.csv +++ b/erpnext/translations/th.csv @@ -3537,7 +3537,7 @@ Quality Feedback Template,เทมเพลตข้อเสนอแนะค Rules for applying different promotional schemes.,กฎสำหรับการใช้รูปแบบการส่งเสริมการขายต่าง ๆ, Shift,เปลี่ยน, Show {0},แสดง {0}, -"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series","ห้ามใช้อักขระพิเศษยกเว้น "-", "#", ".", "/", "{" และ "}" ในซีรี่ส์", +"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}","ห้ามใช้อักขระพิเศษยกเว้น "-", "#", ".", "/", "{{" และ "}}" ในซีรี่ส์ {0}", Target Details,รายละเอียดเป้าหมาย, {0} already has a Parent Procedure {1}.,{0} มี parent Parent {1} อยู่แล้ว, API,API, diff --git a/erpnext/translations/tr.csv b/erpnext/translations/tr.csv index 9e7ba4d142..b65494ce32 100644 --- a/erpnext/translations/tr.csv +++ b/erpnext/translations/tr.csv @@ -3537,7 +3537,7 @@ Quality Feedback Template,Kalite Geribildirim Şablonu, Rules for applying different promotional schemes.,Farklı promosyon programlarını uygulama kuralları., Shift,vardiya, Show {0},{0} göster, -"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series",""-", "#", ".", "/", "{" Ve "}" dışındaki Özel Karakterler, seri dizisine izin verilmez", +"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}",""-", "#", ".", "/", "{{" Ve "}}" dışındaki Özel Karakterler, seri dizisine izin verilmez {0}", Target Details,Hedef Detayları, {0} already has a Parent Procedure {1}.,{0} zaten bir {1} veli prosedürüne sahip., API,API, diff --git a/erpnext/translations/uk.csv b/erpnext/translations/uk.csv index 53e2df518c..4e2f63f870 100644 --- a/erpnext/translations/uk.csv +++ b/erpnext/translations/uk.csv @@ -3537,7 +3537,7 @@ Quality Feedback Template,Шаблон зворотнього зв'язку Rules for applying different promotional schemes.,Правила застосування різних рекламних схем., Shift,Зміна, Show {0},Показати {0}, -"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series","Спеціальні символи, окрім "-", "#", ".", "/", "{" Та "}", не дозволяються в іменуванні серій", +"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}","Спеціальні символи, окрім "-", "#", ".", "/", "{{" Та "}}", не дозволяються в іменуванні серій {0}", Target Details,Деталі цілі, {0} already has a Parent Procedure {1}.,{0} вже має батьківську процедуру {1}., API,API, diff --git a/erpnext/translations/ur.csv b/erpnext/translations/ur.csv index aaaef5895f..db6518e86b 100644 --- a/erpnext/translations/ur.csv +++ b/erpnext/translations/ur.csv @@ -3537,7 +3537,7 @@ Quality Feedback Template,کوالٹی فیڈ بیک ٹیمپلیٹ۔, Rules for applying different promotional schemes.,مختلف پروموشنل اسکیموں کو لاگو کرنے کے قواعد۔, Shift,شفٹ۔, Show {0},دکھائیں {0}, -"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series","-" ، "#" ، "." ، "/" ، "{" اور "}" سوائے خصوصی حروف کی نام بندی سیریز میں اجازت نہیں ہے, +"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}",{0} "-" ، "#" ، "." ، "/" ، "{{" اور "}}" سوائے خصوصی حروف کی نام بندی سیریز میں اجازت نہیں ہے, Target Details,ہدف کی تفصیلات۔, {0} already has a Parent Procedure {1}.,{0} پہلے سے ہی والدین کا طریقہ کار {1} ہے., API,API, diff --git a/erpnext/translations/uz.csv b/erpnext/translations/uz.csv index c983797aae..bb64a15f2b 100644 --- a/erpnext/translations/uz.csv +++ b/erpnext/translations/uz.csv @@ -3537,7 +3537,7 @@ Quality Feedback Template,Sifat bo'yicha fikrlar shablonlari, Rules for applying different promotional schemes.,Turli reklama sxemalarini qo'llash qoidalari., Shift,Shift, Show {0},{0} ni ko'rsatish, -"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series",""-", "#", ".", "/", "{" Va "}" belgilaridan tashqari maxsus belgilarga ruxsat berilmaydi.", +"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}",""-", "#", ".", "/", "{{" Va "}}" belgilaridan tashqari maxsus belgilarga ruxsat berilmaydi {0}.", Target Details,Maqsad tafsilotlari, {0} already has a Parent Procedure {1}.,{0} allaqachon Ota-ona tartibiga ega {1}., API,API, diff --git a/erpnext/translations/vi.csv b/erpnext/translations/vi.csv index 03ff2ccc38..7317b4b027 100644 --- a/erpnext/translations/vi.csv +++ b/erpnext/translations/vi.csv @@ -3537,7 +3537,7 @@ Quality Feedback Template,Mẫu phản hồi chất lượng, Rules for applying different promotional schemes.,Quy tắc áp dụng các chương trình khuyến mãi khác nhau., Shift,Ca, Show {0},Hiển thị {0}, -"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series","Các ký tự đặc biệt ngoại trừ "-", "#", ".", "/", "{" Và "}" không được phép trong chuỗi đặt tên", +"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}","Các ký tự đặc biệt ngoại trừ "-", "#", ".", "/", "{{" Và "}}" không được phép trong chuỗi đặt tên {0}", Target Details,Chi tiết mục tiêu, {0} already has a Parent Procedure {1}.,{0} đã có Quy trình dành cho phụ huynh {1}., API,API, diff --git a/erpnext/translations/zh-TW.csv b/erpnext/translations/zh-TW.csv index de1d7632e7..c30dd7231c 100644 --- a/erpnext/translations/zh-TW.csv +++ b/erpnext/translations/zh-TW.csv @@ -10,20 +10,20 @@ DocType: Supplier Scorecard,Notify Supplier,通知供應商 apps/erpnext/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.js +52,Please select Party Type first,請選擇黨第一型 DocType: Item,Customer Items,客戶項目 DocType: Project,Costing and Billing,成本核算和計費 -apps/erpnext/erpnext/hr/doctype/employee_advance/employee_advance.py +43,Advance account currency should be same as company currency {0},預付帳戶貨幣應與公司貨幣{0}相同 +apps/erpnext/erpnext/hr/doctype/employee_advance/employee_advance.py +43,Advance account currency should be same as company currency {0},預付科目貨幣應與公司貨幣{0}相同 DocType: QuickBooks Migrator,Token Endpoint,令牌端點 -apps/erpnext/erpnext/accounts/doctype/account/account.py +55,Account {0}: Parent account {1} can not be a ledger,帳戶{0}:父帳戶{1}不能是總帳 +apps/erpnext/erpnext/accounts/doctype/account/account.py +55,Account {0}: Parent account {1} can not be a ledger,科目{0}:上層科目{1}不能是總帳 DocType: Item,Publish Item to hub.erpnext.com,發布項目hub.erpnext.com apps/erpnext/erpnext/hr/doctype/leave_application/leave_application.py +270,Cannot find active Leave Period,找不到有效的休假期 apps/erpnext/erpnext/hr/doctype/employee/employee_dashboard.py +22,Evaluation,評估 DocType: Item,Default Unit of Measure,預設的計量單位 DocType: SMS Center,All Sales Partner Contact,所有的銷售合作夥伴聯絡 DocType: Department,Leave Approvers,休假審批人 -DocType: Employee,Bio / Cover Letter,生物/求職信 +DocType: Employee,Bio / Cover Letter,自傳/求職信 DocType: Patient Encounter,Investigations,調查 DocType: Restaurant Order Entry,Click Enter To Add,點擊輸入要添加 apps/erpnext/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.py +29,"Missing value for Password, API Key or Shopify URL",缺少密碼,API密鑰或Shopify網址的值 -apps/erpnext/erpnext/public/js/setup_wizard.js +243,All Accounts,所有帳戶 +apps/erpnext/erpnext/public/js/setup_wizard.js +243,All Accounts,所有科目 apps/erpnext/erpnext/hr/doctype/employee_transfer/employee_transfer.py +15,Cannot transfer Employee with status Left,無法轉移狀態為左的員工 apps/erpnext/erpnext/manufacturing/doctype/production_order/production_order.py +216,"Stopped Production Order cannot be cancelled, Unstop it first to cancel",停止生產訂單無法取消,首先Unstop它取消 apps/erpnext/erpnext/assets/doctype/asset/asset.js +338,Do you really want to scrap this asset?,難道你真的想放棄這項資產? @@ -48,12 +48,12 @@ DocType: Allowed To Transact With,Allowed To Transact With,允許與 DocType: Bank Guarantee,Customer,客戶 DocType: Purchase Receipt Item,Required By,需求來自 DocType: Delivery Note,Return Against Delivery Note,射向送貨單 -DocType: Asset Category,Finance Book Detail,財務圖書細節 +DocType: Asset Category,Finance Book Detail,財務帳簿細節 DocType: Purchase Order,% Billed,%已開立帳單 apps/erpnext/erpnext/controllers/sales_and_purchase_return.py +41,Exchange Rate must be same as {0} {1} ({2}),匯率必須一致{0} {1}({2}) DocType: Sales Invoice,Customer Name,客戶名稱 DocType: Vehicle,Natural Gas,天然氣 -apps/erpnext/erpnext/setup/setup_wizard/operations/company_setup.py +63,Bank account cannot be named as {0},銀行賬戶不能命名為{0} +apps/erpnext/erpnext/setup/setup_wizard/operations/company_setup.py +63,Bank account cannot be named as {0},銀行科目不能命名為{0} DocType: Employee Tax Exemption Declaration,HRA as per Salary Structure,HRA根據薪資結構 DocType: Account,Heads (or groups) against which Accounting Entries are made and balances are maintained.,頭(或組)針對其會計分錄是由和平衡得以維持。 apps/erpnext/erpnext/accounts/doctype/gl_entry/gl_entry.py +197,Outstanding for {0} cannot be less than zero ({1}),傑出的{0}不能小於零( {1} ) @@ -142,11 +142,11 @@ apps/erpnext/erpnext/manufacturing/doctype/production_plan/production_plan_dashb apps/erpnext/erpnext/hr/doctype/attendance/attendance.py +46,Attendance date can not be less than employee's joining date,考勤日期不得少於員工的加盟日期 DocType: Grading Scale,Grading Scale Name,分級標準名稱 apps/erpnext/erpnext/public/js/hub/marketplace.js +147,Add Users to Marketplace,將用戶添加到市場 -apps/erpnext/erpnext/accounts/doctype/account/account.js +37,This is a root account and cannot be edited.,這是一個 root 帳戶,不能被編輯。 -DocType: BOM,Operations,作業 +apps/erpnext/erpnext/accounts/doctype/account/account.js +37,This is a root account and cannot be edited.,這是一個 root 科目,不能被編輯。 +DocType: BOM,Operations,操作 apps/erpnext/erpnext/setup/doctype/authorization_rule/authorization_rule.py +38,Cannot set authorization on basis of Discount for {0},不能在折扣的基礎上設置授權{0} DocType: Subscription,Subscription Start Date,訂閱開始日期 -DocType: Healthcare Settings,Default receivable accounts to be used if not set in Patient to book Appointment charges.,如果未在患者中設置預約費用,則使用默認應收帳戶。 +DocType: Healthcare Settings,Default receivable accounts to be used if not set in Patient to book Appointment charges.,如果未在患者中設置預約費用,則使用默認應收科目。 DocType: Rename Tool,"Attach .csv file with two columns, one for the old name and one for the new name",附加.csv文件有兩列,一為舊名稱,一個用於新名稱 apps/erpnext/erpnext/regional/report/eway_bill/eway_bill.py +195,From Address 2,來自地址2 apps/erpnext/erpnext/accounts/utils.py +74,{0} {1} not in any active Fiscal Year.,{0} {1} 不在任何有效的會計年度 @@ -216,14 +216,14 @@ apps/erpnext/erpnext/stock/doctype/stock_entry/stock_entry.py +36,From {0} to {1 apps/erpnext/erpnext/setup/setup_wizard/setup_wizard.py +51,Failed to setup taxes,無法設置稅收 DocType: Item,Copy From Item Group,從項目群組複製 DocType: Journal Entry,Opening Entry,開放報名 -apps/erpnext/erpnext/accounts/doctype/cheque_print_template/cheque_print_template.js +25,Account Pay Only,賬戶只需支付 +apps/erpnext/erpnext/accounts/doctype/cheque_print_template/cheque_print_template.js +25,Account Pay Only,科目只需支付 DocType: Loan,Repay Over Number of Periods,償還期的超過數 DocType: Stock Entry,Additional Costs,額外費用 -apps/erpnext/erpnext/accounts/doctype/account/account.py +145,Account with existing transaction can not be converted to group.,帳戶與現有的交易不能被轉換到群組。 +apps/erpnext/erpnext/accounts/doctype/account/account.py +145,Account with existing transaction can not be converted to group.,科目與現有的交易不能被轉換到群組。 DocType: Lead,Product Enquiry,產品查詢 DocType: Education Settings,Validate Batch for Students in Student Group,驗證學生組學生的批次 apps/erpnext/erpnext/hr/doctype/attendance/attendance.py +38,No leave record found for employee {0} for {1},未找到員工的假期記錄{0} {1} -DocType: Company,Unrealized Exchange Gain/Loss Account,未實現的匯兌收益/損失賬戶 +DocType: Company,Unrealized Exchange Gain/Loss Account,未實現的匯兌收益/損失科目 apps/erpnext/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.js +23,Please enter company first,請先輸入公司 apps/erpnext/erpnext/accounts/doctype/journal_entry/journal_entry.js +626,Please select Company first,請首先選擇公司 DocType: Employee Education,Under Graduate,根據研究生 @@ -236,7 +236,7 @@ DocType: Fee Schedule,Send Payment Request Email,發送付款請求電子郵件 apps/erpnext/erpnext/manufacturing/doctype/bom/bom.py +282,Item {0} does not exist in the system or has expired,項目{0}不存在於系統中或已過期 DocType: Supplier,Leave blank if the Supplier is blocked indefinitely,如果供應商被無限期封鎖,請留空 apps/erpnext/erpnext/setup/setup_wizard/data/industry_type.py +44,Real Estate,房地產 -apps/erpnext/erpnext/accounts/report/general_ledger/general_ledger.html +1,Statement of Account,帳戶狀態 +apps/erpnext/erpnext/accounts/report/general_ledger/general_ledger.html +1,Statement of Account,科目狀態 apps/erpnext/erpnext/setup/setup_wizard/data/industry_type.py +41,Pharmaceuticals,製藥 DocType: Purchase Invoice Item,Is Fixed Asset,是固定的資產 apps/erpnext/erpnext/stock/doctype/stock_entry/stock_entry.py +355,"Available qty is {0}, you need {1}",可用數量是{0},則需要{1} @@ -284,7 +284,7 @@ DocType: Production Plan,Material Request Detail,材料請求詳情 DocType: Selling Settings,Default Quotation Validity Days,默認報價有效天數 apps/erpnext/erpnext/controllers/accounts_controller.py +886,"To include tax in row {0} in Item rate, taxes in rows {1} must also be included",要包括稅款,行{0}項率,稅收行{1}也必須包括在內 DocType: Payroll Entry,Validate Attendance,驗證出席 -DocType: Sales Invoice,Change Amount,漲跌額 +DocType: Sales Invoice,Change Amount,變動金額 DocType: Party Tax Withholding Config,Certificate Received,已收到證書 DocType: GST Settings,Set Invoice Value for B2C. B2CL and B2CS calculated based on this invoice value.,設置B2C的發票值。 B2CL和B2CS根據此發票值計算。 DocType: BOM Update Tool,New BOM,新的物料清單 @@ -294,7 +294,7 @@ DocType: Supplier Group,Supplier Group Name,供應商集團名稱 DocType: Driver,Driving License Categories,駕駛執照類別 apps/erpnext/erpnext/selling/doctype/sales_order/sales_order.py +124,Please enter Delivery Date,請輸入交貨日期 DocType: Depreciation Schedule,Make Depreciation Entry,計提折舊進入 -DocType: Closed Document,Closed Document,封閉文件 +DocType: Closed Document,Closed Document,關閉文件 DocType: HR Settings,Leave Settings,保留設置 apps/erpnext/erpnext/hr/doctype/staffing_plan/staffing_plan.js +76,Number of positions cannot be less then current count of employees,職位數量不能少於當前員工人數 DocType: Lead,Request Type,請求類型 @@ -308,7 +308,7 @@ apps/erpnext/erpnext/setup/setup_wizard/operations/install_fixtures.py +144,Exec apps/erpnext/erpnext/config/manufacturing.py +62,Details of the operations carried out.,進行的作業細節。 DocType: Asset Maintenance Log,Maintenance Status,維修狀態 apps/erpnext/erpnext/non_profit/doctype/member/member_dashboard.py +10,Membership Details,會員資格 -apps/erpnext/erpnext/accounts/doctype/gl_entry/gl_entry.py +56,{0} {1}: Supplier is required against Payable account {2},{0} {1}:需要對供應商應付賬款{2} +apps/erpnext/erpnext/accounts/doctype/gl_entry/gl_entry.py +56,{0} {1}: Supplier is required against Payable account {2},{0} {1}:需要對供應商應付帳款{2} apps/erpnext/erpnext/config/selling.py +52,Items and Pricing,項目和定價 apps/erpnext/erpnext/projects/doctype/project/project_dashboard.html +2,Total hours: {0},總時間:{0} apps/erpnext/erpnext/accounts/report/trial_balance/trial_balance.py +43,From Date should be within the Fiscal Year. Assuming From Date = {0},從日期應該是在財政年度內。假設起始日期={0} @@ -369,12 +369,12 @@ DocType: Company,Default Payroll Payable Account,默認情況下,應付職工 apps/erpnext/erpnext/education/doctype/student_group/student_group.js +51,Update Email Group,更新電子郵件組 DocType: Sales Invoice,Is Opening Entry,是開放登錄 DocType: Lab Test Template,"If unchecked, the item wont be appear in Sales Invoice, but can be used in group test creation. ",如果取消選中,該項目不會出現在銷售發票中,但可用於創建組測試。 -DocType: Customer Group,Mention if non-standard receivable account applicable,何況,如果不規範應收賬款適用 +DocType: Customer Group,Mention if non-standard receivable account applicable,何況,如果不規範應收帳款適用 DocType: Course Schedule,Instructor Name,導師姓名 DocType: Company,Arrear Component,欠費組件 DocType: Supplier Scorecard,Criteria Setup,條件設置 apps/erpnext/erpnext/manufacturing/doctype/production_order/production_order.py +199,For Warehouse is required before Submit,對於倉庫之前,需要提交 -DocType: Codification Table,Medical Code,醫療法 +DocType: Codification Table,Medical Code,醫療代號 apps/erpnext/erpnext/config/integrations.py +37,Connect Amazon with ERPNext,將Amazon與ERPNext連接起來 apps/erpnext/erpnext/manufacturing/doctype/production_planning_tool/production_planning_tool.py +20,Please enter Company,請輸入公司名稱 DocType: Delivery Note Item,Against Sales Invoice Item,對銷售發票項目 @@ -406,9 +406,9 @@ DocType: Inpatient Record,Discharge Scheduled,出院預定 apps/erpnext/erpnext/hr/doctype/employee/employee.py +138,Relieving Date must be greater than Date of Joining,解除日期必須大於加入的日期 DocType: POS Closing Voucher,Cashier,出納員 apps/erpnext/erpnext/setup/setup_wizard/operations/install_fixtures.py +198,Leaves per Year,每年葉 -apps/erpnext/erpnext/accounts/doctype/journal_entry/journal_entry.py +162,Row {0}: Please check 'Is Advance' against Account {1} if this is an advance entry.,行{0}:請檢查'是推進'對帳戶{1},如果這是一個進步條目。 +apps/erpnext/erpnext/accounts/doctype/journal_entry/journal_entry.py +162,Row {0}: Please check 'Is Advance' against Account {1} if this is an advance entry.,行{0}:請檢查'是進階'對科目{1},如果這是一個進階條目。 apps/erpnext/erpnext/stock/utils.py +243,Warehouse {0} does not belong to company {1},倉庫{0}不屬於公司{1} -DocType: Email Digest,Profit & Loss,利潤損失 +DocType: Email Digest,Profit & Loss,收益與損失 DocType: Task,Total Costing Amount (via Time Sheet),總成本計算量(通過時間表) apps/erpnext/erpnext/education/doctype/fee_schedule/fee_schedule.py +76,Please setup Students under Student Groups,請設置學生組的學生 DocType: Item Website Specification,Item Website Specification,項目網站規格 @@ -479,7 +479,7 @@ apps/erpnext/erpnext/config/desktop.py +159,Learn,學習 DocType: Purchase Invoice Item,Enable Deferred Expense,啟用延期費用 DocType: Asset,Next Depreciation Date,接下來折舊日期 apps/erpnext/erpnext/projects/doctype/activity_type/activity_type.js +3,Activity Cost per Employee,每個員工活動費用 -DocType: Accounts Settings,Settings for Accounts,設置帳戶 +DocType: Accounts Settings,Settings for Accounts,會計設定 apps/erpnext/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +800,Supplier Invoice No exists in Purchase Invoice {0},供應商發票不存在採購發票{0} apps/erpnext/erpnext/config/selling.py +118,Manage Sales Person Tree.,管理銷售人員樹。 apps/erpnext/erpnext/stock/doctype/delivery_trip/delivery_trip.py +105,"Cannot process route, since Google Maps Settings is disabled.",由於禁用了Google地圖設置,因此無法處理路線。 @@ -521,7 +521,7 @@ apps/erpnext/erpnext/setup/doctype/email_digest/templates/default.html +97,Upcom apps/erpnext/erpnext/public/js/templates/item_quick_entry.html +1,Variant Attributes,變量屬性 apps/erpnext/erpnext/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.py +114,Please select month and year,請選擇年份和月份 DocType: Employee,Company Email,企業郵箱 -DocType: GL Entry,Debit Amount in Account Currency,在賬戶幣種借記金額 +DocType: GL Entry,Debit Amount in Account Currency,在科目幣種借記金額 apps/erpnext/erpnext/crm/report/campaign_efficiency/campaign_efficiency.py +21,Order Value,訂單價值 apps/erpnext/erpnext/crm/report/campaign_efficiency/campaign_efficiency.py +21,Order Value,訂單價值 DocType: Certified Consultant,Certified Consultant,認證顧問 @@ -550,7 +550,7 @@ apps/erpnext/erpnext/accounts/doctype/cost_center/cost_center.js +101,Convert to DocType: Project Update,Good/Steady,好/穩定 DocType: Bank Statement Transaction Invoice Item,Invoice Date,發票日期 DocType: GL Entry,Debit Amount,借方金額 -apps/erpnext/erpnext/accounts/party.py +277,There can only be 1 Account per Company in {0} {1},只能有每公司1帳戶{0} {1} +apps/erpnext/erpnext/accounts/party.py +277,There can only be 1 Account per Company in {0} {1},只能有每公司1科目{0} {1} DocType: Support Search Source,Response Result Key Path,響應結果關鍵路徑 DocType: Journal Entry,Inter Company Journal Entry,Inter公司日記帳分錄 apps/erpnext/erpnext/stock/doctype/stock_entry/stock_entry.py +534,For quantity {0} should not be grater than work order quantity {1},數量{0}不應超過工單數量{1} @@ -562,7 +562,7 @@ apps/erpnext/erpnext/accounts/report/accounts_receivable/accounts_receivable.htm DocType: Setup Progress Action,Action Document,行動文件 DocType: Chapter Member,Website URL,網站網址 DocType: Delivery Note,Instructions,說明 -DocType: Quality Inspection,Inspected By,視察 +DocType: Quality Inspection,Inspected By,檢查 DocType: Asset Maintenance Log,Maintenance Type,維護類型 apps/erpnext/erpnext/education/doctype/student_group/student_group.py +45,{0} - {1} is not enrolled in the Course {2},{0} - {1} 未在課程中註冊 {2} apps/erpnext/erpnext/education/doctype/student_report_generation_tool/student_report_generation_tool.html +225,Student Name: ,學生姓名: @@ -642,17 +642,17 @@ apps/erpnext/erpnext/stock/doctype/packing_slip/packing_slip.js +57,'To Case No. DocType: Certification Application,Non Profit,非營利 DocType: Production Plan,Not Started,未啟動 DocType: Lead,Channel Partner,渠道合作夥伴 -DocType: Account,Old Parent,老家長 +DocType: Account,Old Parent,舊上級 apps/erpnext/erpnext/education/doctype/program_enrollment_tool/program_enrollment_tool.py +24,Mandatory field - Academic Year,必修課 - 學年 apps/erpnext/erpnext/education/doctype/program_enrollment_tool/program_enrollment_tool.py +24,Mandatory field - Academic Year,必修課 - 學年 apps/erpnext/erpnext/accounts/doctype/payment_entry/payment_entry.py +221,{0} {1} is not associated with {2} {3},{0} {1} 未與 {2} {3} 關聯 DocType: Notification Control,Customize the introductory text that goes as a part of that email. Each transaction has a separate introductory text.,自定義去作為郵件的一部分的介紹文字。每筆交易都有一個單獨的介紹性文字。 apps/erpnext/erpnext/manufacturing/doctype/job_card/job_card.py +56,Row {0} : Operation is required against the raw material item {1},行{0}:對原材料項{1}需要操作 -apps/erpnext/erpnext/hr/doctype/expense_claim/expense_claim.py +180,Please set default payable account for the company {0},請為公司{0}設置預設應付賬款 +apps/erpnext/erpnext/hr/doctype/expense_claim/expense_claim.py +180,Please set default payable account for the company {0},請為公司{0}設置預設應付帳款 apps/erpnext/erpnext/stock/doctype/stock_entry/stock_entry.py +608,Transaction not allowed against stopped Work Order {0},不允許對停止的工單{0}進行交易 DocType: Setup Progress Action,Min Doc Count,最小文件計數 apps/erpnext/erpnext/config/manufacturing.py +84,Global settings for all manufacturing processes.,所有製造過程中的全域設定。 -DocType: Accounts Settings,Accounts Frozen Upto,帳戶被凍結到 +DocType: Accounts Settings,Accounts Frozen Upto,科目被凍結到 DocType: SMS Log,Sent On,發送於 apps/erpnext/erpnext/stock/doctype/item/item.py +778,Attribute {0} selected multiple times in Attributes Table,屬性{0}多次選擇在屬性表 DocType: HR Settings,Employee record is created using selected field. ,使用所選欄位創建員工記錄。 @@ -717,7 +717,7 @@ apps/erpnext/erpnext/education/doctype/student_group/student_group.py +22,Please DocType: Codification Table,Codification Table,編纂表 DocType: Timesheet Detail,Hrs,小時 apps/erpnext/erpnext/accounts/doctype/journal_entry/journal_entry.js +410,Please select Company,請選擇公司 -DocType: Stock Entry Detail,Difference Account,差異帳戶 +DocType: Stock Entry Detail,Difference Account,差異科目 DocType: Purchase Invoice,Supplier GSTIN,供應商GSTIN apps/erpnext/erpnext/projects/doctype/task/task.py +50,Cannot close task as its dependant task {0} is not closed.,不能因為其依賴的任務{0}沒有關閉關閉任務。 apps/erpnext/erpnext/manufacturing/doctype/production_planning_tool/production_planning_tool.py +435,Please enter Warehouse for which Material Request will be raised,請輸入物料需求欲增加的倉庫 @@ -759,7 +759,7 @@ apps/erpnext/erpnext/config/stock.py +337,Managing Subcontracting,管理轉包 DocType: Vital Signs,Body Temperature,體溫 DocType: Project,Project will be accessible on the website to these users,項目將在網站向這些用戶上訪問 apps/erpnext/erpnext/stock/doctype/serial_no/serial_no.py +292,Cannot cancel {0} {1} because Serial No {2} does not belong to the warehouse {3},無法取消{0} {1},因為序列號{2}不屬於倉庫{3} -DocType: Company,Default Deferred Expense Account,默認遞延費用帳戶 +DocType: Company,Default Deferred Expense Account,默認遞延費用科目 apps/erpnext/erpnext/config/projects.py +29,Define Project type.,定義項目類型。 DocType: Supplier Scorecard,Weighting Function,加權函數 DocType: Healthcare Practitioner,OP Consulting Charge,OP諮詢費 @@ -767,7 +767,7 @@ apps/erpnext/erpnext/utilities/user_progress.py +28,Setup your ,設置你的 DocType: Student Report Generation Tool,Show Marks,顯示標記 DocType: Support Settings,Get Latest Query,獲取最新查詢 DocType: Quotation,Rate at which Price list currency is converted to company's base currency,價目表貨幣被換算成公司基礎貨幣的匯率 -apps/erpnext/erpnext/setup/doctype/company/company.py +74,Account {0} does not belong to company: {1},帳戶{0}不屬於公司:{1} +apps/erpnext/erpnext/setup/doctype/company/company.py +74,Account {0} does not belong to company: {1},科目{0}不屬於公司:{1} apps/erpnext/erpnext/setup/doctype/company/company.py +56,Abbreviation already used for another company,另一家公司已使用此縮寫 DocType: Selling Settings,Default Customer Group,預設客戶群組 DocType: Employee,IFSC Code,IFSC代碼 @@ -792,20 +792,20 @@ apps/erpnext/erpnext/accounts/doctype/journal_entry/journal_entry.py +76,Total C DocType: Installation Note Item,Installation Note Item,安裝注意項 DocType: Production Plan Item,Pending Qty,待定數量 apps/erpnext/erpnext/accounts/party.py +429,{0} {1} is not active,{0} {1}是不活動 -DocType: Woocommerce Settings,Freight and Forwarding Account,貨運和轉運帳戶 +DocType: Woocommerce Settings,Freight and Forwarding Account,貨運和轉運科目 apps/erpnext/erpnext/config/accounts.py +240,Setup cheque dimensions for printing,設置檢查尺寸打印 apps/erpnext/erpnext/hr/doctype/payroll_entry/payroll_entry.js +33,Create Salary Slips,創建工資單 DocType: Vital Signs,Bloated,脹 DocType: Salary Slip,Salary Slip Timesheet,工資單時間表 apps/erpnext/erpnext/controllers/buying_controller.py +200,Supplier Warehouse mandatory for sub-contracted Purchase Receipt,對於轉包的採購入庫單,供應商倉庫是強制性輸入的。 DocType: Sales Invoice,Total Commission,佣金總計 -DocType: Tax Withholding Account,Tax Withholding Account,扣繳稅款賬戶 +DocType: Tax Withholding Account,Tax Withholding Account,扣繳稅款科目 DocType: Pricing Rule,Sales Partner,銷售合作夥伴 apps/erpnext/erpnext/config/buying.py +150,All Supplier scorecards.,所有供應商記分卡。 DocType: Buying Settings,Purchase Receipt Required,需要採購入庫單 DocType: Delivery Note,Rail,軌 apps/erpnext/erpnext/stock/doctype/stock_entry/stock_entry.py +267,Target warehouse in row {0} must be same as Work Order,行{0}中的目標倉庫必須與工單相同 -apps/erpnext/erpnext/stock/doctype/item/item.py +183,Valuation Rate is mandatory if Opening Stock entered,估價費用是強制性的,如果打開股票進入 +apps/erpnext/erpnext/stock/doctype/item/item.py +183,Valuation Rate is mandatory if Opening Stock entered,估價費用是強制性的,如果打開庫存進入 apps/erpnext/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +143,No records found in the Invoice table,沒有在發票表中找到記錄 apps/erpnext/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js +36,Please select Company and Party Type first,請選擇公司和黨的第一型 apps/erpnext/erpnext/accounts/doctype/pos_profile/pos_profile.py +31,"Already set default in pos profile {0} for user {1}, kindly disabled default",已經在用戶{1}的pos配置文件{0}中設置了默認值,請禁用默認值 @@ -828,7 +828,7 @@ apps/erpnext/erpnext/hr/doctype/attendance_request/attendance_request.py +18,Hal apps/erpnext/erpnext/public/js/pos/pos.html +4,Item Cart,項目車 apps/erpnext/erpnext/accounts/doctype/fiscal_year/fiscal_year.py +38,Fiscal Year Start Date should not be greater than Fiscal Year End Date,會計年度開始日期應不大於財政年度結束日期 DocType: Issue,Resolution,決議 -DocType: Employee,Personal Bio,個人生物 +DocType: Employee,Personal Bio,個人自傳 apps/erpnext/erpnext/non_profit/report/expiring_memberships/expiring_memberships.py +15,Membership ID,會員ID apps/erpnext/erpnext/templates/pages/order.html +77,Delivered: {0},交貨:{0} DocType: QuickBooks Migrator,Connected to QuickBooks,連接到QuickBooks @@ -868,7 +868,7 @@ DocType: Loan Application,Total Payable Interest,合計應付利息 apps/erpnext/erpnext/education/doctype/fee_schedule/fee_schedule.js +58,Total Outstanding: {0},總計:{0} DocType: Sales Invoice Timesheet,Sales Invoice Timesheet,銷售發票時間表 apps/erpnext/erpnext/accounts/doctype/journal_entry/journal_entry.py +150,Reference No & Reference Date is required for {0},參考號與參考日期須為{0} -DocType: Payroll Entry,Select Payment Account to make Bank Entry,選擇付款賬戶,使銀行進入 +DocType: Payroll Entry,Select Payment Account to make Bank Entry,選擇付款科目,使銀行進入 DocType: Hotel Settings,Default Invoice Naming Series,默認發票命名系列 apps/erpnext/erpnext/utilities/activation.py +136,"Create Employee records to manage leaves, expense claims and payroll",建立員工檔案管理葉,報銷和工資 apps/erpnext/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +712,An error occurred during the update process,更新過程中發生錯誤 @@ -898,7 +898,7 @@ DocType: Timesheet,Billed,計費 DocType: Batch,Batch Description,批次說明 apps/erpnext/erpnext/education/doctype/student_group_creation_tool/student_group_creation_tool.js +12,Creating student groups,創建學生組 apps/erpnext/erpnext/education/doctype/student_group_creation_tool/student_group_creation_tool.js +12,Creating student groups,創建學生組 -apps/erpnext/erpnext/accounts/utils.py +763,"Payment Gateway Account not created, please create one manually.",支付網關帳戶沒有創建,請手動創建一個。 +apps/erpnext/erpnext/accounts/utils.py +763,"Payment Gateway Account not created, please create one manually.",支付閘道科目沒有創建,請手動創建一個。 apps/erpnext/erpnext/education/doctype/student_applicant/student_applicant.py +51,Not eligible for the admission in this program as per DOB,按照DOB的規定,沒有資格參加本計劃 DocType: Sales Invoice,Sales Taxes and Charges,銷售稅金及費用 DocType: Student,Sibling Details,兄弟姐妹詳情 @@ -922,7 +922,7 @@ apps/erpnext/erpnext/education/report/student_and_guardian_contact_details/stude apps/erpnext/erpnext/setup/setup_wizard/operations/install_fixtures.py +87,Manager,經理 apps/erpnext/erpnext/accounts/report/budget_variance_report/budget_variance_report.js +8,From Fiscal Year,從財政年度開始 apps/erpnext/erpnext/selling/doctype/customer/customer.py +185,New credit limit is less than current outstanding amount for the customer. Credit limit has to be atleast {0},新的信用額度小於當前餘額為客戶著想。信用額度是ATLEAST {0} -apps/erpnext/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +453,Please set account in Warehouse {0},請在倉庫{0}中設置帳戶 +apps/erpnext/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +453,Please set account in Warehouse {0},請在倉庫{0}中設置會計科目 apps/erpnext/erpnext/controllers/trends.py +39,'Based On' and 'Group By' can not be same,“根據”和“分組依據”不能相同 DocType: Sales Person,Sales Person Targets,銷售人員目標 DocType: Work Order Operation,In minutes,在幾分鐘內 @@ -970,7 +970,7 @@ apps/erpnext/erpnext/config/accounts.py +293,To make recurring documents,複製 DocType: Loan,Total Interest Payable,合計應付利息 DocType: Landed Cost Taxes and Charges,Landed Cost Taxes and Charges,到岸成本稅費 DocType: Work Order Operation,Actual Start Time,實際開始時間 -DocType: Purchase Invoice Item,Deferred Expense Account,遞延費用帳戶 +DocType: Purchase Invoice Item,Deferred Expense Account,遞延費用科目 DocType: BOM Operation,Operation Time,操作時間 apps/erpnext/erpnext/manufacturing/doctype/work_order/work_order.js +466,Finish,完 apps/erpnext/erpnext/hr/doctype/salary_structure/salary_structure.js +441,Base,基礎 @@ -979,7 +979,7 @@ DocType: Travel Itinerary,Travel To,前往 apps/erpnext/erpnext/selling/page/point_of_sale/point_of_sale.js +1659,Write Off Amount,核銷金額 DocType: Leave Block List Allow,Allow User,允許用戶 DocType: Journal Entry,Bill No,帳單號碼 -DocType: Company,Gain/Loss Account on Asset Disposal,在資產處置收益/損失帳戶 +DocType: Company,Gain/Loss Account on Asset Disposal,在資產處置收益/損失科目 DocType: Vehicle Log,Service Details,服務細節 DocType: Vehicle Log,Service Details,服務細節 DocType: Lab Test Template,Grouped,分組 @@ -1013,7 +1013,7 @@ apps/erpnext/erpnext/hr/doctype/salary_structure/salary_structure.js +408,Previe apps/erpnext/erpnext/accounts/doctype/budget/budget.py +64,Account {0} has been entered multiple times,帳戶{0}已多次輸入 DocType: Account,Expenses Included In Valuation,支出計入估值 apps/erpnext/erpnext/non_profit/doctype/membership/membership.py +38,You can only renew if your membership expires within 30 days,如果您的會員資格在30天內到期,您只能續訂 -DocType: Shopping Cart Settings,Show Stock Availability,顯示股票可用性 +DocType: Shopping Cart Settings,Show Stock Availability,顯示庫存可用性 apps/erpnext/erpnext/assets/doctype/asset/asset.py +519,Set {0} in asset category {1} or company {2},在資產類別{1}或公司{2}中設置{0} DocType: Location,Longitude,經度 ,Absent Student Report,缺席學生報告 @@ -1038,7 +1038,7 @@ apps/erpnext/erpnext/education/doctype/student_group/student_group.py +24,Please DocType: Project,Estimated Cost,估計成本 DocType: Request for Quotation,Link to material requests,鏈接到材料請求 DocType: Journal Entry,Credit Card Entry,信用卡進入 -apps/erpnext/erpnext/config/accounts.py +35,Company and Accounts,公司與賬戶 +apps/erpnext/erpnext/config/accounts.py +35,Company and Accounts,公司與科目 apps/erpnext/erpnext/stock/report/stock_balance/stock_balance.py +85,In Value,在數值 DocType: Asset Settings,Depreciation Options,折舊選項 apps/erpnext/erpnext/assets/doctype/asset_movement/asset_movement.py +28,Either location or employee must be required,必須要求地點或員工 @@ -1055,7 +1055,7 @@ DocType: Purchase Order,Supply Raw Materials,供應原料 apps/erpnext/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py +10,Current Assets,流動資產 apps/erpnext/erpnext/stock/doctype/stock_entry/stock_entry.py +161,{0} is not a stock Item,{0}不是庫存項目 apps/erpnext/erpnext/hr/notification/training_feedback/training_feedback.html +6,Please share your feedback to the training by clicking on 'Training Feedback' and then 'New',請通過點擊“培訓反饋”,然後點擊“新建” -DocType: Mode of Payment Account,Default Account,預設帳戶 +DocType: Mode of Payment Account,Default Account,預設科目 apps/erpnext/erpnext/stock/doctype/item/item.py +302,Please select Sample Retention Warehouse in Stock Settings first,請先在庫存設置中選擇樣品保留倉庫 apps/erpnext/erpnext/accounts/doctype/loyalty_program/loyalty_program.js +62,Please select the Multiple Tier Program type for more than one collection rules.,請為多個收集規則選擇多層程序類型。 DocType: Payment Entry,Received Amount (Company Currency),收到的款項(公司幣種) @@ -1069,7 +1069,7 @@ DocType: Work Order Operation,Planned End Time,計劃結束時間 apps/erpnext/erpnext/accounts/doctype/account/account.py +100,Account with existing transaction cannot be converted to ledger,帳戶與現有的交易不能被轉換為總賬 apps/erpnext/erpnext/config/non_profit.py +33,Memebership Type Details,Memebership類型詳細信息 DocType: Delivery Note,Customer's Purchase Order No,客戶的採購訂單編號 -DocType: Clinical Procedure,Consume Stock,消費股票 +DocType: Clinical Procedure,Consume Stock,消費庫存 DocType: Budget,Budget Against,反對財政預算案 apps/erpnext/erpnext/stock/reorder_item.py +194,Auto Material Requests Generated,汽車材料的要求生成 apps/erpnext/erpnext/buying/doctype/supplier_quotation/supplier_quotation_list.js +7,Lost,丟失 @@ -1084,7 +1084,7 @@ DocType: Special Test Items,Particulars,細節 apps/erpnext/erpnext/hr/doctype/leave_application/leave_application.py +23,{0}: From {0} of type {1},{0}:從{0}類型{1} apps/erpnext/erpnext/controllers/buying_controller.py +399,Row {0}: Conversion Factor is mandatory,列#{0}:轉換係數是強制性的 apps/erpnext/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +353,"Multiple Price Rules exists with same criteria, please resolve conflict by assigning priority. Price Rules: {0}",海報價格規則,同樣的標準存在,請通過分配優先解決衝突。價格規則:{0} -DocType: Exchange Rate Revaluation,Exchange Rate Revaluation Account,匯率重估賬戶 +DocType: Exchange Rate Revaluation,Exchange Rate Revaluation Account,匯率重估科目 apps/erpnext/erpnext/manufacturing/doctype/bom/bom.py +539,Cannot deactivate or cancel BOM as it is linked with other BOMs,無法關閉或取消BOM,因為它是與其他材料明細表鏈接 apps/erpnext/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.js +106,Please select Company and Posting Date to getting entries,請選擇公司和發布日期以獲取條目 DocType: Asset,Maintenance,維護 @@ -1146,9 +1146,9 @@ apps/erpnext/erpnext/regional/report/eway_bill/eway_bill.py +163,Doc Name,文件 DocType: Expense Claim Detail,Expense Claim Type,費用報銷型 DocType: Shopping Cart Settings,Default settings for Shopping Cart,對購物車的預設設定 apps/erpnext/erpnext/healthcare/doctype/practitioner_schedule/practitioner_schedule.js +27,Add Timeslots,添加時代 -apps/erpnext/erpnext/stock/__init__.py +57,Please set Account in Warehouse {0} or Default Inventory Account in Company {1},請在倉庫{0}中設置帳戶或在公司{1}中設置默認庫存帳戶 +apps/erpnext/erpnext/stock/__init__.py +57,Please set Account in Warehouse {0} or Default Inventory Account in Company {1},請在倉庫{0}中設科目或在公司{1}中設置默認庫存科目 apps/erpnext/erpnext/assets/doctype/asset/depreciation.py +143,Asset scrapped via Journal Entry {0},通過資產日記帳分錄報廢{0} -DocType: Loan,Interest Income Account,利息收入賬戶 +DocType: Loan,Interest Income Account,利息收入科目 apps/erpnext/erpnext/hr/doctype/salary_structure/salary_structure.py +61,Max benefits should be greater than zero to dispense benefits,最大的好處應該大於零來分配好處 apps/erpnext/erpnext/non_profit/doctype/grant_application/grant_application.py +58,Review Invitation Sent,審核邀請已發送 DocType: Shift Assignment,Shift Assignment,班次分配 @@ -1167,7 +1167,7 @@ DocType: Account,Liability,責任 apps/erpnext/erpnext/hr/doctype/expense_claim/expense_claim.py +228,Sanctioned Amount cannot be greater than Claim Amount in Row {0}.,制裁金額不能大於索賠額行{0}。 apps/erpnext/erpnext/education/report/course_wise_assessment_report/course_wise_assessment_report.html +14,Academic Term: ,學術期限: DocType: Salary Component,Do not include in total,不包括在內 -DocType: Company,Default Cost of Goods Sold Account,銷貨帳戶的預設成本 +DocType: Company,Default Cost of Goods Sold Account,銷貨成本科目 apps/erpnext/erpnext/stock/doctype/stock_entry/stock_entry.py +1287,Sample quantity {0} cannot be more than received quantity {1},採樣數量{0}不能超過接收數量{1} apps/erpnext/erpnext/stock/get_item_details.py +523,Price List not selected,未選擇價格列表 DocType: Request for Quotation Supplier,Send Email,發送電子郵件 @@ -1176,7 +1176,7 @@ DocType: Item,Max Sample Quantity,最大樣品量 apps/erpnext/erpnext/accounts/doctype/journal_entry/journal_entry.py +810,No Permission,無權限 DocType: Contract Fulfilment Checklist,Contract Fulfilment Checklist,合同履行清單 DocType: Vital Signs,Heart Rate / Pulse,心率/脈搏 -DocType: Company,Default Bank Account,預設銀行帳戶 +DocType: Company,Default Bank Account,預設銀行會計科目 apps/erpnext/erpnext/accounts/report/general_ledger/general_ledger.py +76,"To filter based on Party, select Party Type first",要根據黨的篩選,選擇黨第一類型 apps/erpnext/erpnext/controllers/sales_and_purchase_return.py +46,'Update Stock' can not be checked because items are not delivered via {0},不能勾選`更新庫存',因為項目未交付{0} DocType: Vehicle,Acquisition Date,採集日期 @@ -1202,7 +1202,7 @@ DocType: Item,Website Warehouse,網站倉庫 DocType: Payment Reconciliation,Minimum Invoice Amount,最小發票金額 apps/erpnext/erpnext/accounts/doctype/gl_entry/gl_entry.py +112,{0} {1}: Cost Center {2} does not belong to Company {3},{0} {1}:成本中心{2}不屬於公司{3} apps/erpnext/erpnext/utilities/user_progress.py +92,Upload your letter head (Keep it web friendly as 900px by 100px),上傳你的信頭(保持網頁友好,900px乘100px) -apps/erpnext/erpnext/accounts/doctype/gl_entry/gl_entry.py +89,{0} {1}: Account {2} cannot be a Group,{0} {1}帳戶{2}不能是一個組 +apps/erpnext/erpnext/accounts/doctype/gl_entry/gl_entry.py +89,{0} {1}: Account {2} cannot be a Group,{0} {1}科目{2}不能是一個群組科目 apps/erpnext/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +367,Timesheet {0} is already completed or cancelled,時間表{0}已完成或取消 apps/erpnext/erpnext/templates/pages/projects.html +42,No tasks,沒有任務 apps/erpnext/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py +81,Sales Invoice {0} created as paid,銷售發票{0}已創建為已付款 @@ -1282,7 +1282,7 @@ apps/erpnext/erpnext/projects/report/project_wise_stock_tracking/project_wise_st apps/erpnext/erpnext/config/selling.py +332,Point-of-Sale,銷售點 DocType: Fee Schedule,Fee Creation Status,費用創建狀態 DocType: Vehicle Log,Odometer Reading,里程表讀數 -apps/erpnext/erpnext/accounts/doctype/account/account.py +123,"Account balance already in Credit, you are not allowed to set 'Balance Must Be' as 'Debit'",帳戶餘額已歸為信用帳戶,不允許設為借方帳戶 +apps/erpnext/erpnext/accounts/doctype/account/account.py +123,"Account balance already in Credit, you are not allowed to set 'Balance Must Be' as 'Debit'",科目餘額已歸為貸方,不允許設為借方 DocType: Account,Balance must be,餘額必須 DocType: Notification Control,Expense Claim Rejected Message,報銷回絕訊息 ,Available Qty,可用數量 @@ -1349,9 +1349,9 @@ apps/erpnext/erpnext/stock/report/item_prices/item_prices.py +40,Sales Price Lis DocType: Healthcare Settings,"If checked, a customer will be created, mapped to Patient. Patient Invoices will be created against this Customer. You can also select existing Customer while creating Patient.",如果選中,將創建一個客戶,映射到患者。將針對該客戶創建病人發票。您也可以在創建患者時選擇現有客戶。 apps/erpnext/erpnext/accounts/doctype/loyalty_program/loyalty_program.py +63,Customer isn't enrolled in any Loyalty Program,客戶未加入任何忠誠度計劃 -DocType: Bank Reconciliation,Account Currency,賬戶幣種 +DocType: Bank Reconciliation,Account Currency,科目幣種 DocType: Lab Test,Sample ID,樣品編號 -apps/erpnext/erpnext/accounts/general_ledger.py +178,Please mention Round Off Account in Company,請註明舍入賬戶的公司 +apps/erpnext/erpnext/accounts/general_ledger.py +178,Please mention Round Off Account in Company,請註明舍入科目的公司 DocType: Purchase Receipt,Range,範圍 DocType: Supplier,Default Payable Accounts,預設應付帳款 apps/erpnext/erpnext/hr/doctype/attendance/attendance.py +52,Employee {0} is not active or does not exist,員工{0}不活躍或不存在 @@ -1471,7 +1471,7 @@ DocType: Repayment Schedule,Balance Loan Amount,平衡貸款額 apps/erpnext/erpnext/hr/doctype/employee_promotion/employee_promotion.js +132,Added to details,添加到細節 apps/erpnext/erpnext/education/doctype/course_scheduling_tool/course_scheduling_tool.js +14,Schedule Course,課程時間表 DocType: Budget,Applicable on Material Request,適用於材料請求 -apps/erpnext/erpnext/setup/setup_wizard/operations/install_fixtures.py +194,Stock Options,股票期權 +apps/erpnext/erpnext/setup/setup_wizard/operations/install_fixtures.py +194,Stock Options,庫存期權 apps/erpnext/erpnext/selling/page/point_of_sale/point_of_sale.js +628,No Items added to cart,沒有項目已添加到購物車 DocType: Journal Entry Account,Expense Claim,報銷 apps/erpnext/erpnext/assets/doctype/asset/asset.js +352,Do you really want to restore this scrapped asset?,難道你真的想恢復這個報廢的資產? @@ -1491,7 +1491,7 @@ DocType: Landed Cost Purchase Receipt,Landed Cost Purchase Receipt,到岸成本 DocType: Company,Default Terms,默認條款 DocType: Supplier Scorecard Period,Criteria,標準 DocType: Packing Slip Item,Packing Slip Item,包裝單項目 -DocType: Purchase Invoice,Cash/Bank Account,現金/銀行帳戶 +DocType: Purchase Invoice,Cash/Bank Account,現金/銀行會計科目 DocType: Travel Itinerary,Train,培養 apps/erpnext/erpnext/public/js/queries.js +96,Please specify a {0},請指定{0} apps/erpnext/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +74,Removed items with no change in quantity or value.,刪除的項目在數量或價值沒有變化。 @@ -1516,7 +1516,7 @@ DocType: Agriculture Task,Urgent,緊急 apps/erpnext/erpnext/accounts/doctype/payment_entry/payment_entry.js +187,Please specify a valid Row ID for row {0} in table {1},請指定行{0}在表中的有效行ID {1} apps/erpnext/erpnext/buying/doctype/supplier_scorecard_criteria/supplier_scorecard_criteria.py +84,Unable to find variable: ,無法找到變量: apps/erpnext/erpnext/selling/page/point_of_sale/point_of_sale.js +893,Please select a field to edit from numpad,請選擇要從數字鍵盤編輯的字段 -apps/erpnext/erpnext/stock/doctype/item/item.py +293,Cannot be a fixed asset item as Stock Ledger is created.,不能成為股票分類賬創建的固定資產項目。 +apps/erpnext/erpnext/stock/doctype/item/item.py +293,Cannot be a fixed asset item as Stock Ledger is created.,不能成為庫存分類賬創建的固定資產項目。 apps/erpnext/erpnext/healthcare/doctype/inpatient_record/inpatient_record.js +7,Admit,承認 apps/erpnext/erpnext/setup/page/welcome_to_erpnext/welcome_to_erpnext.html +23,Go to the Desktop and start using ERPNext,轉到桌面和開始使用ERPNext apps/erpnext/erpnext/templates/pages/order.js +31,Pay Remaining,支付剩餘 @@ -1563,7 +1563,7 @@ DocType: Buying Settings,Material Transferred for Subcontract,轉包材料轉讓 DocType: Email Digest,Purchase Orders Items Overdue,採購訂單項目逾期 apps/erpnext/erpnext/accounts/page/pos/pos.js +1638,ZIP Code,郵政編碼 apps/erpnext/erpnext/controllers/selling_controller.py +265,Sales Order {0} is {1},銷售訂單{0} {1} -apps/erpnext/erpnext/hr/doctype/payroll_entry/payroll_entry.py +260,Select interest income account in loan {0},選擇貸款{0}中的利息收入帳戶 +apps/erpnext/erpnext/hr/doctype/payroll_entry/payroll_entry.py +260,Select interest income account in loan {0},選擇貸款{0}中的利息收入科目 DocType: Opportunity,Contact Info,聯絡方式 apps/erpnext/erpnext/config/stock.py +322,Making Stock Entries,製作Stock條目 apps/erpnext/erpnext/hr/doctype/employee_promotion/employee_promotion.py +15,Cannot promote Employee with status Left,無法提升狀態為Left的員工 @@ -1681,10 +1681,10 @@ apps/erpnext/erpnext/accounts/page/pos/pos.js +2530,"Payment Mode is not configu apps/erpnext/erpnext/buying/utils.py +74,Same item cannot be entered multiple times.,同一項目不能輸入多次。 apps/erpnext/erpnext/accounts/doctype/account/account_tree.js +30,"Further accounts can be made under Groups, but entries can be made against non-Groups",進一步帳戶可以根據組進行,但條目可針對非組進行 DocType: Lead,Lead,潛在客戶 -DocType: Email Digest,Payables,應付賬款 +DocType: Email Digest,Payables,應付帳款 DocType: Course,Course Intro,課程介紹 DocType: Amazon MWS Settings,MWS Auth Token,MWS Auth Token -apps/erpnext/erpnext/stock/doctype/batch/batch.js +105,Stock Entry {0} created,股票輸入{0}創建 +apps/erpnext/erpnext/stock/doctype/batch/batch.js +105,Stock Entry {0} created,庫存輸入{0}創建 apps/erpnext/erpnext/accounts/doctype/loyalty_program/loyalty_program.py +110,You don't have enought Loyalty Points to redeem,您沒有獲得忠誠度積分兌換 apps/erpnext/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +23,Please set associated account in Tax Withholding Category {0} against Company {1},請在針對公司{1}的預扣稅分類{0}中設置關聯帳戶 apps/erpnext/erpnext/controllers/buying_controller.py +405,Row #{0}: Rejected Qty can not be entered in Purchase Return,行#{0}:駁回採購退貨數量不能進入 @@ -1764,11 +1764,11 @@ DocType: Work Order,Qty To Manufacture,製造數量 DocType: Buying Settings,Maintain same rate throughout purchase cycle,在整個採購週期價格保持一致 DocType: Opportunity Item,Opportunity Item,項目的機會 ,Student and Guardian Contact Details,學生和監護人聯繫方式 -apps/erpnext/erpnext/accounts/doctype/account/account.js +51,Merge Account,合併帳戶 +apps/erpnext/erpnext/accounts/doctype/account/account.js +51,Merge Account,合併科目 apps/erpnext/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py +53,Row {0}: For supplier {0} Email Address is required to send email,行{0}:對於供應商{0}的電郵地址發送電子郵件 apps/erpnext/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py +76,Temporary Opening,臨時開通 ,Employee Leave Balance,員工休假餘額 -apps/erpnext/erpnext/accounts/doctype/gl_entry/gl_entry.py +148,Balance for Account {0} must always be {1},帳戶{0}的餘額必須始終為{1} +apps/erpnext/erpnext/accounts/doctype/gl_entry/gl_entry.py +148,Balance for Account {0} must always be {1},科目{0}的餘額必須始終為{1} DocType: Patient Appointment,More Info,更多訊息 apps/erpnext/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +181,Valuation Rate required for Item in row {0},行對項目所需的估值速率{0} DocType: Supplier Scorecard,Scorecard Actions,記分卡操作 @@ -1841,7 +1841,7 @@ DocType: Purchase Invoice Item,Item Tax Rate,項目稅率 apps/erpnext/erpnext/regional/report/eway_bill/eway_bill.py +176,From Party Name,來自黨名 DocType: Student Group Student,Group Roll Number,組卷編號 DocType: Student Group Student,Group Roll Number,組卷編號 -apps/erpnext/erpnext/accounts/doctype/journal_entry/journal_entry.py +177,"For {0}, only credit accounts can be linked against another debit entry",{0},只有貸方帳戶可以連接另一個借方分錄 +apps/erpnext/erpnext/accounts/doctype/journal_entry/journal_entry.py +177,"For {0}, only credit accounts can be linked against another debit entry",{0},只有貸方科目可以連接另一個借方分錄 apps/erpnext/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +660,Delivery Note {0} is not submitted,送貨單{0}未提交 apps/erpnext/erpnext/stock/get_item_details.py +167,Item {0} must be a Sub-contracted Item,項{0}必須是一個小項目簽約 apps/erpnext/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py +44,Capital Equipments,資本設備 @@ -1856,7 +1856,7 @@ DocType: Employee,Department and Grade,部門和年級 DocType: Sales Invoice Item,Edit Description,編輯說明 ,Team Updates,團隊更新 apps/erpnext/erpnext/accounts/doctype/payment_order/payment_order.js +39,For Supplier,對供應商 -DocType: Account,Setting Account Type helps in selecting this Account in transactions.,設置帳戶類型有助於在交易中選擇該帳戶。 +DocType: Account,Setting Account Type helps in selecting this Account in transactions.,設置會計科目類型有助於在交易中選擇該科目。 DocType: Purchase Invoice,Grand Total (Company Currency),總計(公司貨幣) apps/erpnext/erpnext/accounts/doctype/cheque_print_template/cheque_print_template.js +9,Create Print Format,創建打印格式 apps/erpnext/erpnext/education/doctype/fee_schedule/fee_schedule_list.js +5,Fee Created,創建費用 @@ -1914,7 +1914,7 @@ DocType: Stock Settings,Naming Series Prefix,命名系列前綴 DocType: Appraisal Template Goal,Appraisal Template Goal,考核目標模板 DocType: Salary Component,Earning,盈利 DocType: Supplier Scorecard,Scoring Criteria,評分標準 -DocType: Purchase Invoice,Party Account Currency,黨的賬戶幣種 +DocType: Purchase Invoice,Party Account Currency,黨的科目幣種 DocType: Delivery Trip,Total Estimated Distance,總估計距離 ,BOM Browser,BOM瀏覽器 apps/erpnext/erpnext/templates/emails/training_event.html +13,Please update your status for this training event,請更新此培訓活動的狀態 @@ -1929,7 +1929,7 @@ DocType: Inpatient Occupancy,Check In,報到 DocType: Maintenance Schedule Item,No of Visits,沒有訪問量的 apps/erpnext/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py +165,Maintenance Schedule {0} exists against {1},針對{1}存在維護計劃{0} apps/erpnext/erpnext/education/doctype/student_applicant/student_applicant.js +36,Enrolling student,招生學生 -apps/erpnext/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py +33,Currency of the Closing Account must be {0},在關閉帳戶的貨幣必須是{0} +apps/erpnext/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py +33,Currency of the Closing Account must be {0},關閉科目的貨幣必須是{0} apps/erpnext/erpnext/hr/doctype/appraisal_template/appraisal_template.py +21,Sum of points for all goals should be 100. It is {0},對所有目標點的總和應該是100。{0} DocType: Project,Start and End Dates,開始和結束日期 DocType: Contract Template Fulfilment Terms,Contract Template Fulfilment Terms,合同模板履行條款 @@ -2027,7 +2027,7 @@ DocType: Job Opening,"Job profile, qualifications required etc.",所需的工作 DocType: Journal Entry Account,Account Balance,帳戶餘額 apps/erpnext/erpnext/config/accounts.py +179,Tax Rule for transactions.,稅收規則進行的交易。 DocType: Rename Tool,Type of document to rename.,的文件類型進行重命名。 -apps/erpnext/erpnext/accounts/doctype/gl_entry/gl_entry.py +53,{0} {1}: Customer is required against Receivable account {2},{0} {1}:需要客戶對應收賬款{2} +apps/erpnext/erpnext/accounts/doctype/gl_entry/gl_entry.py +53,{0} {1}: Customer is required against Receivable account {2},{0} {1}:需要客戶對應收帳款{2} DocType: Purchase Invoice,Total Taxes and Charges (Company Currency),總稅費和費用(公司貨幣) DocType: Weather,Weather Parameter,天氣參數 apps/erpnext/erpnext/accounts/report/trial_balance/trial_balance.js +76,Show unclosed fiscal year's P&L balances,顯示未關閉的會計年度的盈虧平衡 @@ -2036,8 +2036,8 @@ apps/erpnext/erpnext/regional/india/utils.py +179,House rented dates should be a DocType: Clinical Procedure Template,Collection Details,收集細節 DocType: POS Profile,Allow Print Before Pay,付款前允許打印 DocType: Linked Soil Texture,Linked Soil Texture,連接的土壤紋理 -DocType: Shipping Rule,Shipping Account,送貨帳戶 -apps/erpnext/erpnext/accounts/doctype/gl_entry/gl_entry.py +93,{0} {1}: Account {2} is inactive,{0} {1}帳戶{2}無效 +DocType: Shipping Rule,Shipping Account,送貨科目 +apps/erpnext/erpnext/accounts/doctype/gl_entry/gl_entry.py +93,{0} {1}: Account {2} is inactive,{0} {1}科目{2}無效 apps/erpnext/erpnext/utilities/activation.py +82,Make Sales Orders to help you plan your work and deliver on-time,製作銷售訂單,以幫助你計劃你的工作和按時交付 DocType: Bank Statement Transaction Entry,Bank Transaction Entries,銀行交易分錄 DocType: Quality Inspection,Readings,閱讀 @@ -2130,7 +2130,7 @@ DocType: Timesheet Detail,Expected Hrs,預計的小時數 apps/erpnext/erpnext/config/non_profit.py +28,Memebership Details,Memebership細節 DocType: Leave Block List,Block Holidays on important days.,重要的日子中封鎖假期。 apps/erpnext/erpnext/healthcare/doctype/lab_test/lab_test.js +194,Please input all required Result Value(s),請輸入所有必需的結果值(s) -apps/erpnext/erpnext/accounts/report/accounts_receivable/accounts_receivable.js +142,Accounts Receivable Summary,應收賬款匯總 +apps/erpnext/erpnext/accounts/report/accounts_receivable/accounts_receivable.js +142,Accounts Receivable Summary,應收帳款匯總 DocType: POS Closing Voucher,Linked Invoices,鏈接的發票 DocType: Loan,Monthly Repayment Amount,每月還款額 apps/erpnext/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool_dashboard.html +9,Opening Invoices,打開發票 @@ -2257,11 +2257,11 @@ DocType: Loan,Applicant Type,申請人類型 DocType: Purchase Invoice,03-Deficiency in services,03-服務不足 DocType: Healthcare Settings,Default Medical Code Standard,默認醫療代碼標準 apps/erpnext/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +267,Purchase Receipt {0} is not submitted,採購入庫單{0}未提交 -DocType: Company,Default Payable Account,預設應付賬款 +DocType: Company,Default Payable Account,預設應付帳款科目 apps/erpnext/erpnext/config/website.py +17,"Settings for online shopping cart such as shipping rules, price list etc.",設定線上購物車,如航運規則,價格表等 apps/erpnext/erpnext/controllers/website_list_for_contact.py +113,{0}% Billed,{0}%已開立帳單 apps/erpnext/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py +73,Reserved Qty,保留數量 -DocType: Party Account,Party Account,黨的帳戶 +DocType: Party Account,Party Account,參與者科目 apps/erpnext/erpnext/hr/doctype/staffing_plan/staffing_plan.py +142,Please select Company and Designation,請選擇公司和指定 apps/erpnext/erpnext/config/setup.py +116,Human Resources,人力資源 apps/erpnext/erpnext/education/doctype/student_applicant/student_applicant.js +17,Reject,拒絕 @@ -2283,7 +2283,7 @@ DocType: Customer,Default Price List,預設價格表 apps/erpnext/erpnext/assets/doctype/asset/asset.py +492,Asset Movement record {0} created,資產運動記錄{0}創建 apps/erpnext/erpnext/utilities/page/leaderboard/leaderboard.js +175,No items found.,未找到任何項目。 apps/erpnext/erpnext/accounts/doctype/fiscal_year/fiscal_year.py +51,You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global Settings,您不能刪除會計年度{0}。會計年度{0}設置為默認的全局設置 -DocType: Share Transfer,Equity/Liability Account,股票/負債賬戶 +DocType: Share Transfer,Equity/Liability Account,庫存/負債科目 apps/erpnext/erpnext/setup/doctype/customer_group/customer_group.py +20,A customer with the same name already exists,一個同名的客戶已經存在 DocType: Contract,Inactive,待用 apps/erpnext/erpnext/hr/doctype/payroll_entry/payroll_entry.js +224,This will submit Salary Slips and create accrual Journal Entry. Do you want to proceed?,這將提交工資單,並創建權責發生製日記賬分錄。你想繼續嗎? @@ -2292,7 +2292,7 @@ DocType: Purchase Order,Order Confirmation No,訂單確認號 DocType: Purchase Invoice,Eligibility For ITC,適用於ITC的資格 DocType: Journal Entry,Entry Type,條目類型 ,Customer Credit Balance,客戶信用平衡 -apps/erpnext/erpnext/accounts/report/cash_flow/cash_flow.py +82,Net Change in Accounts Payable,應付賬款淨額變化 +apps/erpnext/erpnext/accounts/report/cash_flow/cash_flow.py +82,Net Change in Accounts Payable,應付帳款淨額變化 apps/erpnext/erpnext/selling/doctype/customer/customer.py +257,Credit limit has been crossed for customer {0} ({1}/{2}),客戶{0}({1} / {2})的信用額度已超過 apps/erpnext/erpnext/setup/doctype/authorization_rule/authorization_rule.py +42,Customer required for 'Customerwise Discount',需要' Customerwise折扣“客戶 apps/erpnext/erpnext/config/accounts.py +136,Update bank payment dates with journals.,更新與日記帳之銀行付款日期。 @@ -2313,7 +2313,7 @@ DocType: Special Test Template,Result Component,結果組件 apps/erpnext/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js +46,Warranty Claim,保修索賠 ,Lead Details,潛在客戶詳情 DocType: Salary Slip,Loan repayment,償還借款 -DocType: Share Transfer,Asset Account,資產賬戶 +DocType: Share Transfer,Asset Account,資產科目 DocType: Purchase Invoice,End date of current invoice's period,當前發票的期限的最後一天 DocType: Pricing Rule,Applicable For,適用 DocType: Lab Test,Technician Name,技術員姓名 @@ -2347,7 +2347,7 @@ DocType: Leave Type,Earned Leave,獲得休假 DocType: Employee,Salary Details,薪資明細 DocType: Territory,Territory Manager,區域經理 DocType: Packed Item,To Warehouse (Optional),倉庫(可選) -DocType: GST Settings,GST Accounts,GST賬戶 +DocType: GST Settings,GST Accounts,GST科目 DocType: Payment Entry,Paid Amount (Company Currency),支付的金額(公司貨幣) DocType: Purchase Invoice,Additional Discount,更多優惠 DocType: Selling Settings,Selling Settings,銷售設置 @@ -2382,12 +2382,12 @@ DocType: Material Request,Transferred,轉入 DocType: Vehicle,Doors,門 apps/erpnext/erpnext/setup/setup_wizard/operations/defaults_setup.py +118,ERPNext Setup Complete!,ERPNext設定完成! DocType: Healthcare Settings,Collect Fee for Patient Registration,收取病人登記費 -apps/erpnext/erpnext/stock/doctype/item/item.py +737,Cannot change Attributes after stock transaction. Make a new Item and transfer stock to the new Item,股票交易後不能更改屬性。創建一個新項目並將庫存轉移到新項目 +apps/erpnext/erpnext/stock/doctype/item/item.py +737,Cannot change Attributes after stock transaction. Make a new Item and transfer stock to the new Item,庫存交易後不能更改屬性。創建一個新項目並將庫存轉移到新項目 DocType: Course Assessment Criteria,Weightage,權重 DocType: Purchase Invoice,Tax Breakup,稅收分解 DocType: Employee,Joining Details,加入詳情 DocType: Member,Non Profit Member,非盈利會員 -apps/erpnext/erpnext/accounts/doctype/gl_entry/gl_entry.py +67,{0} {1}: Cost Center is required for 'Profit and Loss' account {2}. Please set up a default Cost Center for the Company.,{0} {1}:需要的損益“賬戶成本中心{2}。請設置為公司默認的成本中心。 +apps/erpnext/erpnext/accounts/doctype/gl_entry/gl_entry.py +67,{0} {1}: Cost Center is required for 'Profit and Loss' account {2}. Please set up a default Cost Center for the Company.,{0} {1}:需要的損益“科目成本中心{2}。請設置為公司默認的成本中心。 apps/erpnext/erpnext/selling/doctype/customer/customer.py +175,A Customer Group exists with same name please change the Customer name or rename the Customer Group,客戶群組存在相同名稱,請更改客戶名稱或重新命名客戶群組 DocType: Location,Area,區 apps/erpnext/erpnext/public/js/templates/contact_list.html +37,New Contact,新建聯絡人 @@ -2474,11 +2474,11 @@ apps/erpnext/erpnext/selling/page/point_of_sale/point_of_sale.js +901,Discount a apps/erpnext/erpnext/accounts/doctype/cost_center/cost_center_tree.js +26,"Number of new Cost Center, it will be included in the cost center name as a prefix",新成本中心的數量,它將作為前綴包含在成本中心名稱中 DocType: Sales Order,To Deliver and Bill,準備交貨及開立發票 DocType: Student Group,Instructors,教師 -DocType: GL Entry,Credit Amount in Account Currency,在賬戶幣金額 +DocType: GL Entry,Credit Amount in Account Currency,在科目幣金額 apps/erpnext/erpnext/manufacturing/doctype/bom/bom.py +631,BOM {0} must be submitted,BOM {0}必須提交 DocType: Authorization Control,Authorization Control,授權控制 apps/erpnext/erpnext/controllers/buying_controller.py +416,Row #{0}: Rejected Warehouse is mandatory against rejected Item {1},行#{0}:拒絕倉庫是強制性的反對否決項{1} -apps/erpnext/erpnext/controllers/stock_controller.py +96,"Warehouse {0} is not linked to any account, please mention the account in the warehouse record or set default inventory account in company {1}.",倉庫{0}未與任何帳戶關聯,請在倉庫記錄中提及該帳戶,或在公司{1}中設置默認庫存帳戶。 +apps/erpnext/erpnext/controllers/stock_controller.py +96,"Warehouse {0} is not linked to any account, please mention the account in the warehouse record or set default inventory account in company {1}.",倉庫{0}未與任何科目關聯,請在倉庫記錄中設定科目,或在公司{1}中設置默認庫存科目。 apps/erpnext/erpnext/utilities/activation.py +81,Manage your orders,管理您的訂單 DocType: Work Order Operation,Actual Time and Cost,實際時間和成本 apps/erpnext/erpnext/stock/doctype/material_request/material_request.py +57,Material Request of maximum {0} can be made for Item {1} against Sales Order {2},針對銷售訂單{2}的項目{1},最多可以有 {0} 被完成。 @@ -2514,14 +2514,14 @@ DocType: SMS Center,Create Receiver List,創建接收器列表 apps/erpnext/erpnext/assets/doctype/asset/asset.py +98,Available-for-use Date should be after purchase date,可供使用的日期應在購買日期之後 DocType: Vehicle,Wheels,車輪 DocType: Packing Slip,To Package No.,以包號 -DocType: Sales Invoice Item,Deferred Revenue Account,遞延收入帳戶 +DocType: Sales Invoice Item,Deferred Revenue Account,遞延收入科目 DocType: Production Plan,Material Requests,材料要求 DocType: Warranty Claim,Issue Date,發行日期 DocType: Activity Cost,Activity Cost,項目成本 DocType: Sales Invoice Timesheet,Timesheet Detail,詳細時間表 DocType: Purchase Receipt Item Supplied,Consumed Qty,消耗的數量 apps/erpnext/erpnext/setup/setup_wizard/data/industry_type.py +52,Telecommunications,電信 -apps/erpnext/erpnext/accounts/party.py +292,Billing currency must be equal to either default company's currency or party account currency,帳單貨幣必須等於默認公司的貨幣或帳戶幣種 +apps/erpnext/erpnext/accounts/party.py +292,Billing currency must be equal to either default company's currency or party account currency,帳單貨幣必須等於默認公司的貨幣或科目幣種 DocType: Packing Slip,Indicates that the package is a part of this delivery (Only Draft),表示該包是這個交付的一部分(僅草案) apps/erpnext/erpnext/controllers/accounts_controller.py +786,Row {0}: Due Date cannot be before posting date,行{0}:到期日期不能在發布日期之前 apps/erpnext/erpnext/accounts/doctype/payment_request/payment_request.js +37,Make Payment Entry,製作付款分錄 @@ -2535,7 +2535,7 @@ apps/erpnext/erpnext/config/accounts.py +209,Tree of financial Cost Centers.,財 apps/erpnext/erpnext/regional/report/eway_bill/eway_bill.py +151,Sub Type,子類型 DocType: Serial No,Delivery Document No,交貨證明文件號碼 DocType: Sales Order Item,Ensure Delivery Based on Produced Serial No,確保基於生產的序列號的交貨 -apps/erpnext/erpnext/assets/doctype/asset/depreciation.py +197,Please set 'Gain/Loss Account on Asset Disposal' in Company {0},請公司制定“關於資產處置收益/損失帳戶”{0} +apps/erpnext/erpnext/assets/doctype/asset/depreciation.py +197,Please set 'Gain/Loss Account on Asset Disposal' in Company {0},請公司制定“關於資產處置收益/損失科目”{0} DocType: Landed Cost Voucher,Get Items From Purchase Receipts,從採購入庫單取得項目 DocType: Serial No,Creation Date,創建日期 apps/erpnext/erpnext/assets/doctype/asset_movement/asset_movement.py +55,Target Location is required for the asset {0},目標位置是資產{0}所必需的 @@ -2569,7 +2569,7 @@ DocType: Bank Guarantee,Margin Money,保證金 DocType: Budget,Budget,預算 apps/erpnext/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js +83,Set Open,設置打開 apps/erpnext/erpnext/stock/doctype/item/item.py +287,Fixed Asset Item must be a non-stock item.,固定資產項目必須是一個非庫存項目。 -apps/erpnext/erpnext/accounts/doctype/budget/budget.py +60,"Budget cannot be assigned against {0}, as it's not an Income or Expense account",財政預算案不能對{0}指定的,因為它不是一個收入或支出帳戶 +apps/erpnext/erpnext/accounts/doctype/budget/budget.py +60,"Budget cannot be assigned against {0}, as it's not an Income or Expense account",財政預算案不能對{0}指定的,因為它不是一個收入或支出科目 apps/erpnext/erpnext/hr/utils.py +228,Max exemption amount for {0} is {1},{0}的最大免除金額為{1} apps/erpnext/erpnext/selling/report/sales_person_target_variance_item_group_wise/sales_person_target_variance_item_group_wise.py +51,Achieved,已實現 DocType: Student Admission,Application Form Route,申請表路線 @@ -2644,15 +2644,15 @@ DocType: Loan Application,Total Payable Amount,合計應付額 DocType: Task,Expected Time (in hours),預期時間(以小時計) DocType: Item Reorder,Check in (group),檢查(組) ,Qty to Order,訂購數量 -DocType: Period Closing Voucher,"The account head under Liability or Equity, in which Profit/Loss will be booked",負債或權益下的帳戶頭,其中利潤/虧損將被黃牌警告 -apps/erpnext/erpnext/accounts/doctype/budget/budget.py +44,Another Budget record '{0}' already exists against {1} '{2}' and account '{3}' for fiscal year {4},對於財務年度{4},{1}'{2}'和帳戶“{3}”已存在另一個預算記錄“{0}” +DocType: Period Closing Voucher,"The account head under Liability or Equity, in which Profit/Loss will be booked",負債或權益下的科目頭,其中利潤/虧損將被黃牌警告 +apps/erpnext/erpnext/accounts/doctype/budget/budget.py +44,Another Budget record '{0}' already exists against {1} '{2}' and account '{3}' for fiscal year {4},對於財務年度{4},{1}'{2}'和科目“{3}”已存在另一個預算記錄“{0}” apps/erpnext/erpnext/config/projects.py +36,Gantt chart of all tasks.,所有任務的甘特圖。 DocType: Opportunity,Mins to First Response,分鐘為第一個反應 DocType: Pricing Rule,Margin Type,保證金類型 apps/erpnext/erpnext/projects/doctype/project/project_dashboard.html +15,{0} hours,{0}小時 DocType: Course,Default Grading Scale,默認等級規模 DocType: Appraisal,For Employee Name,對於員工姓名 -DocType: Woocommerce Settings,Tax Account,稅收帳戶 +DocType: Woocommerce Settings,Tax Account,稅收科目 DocType: C-Form Invoice Detail,Invoice No,發票號碼 DocType: Room,Room Name,房間名稱 DocType: Prescription Duration,Prescription Duration,處方時間 @@ -2704,7 +2704,7 @@ apps/erpnext/erpnext/accounts/doctype/payment_entry/payment_entry.py +389,Amount ,Quotation Trends,報價趨勢 apps/erpnext/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +166,Item Group not mentioned in item master for item {0},項目{0}之項目主檔未提及之項目群組 DocType: GoCardless Mandate,GoCardless Mandate,GoCardless任務 -apps/erpnext/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +440,Debit To account must be a Receivable account,借方帳戶必須是應收帳款帳戶 +apps/erpnext/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +440,Debit To account must be a Receivable account,借方科目必須是應收帳款科目 DocType: Shipping Rule,Shipping Amount,航運量 DocType: Supplier Scorecard Period,Period Score,期間得分 apps/erpnext/erpnext/public/js/event.js +19,Add Customers,添加客戶 @@ -2777,7 +2777,7 @@ apps/erpnext/erpnext/accounts/doctype/fiscal_year/fiscal_year.py +22,{0} is now apps/erpnext/erpnext/projects/doctype/task/task.js +45,Expense Claims,報銷 DocType: Employee Tax Exemption Declaration,Total Exemption Amount,免稅總額 ,BOM Search,BOM搜索 -DocType: Project,Total Consumed Material Cost (via Stock Entry),總消耗材料成本(通過股票輸入) +DocType: Project,Total Consumed Material Cost (via Stock Entry),總消耗材料成本(通過庫存輸入) DocType: Subscription,Subscription Period,訂閱期 apps/erpnext/erpnext/hr/doctype/leave_application/leave_application.js +169,To Date cannot be less than From Date,迄今不能少於起始日期 DocType: Item,"Publish ""In Stock"" or ""Not in Stock"" on Hub based on stock available in this warehouse.",基於倉庫中的庫存,在Hub上發布“庫存”或“庫存”。 @@ -2786,7 +2786,7 @@ apps/erpnext/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_ DocType: Workstation,Wages per hour,時薪 apps/erpnext/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py +47,Stock balance in Batch {0} will become negative {1} for Item {2} at Warehouse {3},在批量庫存餘額{0}將成為負{1}的在倉庫項目{2} {3} apps/erpnext/erpnext/templates/emails/reorder_item.html +1,Following Material Requests have been raised automatically based on Item's re-order level,下列資料的要求已自動根據項目的重新排序水平的提高 -apps/erpnext/erpnext/controllers/accounts_controller.py +376,Account {0} is invalid. Account Currency must be {1},帳戶{0}是無效的。帳戶貨幣必須是{1} +apps/erpnext/erpnext/controllers/accounts_controller.py +376,Account {0} is invalid. Account Currency must be {1},科目{0}是無效的。科目貨幣必須是{1} apps/erpnext/erpnext/hr/doctype/salary_structure_assignment/salary_structure_assignment.py +31,From Date {0} cannot be after employee's relieving Date {1},起始日期{0}不能在員工解除日期之後{1} DocType: Supplier,Is Internal Supplier,是內部供應商 DocType: Employee,Create User Permission,創建用戶權限 @@ -2813,7 +2813,7 @@ apps/erpnext/erpnext/setup/doctype/email_digest/email_digest.js +64,disabled use apps/erpnext/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +970,Quotation,報價 apps/erpnext/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js +1041,Cannot set a received RFQ to No Quote,無法將收到的詢價單設置為無報價 DocType: Salary Slip,Total Deduction,扣除總額 -apps/erpnext/erpnext/accounts/report/general_ledger/general_ledger.py +22,Select an account to print in account currency,選擇一個賬戶以賬戶貨幣進行打印 +apps/erpnext/erpnext/accounts/report/general_ledger/general_ledger.py +22,Select an account to print in account currency,選擇一個科目以科目貨幣進行打印 ,Production Analytics,生產Analytics(分析) apps/erpnext/erpnext/healthcare/doctype/patient/patient_dashboard.py +6,This is based on transactions against this Patient. See timeline below for details,這是基於對這個病人的交易。有關詳情,請參閱下面的時間表 apps/erpnext/erpnext/controllers/sales_and_purchase_return.py +136,Item {0} has already been returned,項{0}已被退回 @@ -2852,7 +2852,7 @@ DocType: BOM,Scrap Material Cost,廢料成本 apps/erpnext/erpnext/stock/doctype/serial_no/serial_no.py +243,Serial No {0} does not belong to any Warehouse,序列號{0}不屬於任何倉庫 DocType: Grant Application,Email Notification Sent,電子郵件通知已發送 DocType: Purchase Invoice,In Words (Company Currency),大寫(Company Currency) -apps/erpnext/erpnext/accounts/doctype/bank_account/bank_account.py +24,Company is manadatory for company account,公司是公司賬戶的管理者 +apps/erpnext/erpnext/accounts/doctype/bank_account/bank_account.py +24,Company is manadatory for company account,公司是公司科目的管理者 apps/erpnext/erpnext/buying/doctype/purchase_order/purchase_order.js +1092,"Item Code, warehouse, quantity are required on row",在行上需要項目代碼,倉庫,數量 DocType: Bank Guarantee,Supplier,供應商 apps/erpnext/erpnext/hr/doctype/department/department.js +9,This is a root department and cannot be edited.,這是根部門,無法編輯。 @@ -2861,7 +2861,7 @@ apps/erpnext/erpnext/crm/report/lead_conversion_time/lead_conversion_time.py +53 apps/erpnext/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py +113,Miscellaneous Expenses,雜項開支 DocType: Global Defaults,Default Company,預設公司 DocType: Company,Transactions Annual History,交易年曆 -apps/erpnext/erpnext/controllers/stock_controller.py +231,Expense or Difference account is mandatory for Item {0} as it impacts overall stock value,對項目{0}而言, 費用或差異帳戶是強制必填的,因為它影響整個庫存總值。 +apps/erpnext/erpnext/controllers/stock_controller.py +231,Expense or Difference account is mandatory for Item {0} as it impacts overall stock value,對項目{0}而言, 費用或差異科目是強制必填的,因為它影響整個庫存總值。 DocType: Bank,Bank Name,銀行名稱 apps/erpnext/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py +78,-Above,-以上 apps/erpnext/erpnext/selling/doctype/sales_order/sales_order.js +1301,Leave the field empty to make purchase orders for all suppliers,將該字段留空以為所有供應商下達採購訂單 @@ -2891,7 +2891,7 @@ DocType: Payment Entry,Unallocated Amount,未分配金額 apps/erpnext/erpnext/accounts/doctype/budget/budget.py +77,Please enable Applicable on Purchase Order and Applicable on Booking Actual Expenses,請啟用適用於採購訂單並適用於預訂實際費用 apps/erpnext/erpnext/templates/includes/product_page.js +101,Cannot find a matching Item. Please select some other value for {0}.,無法找到匹配的項目。請選擇其他值{0}。 DocType: POS Profile,Taxes and Charges,稅收和收費 -DocType: Item,"A Product or a Service that is bought, sold or kept in stock.",產品或服務已購買,出售或持有的股票。 +DocType: Item,"A Product or a Service that is bought, sold or kept in stock.",產品或服務已購買,出售或持有的庫存。 apps/erpnext/erpnext/hr/page/team_updates/team_updates.js +44,No more updates,沒有更多的更新 apps/erpnext/erpnext/accounts/doctype/payment_entry/payment_entry.js +184,Cannot select charge type as 'On Previous Row Amount' or 'On Previous Row Total' for first row,不能選擇充電式為'在上一行量'或'在上一行總'的第一行 apps/erpnext/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard_dashboard.py +6,This covers all scorecards tied to this Setup,這涵蓋了與此安裝程序相關的所有記分卡 @@ -2929,7 +2929,7 @@ apps/erpnext/erpnext/education/doctype/student_group_creation_tool/student_group apps/erpnext/erpnext/education/doctype/student_group_creation_tool/student_group_creation_tool.py +77,{0} Student Groups created.,{0}創建學生組。 DocType: Sales Invoice,Total Billing Amount,總結算金額 apps/erpnext/erpnext/education/doctype/fee_schedule/fee_schedule.py +50,Program in the Fee Structure and Student Group {0} are different.,費用結構和學生組{0}中的課程是不同的。 -DocType: Bank Statement Transaction Entry,Receivable Account,應收賬款 +DocType: Bank Statement Transaction Entry,Receivable Account,應收帳款 apps/erpnext/erpnext/stock/doctype/item_price/item_price.py +31,Valid From Date must be lesser than Valid Upto Date.,有效起始日期必須小於有效起始日期。 apps/erpnext/erpnext/controllers/accounts_controller.py +689,Row #{0}: Asset {1} is already {2},行#{0}:資產{1}已經是{2} DocType: Quotation Item,Stock Balance,庫存餘額 @@ -2939,13 +2939,13 @@ DocType: Expense Claim Detail,Expense Claim Detail,報銷詳情 DocType: Purchase Invoice,TRIPLICATE FOR SUPPLIER,供應商提供服務 DocType: Exchange Rate Revaluation Account,New Balance In Base Currency,基礎貨幣的新平衡 DocType: Crop Cycle,This will be day 1 of the crop cycle,這將是作物週期的第一天 -apps/erpnext/erpnext/accounts/doctype/journal_entry/journal_entry.py +915,Please select correct account,請選擇正確的帳戶 +apps/erpnext/erpnext/accounts/doctype/journal_entry/journal_entry.py +915,Please select correct account,請選擇正確的科目 DocType: Salary Structure Assignment,Salary Structure Assignment,薪酬結構分配 DocType: Purchase Invoice Item,Weight UOM,重量計量單位 apps/erpnext/erpnext/config/accounts.py +435,List of available Shareholders with folio numbers,包含folio號碼的可用股東名單 DocType: Salary Structure Employee,Salary Structure Employee,薪資結構員工 apps/erpnext/erpnext/stock/report/stock_balance/stock_balance.js +62,Show Variant Attributes,顯示變體屬性 -apps/erpnext/erpnext/accounts/doctype/payment_request/payment_request.py +48,The payment gateway account in plan {0} is different from the payment gateway account in this payment request,計劃{0}中的支付網關帳戶與此付款請求中的支付網關帳戶不同 +apps/erpnext/erpnext/accounts/doctype/payment_request/payment_request.py +48,The payment gateway account in plan {0} is different from the payment gateway account in this payment request,計劃{0}中的支付閘道科目與此付款請求中的支付閘道科目不同 DocType: Course,Course Name,課程名 apps/erpnext/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +50,No Tax Withholding data found for the current Fiscal Year.,未找到當前財年的預扣稅數據。 DocType: Employee Leave Approver,Users who can approve a specific employee's leave applications,用戶可以批准特定員工的休假申請 @@ -2989,14 +2989,14 @@ apps/erpnext/erpnext/templates/pages/product_search.html +3,Product Search,產 DocType: Cashier Closing,To Time,要時間 apps/erpnext/erpnext/hr/utils.py +202,) for {0},)為{0} DocType: Authorization Rule,Approving Role (above authorized value),批准角色(上述授權值) -apps/erpnext/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +147,Credit To account must be a Payable account,信用帳戶必須是應付賬款 +apps/erpnext/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +147,Credit To account must be a Payable account,信用科目必須是應付帳款 DocType: Loan,Total Amount Paid,總金額支付 DocType: Asset,Insurance End Date,保險終止日期 apps/erpnext/erpnext/education/doctype/student_applicant/student_applicant.py +43,Please select Student Admission which is mandatory for the paid student applicant,請選擇付費學生申請者必須入學的學生 apps/erpnext/erpnext/manufacturing/doctype/bom/bom.py +370,BOM recursion: {0} cannot be parent or child of {2},BOM遞歸: {0}不能父母或兒童{2} apps/erpnext/erpnext/accounts/doctype/cost_center/cost_center_tree.js +40,Budget List,預算清單 DocType: Work Order Operation,Completed Qty,完成數量 -apps/erpnext/erpnext/accounts/doctype/journal_entry/journal_entry.py +180,"For {0}, only debit accounts can be linked against another credit entry",{0},只有借方帳戶可以連接另一個貸方分錄 +apps/erpnext/erpnext/accounts/doctype/journal_entry/journal_entry.py +180,"For {0}, only debit accounts can be linked against another credit entry",{0},只有借方科目可以連接另一個貸方分錄 DocType: Manufacturing Settings,Allow Overtime,允許加班 apps/erpnext/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +149,"Serialized Item {0} cannot be updated using Stock Reconciliation, please use Stock Entry",序列化項目{0}無法使用庫存調節更新,請使用庫存條目 apps/erpnext/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +149,"Serialized Item {0} cannot be updated using Stock Reconciliation, please use Stock Entry",序列化項目{0}無法使用庫存調節更新,請使用庫存條目 @@ -3010,7 +3010,7 @@ apps/erpnext/erpnext/config/integrations.py +13,GoCardless payment gateway setti apps/erpnext/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py +129,Exchange Gain/Loss,兌換收益/損失 DocType: Opportunity,Lost Reason,失落的原因 DocType: Amazon MWS Settings,Enable Amazon,啟用亞馬遜 -apps/erpnext/erpnext/controllers/accounts_controller.py +322,Row #{0}: Account {1} does not belong to company {2},行#{0}:帳戶{1}不屬於公司{2} +apps/erpnext/erpnext/controllers/accounts_controller.py +322,Row #{0}: Account {1} does not belong to company {2},行#{0}:科目{1}不屬於公司{2} apps/erpnext/erpnext/setup/doctype/naming_series/naming_series.py +30,Unable to find DocType {0},無法找到DocType {0} DocType: Quality Inspection,Sample Size,樣本大小 apps/erpnext/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py +47,Please enter Receipt Document,請輸入收據憑證 @@ -3056,7 +3056,7 @@ DocType: Timesheet Detail,Costing Amount,成本核算金額 DocType: Student Admission Program,Application Fee,報名費 apps/erpnext/erpnext/hr/doctype/payroll_entry/payroll_entry.js +75,Submit Salary Slip,提交工資單 apps/erpnext/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_list.js +13,On Hold,等候接聽 -DocType: Account,Inter Company Account,Inter公司帳戶 +DocType: Account,Inter Company Account,公司內帳戶 apps/erpnext/erpnext/stock/doctype/item_price/item_price.js +17,Import in Bulk,進口散裝 DocType: Sales Partner,Address & Contacts,地址及聯絡方式 DocType: SMS Log,Sender Name,發件人名稱 @@ -3108,7 +3108,7 @@ DocType: BOM,"Specify the operations, operating cost and give a unique Operation DocType: Travel Request,Any other details,任何其他細節 apps/erpnext/erpnext/controllers/status_updater.py +207,This document is over limit by {0} {1} for item {4}. Are you making another {3} against the same {2}?,這份文件是超過限制,通過{0} {1}項{4}。你在做另一個{3}對同一{2}? apps/erpnext/erpnext/public/js/controllers/transaction.js +1288,Please set recurring after saving,請設置保存後復發 -apps/erpnext/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +874,Select change amount account,選擇變化量賬戶 +apps/erpnext/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +874,Select change amount account,選擇變化量科目 DocType: Purchase Invoice,Price List Currency,價格表之貨幣 DocType: Naming Series,User must always select,用戶必須始終選擇 DocType: Stock Settings,Allow Negative Stock,允許負庫存 @@ -3148,7 +3148,7 @@ apps/erpnext/erpnext/accounts/report/general_ledger/general_ledger.py +56,Group apps/erpnext/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js +391,Are you sure you want to cancel this appointment?,你確定要取消這個預約嗎? DocType: Hotel Room Pricing Package,Hotel Room Pricing Package,酒店房間價格套餐 apps/erpnext/erpnext/selling/page/sales_funnel/sales_funnel.js +42,Sales Pipeline,銷售渠道 -apps/erpnext/erpnext/hr/doctype/payroll_entry/payroll_entry.py +167,Please set default account in Salary Component {0},請薪酬部分設置默認帳戶{0} +apps/erpnext/erpnext/hr/doctype/payroll_entry/payroll_entry.py +167,Please set default account in Salary Component {0},請薪酬部分設置默認科目{0} apps/erpnext/erpnext/manufacturing/doctype/production_planning_tool/production_planning_tool.py +200,Please select BOM for Item in Row {0},請行選擇BOM為項目{0} apps/erpnext/erpnext/accounts/doctype/subscription/subscription.js +13,Fetch Subscription Updates,獲取訂閱更新 apps/erpnext/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.py +28,Account {0} does not match with Company {1} in Mode of Account: {2},帳戶{0}與帳戶模式{2}中的公司{1}不符 @@ -3183,7 +3183,7 @@ apps/erpnext/erpnext/manufacturing/doctype/work_order/work_order.js +232,For Job apps/erpnext/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +1556,Prescriptions,處方 DocType: Payment Gateway Account,Payment Account,付款帳號 apps/erpnext/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +1112,Please specify Company to proceed,請註明公司以處理 -apps/erpnext/erpnext/accounts/report/cash_flow/cash_flow.py +81,Net Change in Accounts Receivable,應收賬款淨額變化 +apps/erpnext/erpnext/accounts/report/cash_flow/cash_flow.py +81,Net Change in Accounts Receivable,應收帳款淨額變化 apps/erpnext/erpnext/setup/setup_wizard/operations/install_fixtures.py +66,Compensatory Off,補假 DocType: Job Offer,Accepted,接受的 DocType: POS Closing Voucher,Sales Invoices Summary,銷售發票摘要 @@ -3225,7 +3225,7 @@ DocType: Support Search Source,Result Preview Field,結果預覽字段 DocType: Item Price,Packing Unit,包裝單位 DocType: Subscription,Trialling,試用 DocType: Sales Invoice Item,Deferred Revenue,遞延收入 -DocType: Shopify Settings,Cash Account will used for Sales Invoice creation,現金帳戶將用於創建銷售發票 +DocType: Shopify Settings,Cash Account will used for Sales Invoice creation,現金科目將用於創建銷售發票 DocType: Employee Tax Exemption Declaration Category,Exemption Sub Category,豁免子類別 DocType: Member,Membership Expiry Date,會員到期日 apps/erpnext/erpnext/controllers/sales_and_purchase_return.py +134,{0} must be negative in return document,{0}必須返回文檔中負 @@ -3355,17 +3355,17 @@ DocType: Employee Separation,Employee Separation,員工分離 DocType: BOM Item,Original Item,原始項目 DocType: Purchase Receipt Item,Recd Quantity,到貨數量 apps/erpnext/erpnext/education/doctype/program_enrollment/program_enrollment.py +64,Fee Records Created - {0},費紀錄創造 - {0} -DocType: Asset Category Account,Asset Category Account,資產類別的帳戶 +DocType: Asset Category Account,Asset Category Account,資產類別的科目 apps/erpnext/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +1024,Row #{0} (Payment Table): Amount must be positive,行#{0}(付款表):金額必須為正值 apps/erpnext/erpnext/manufacturing/doctype/production_order/production_order.py +137,Cannot produce more Item {0} than Sales Order quantity {1},無法產生更多的項目{0}不是銷售訂單數量{1} apps/erpnext/erpnext/stock/doctype/item/item.js +446,Select Attribute Values,選擇屬性值 DocType: Purchase Invoice,Reason For Issuing document,簽發文件的原因 -apps/erpnext/erpnext/accounts/doctype/journal_entry/journal_entry.py +583,Stock Entry {0} is not submitted,股票輸入{0}不提交 -DocType: Payment Reconciliation,Bank / Cash Account,銀行/現金帳戶 +apps/erpnext/erpnext/accounts/doctype/journal_entry/journal_entry.py +583,Stock Entry {0} is not submitted,庫存輸入{0}不提交 +DocType: Payment Reconciliation,Bank / Cash Account,銀行/現金科目 apps/erpnext/erpnext/crm/doctype/lead/lead.py +47,Next Contact By cannot be same as the Lead Email Address,接著聯繫到不能相同鉛郵箱地址 DocType: Tax Rule,Billing City,結算城市 DocType: Asset,Manual,手冊 -DocType: Salary Component Account,Salary Component Account,薪金部分賬戶 +DocType: Salary Component Account,Salary Component Account,薪金部分科目 DocType: Global Defaults,Hide Currency Symbol,隱藏貨幣符號 apps/erpnext/erpnext/selling/page/sales_funnel/sales_funnel.js +281,Sales Opportunities by Source,來源的銷售機會 apps/erpnext/erpnext/config/accounts.py +287,"e.g. Bank, Cash, Credit Card",例如:銀行,現金,信用卡 @@ -3449,7 +3449,7 @@ DocType: Purchase Invoice Item,Received Qty,到貨數量 DocType: Stock Entry Detail,Serial No / Batch,序列號/批次 apps/erpnext/erpnext/selling/doctype/sales_order/sales_order.py +359,Not Paid and Not Delivered,沒有支付,未送達 DocType: Product Bundle,Parent Item,父項目 -DocType: Account,Account Type,帳戶類型 +DocType: Account,Account Type,科目類型 DocType: Shopify Settings,Webhooks Details,Webhooks詳細信息 apps/erpnext/erpnext/templates/pages/projects.html +58,No time sheets,沒有考勤表 DocType: GoCardless Mandate,GoCardless Customer,GoCardless客戶 @@ -3476,7 +3476,7 @@ apps/erpnext/erpnext/manufacturing/doctype/job_card/job_card.js +26,Start Job, apps/erpnext/erpnext/assets/doctype/asset_movement/asset_movement.py +32,Serial no is required for the asset {0},資產{0}需要序列號 apps/erpnext/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py +43,Disabled template must not be default template,殘疾人模板必須不能默認模板 apps/erpnext/erpnext/manufacturing/doctype/production_plan/production_plan.py +542,For row {0}: Enter planned qty,對於行{0}:輸入計劃的數量 -DocType: Account,Income Account,收入帳戶 +DocType: Account,Income Account,收入科目 DocType: Payment Request,Amount in customer's currency,量客戶的貨幣 apps/erpnext/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +880,Delivery,交貨 DocType: Stock Reconciliation Item,Current Qty,目前數量 @@ -3488,7 +3488,7 @@ DocType: Appraisal Goal,Key Responsibility Area,關鍵責任區 DocType: Delivery Trip,Distance UOM,距離UOM apps/erpnext/erpnext/utilities/activation.py +127,"Student Batches help you track attendance, assessments and fees for students",學生批幫助您跟踪學生的出勤,評估和費用 DocType: Payment Entry,Total Allocated Amount,總撥款額 -apps/erpnext/erpnext/setup/doctype/company/company.py +163,Set default inventory account for perpetual inventory,設置永久庫存的默認庫存帳戶 +apps/erpnext/erpnext/setup/doctype/company/company.py +163,Set default inventory account for perpetual inventory,設置永久庫存的默認庫存科目 apps/erpnext/erpnext/stock/doctype/serial_no/serial_no.py +261,"Cannot deliver Serial No {0} of item {1} as it is reserved to \ fullfill Sales Order {2}",無法提供項目{1}的序列號{0},因為它保留在\ fullfill銷售訂單{2}中 DocType: Item Reorder,Material Request Type,材料需求類型 @@ -3532,7 +3532,7 @@ DocType: Task,% Progress,%進展 apps/erpnext/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py +130,Gain/Loss on Asset Disposal,在資產處置收益/損失 apps/erpnext/erpnext/education/doctype/program_enrollment_tool/program_enrollment_tool.js +24,"Only the Student Applicant with the status ""Approved"" will be selected in the table below.",下表中將只選擇狀態為“已批准”的學生申請人。 DocType: Tax Withholding Category,Rates,價格 -apps/erpnext/erpnext/regional/report/fichier_des_ecritures_comptables_[fec]/fichier_des_ecritures_comptables_[fec].py +121,Account number for account {0} is not available.
Please setup your Chart of Accounts correctly.,帳戶{0}的帳戶號碼不可用。
請正確設置您的會計科目表。 +apps/erpnext/erpnext/regional/report/fichier_des_ecritures_comptables_[fec]/fichier_des_ecritures_comptables_[fec].py +121,Account number for account {0} is not available.
Please setup your Chart of Accounts correctly.,科目{0}的科目號碼不可用。
請正確設置您的會計科目表。 DocType: Task,Depends on Tasks,取決於任務 apps/erpnext/erpnext/config/selling.py +36,Manage Customer Group Tree.,管理客戶群組樹。 DocType: Normal Test Items,Result Value,結果值 @@ -3657,7 +3657,7 @@ Examples: DocType: Issue,Issue Type,發行類型 DocType: Attendance,Leave Type,休假類型 DocType: Purchase Invoice,Supplier Invoice Details,供應商發票明細 -apps/erpnext/erpnext/controllers/stock_controller.py +237,Expense / Difference account ({0}) must be a 'Profit or Loss' account,費用/差異帳戶({0})必須是一個'溢利或虧損的帳戶 +apps/erpnext/erpnext/controllers/stock_controller.py +237,Expense / Difference account ({0}) must be a 'Profit or Loss' account,費用/差異科目({0})必須是一個'收益或損失'的科目 DocType: Project,Copied From,複製自 DocType: Project,Copied From,複製自 apps/erpnext/erpnext/projects/doctype/timesheet/timesheet.py +265,Invoice already created for all billing hours,發票已在所有結算時間創建 @@ -3701,7 +3701,7 @@ DocType: Asset,In Maintenance,在維護中 DocType: Amazon MWS Settings,Click this button to pull your Sales Order data from Amazon MWS.,單擊此按鈕可從亞馬遜MWS中提取銷售訂單數據。 DocType: Purchase Invoice,Overdue,過期的 DocType: Account,Stock Received But Not Billed,庫存接收,但不付款 -apps/erpnext/erpnext/accounts/doctype/account/account.py +91,Root Account must be a group,根帳戶必須是一組 +apps/erpnext/erpnext/accounts/doctype/account/account.py +91,Root Account must be a group,Root 科目必須是群組科目 DocType: Drug Prescription,Drug Prescription,藥物處方 DocType: Loan,Repaid/Closed,償還/關閉 DocType: Item,Total Projected Qty,預計總數量 @@ -3737,7 +3737,7 @@ apps/erpnext/erpnext/stock/doctype/item/item.py +578,Item {0} does not exist,項 DocType: Sales Invoice,Customer Address,客戶地址 DocType: Loan,Loan Details,貸款詳情 apps/erpnext/erpnext/setup/setup_wizard/setup_wizard.py +62,Failed to setup post company fixtures,未能設置公司固定裝置 -DocType: Company,Default Inventory Account,默認庫存帳戶 +DocType: Company,Default Inventory Account,默認庫存科目 apps/erpnext/erpnext/accounts/doctype/share_transfer/share_transfer.py +193,The folio numbers are not matching,作品集編號不匹配 apps/erpnext/erpnext/accounts/doctype/payment_request/payment_request.py +304,Payment Request for {0},付款申請{0} DocType: Item Barcode,Barcode Type,條碼類型 @@ -3786,7 +3786,7 @@ DocType: Antibiotic,Healthcare Administrator,醫療管理員 apps/erpnext/erpnext/utilities/user_progress.py +47,Set a Target,設定目標 DocType: Dosage Strength,Dosage Strength,劑量強度 DocType: Healthcare Practitioner,Inpatient Visit Charge,住院訪問費用 -DocType: Account,Expense Account,費用帳戶 +DocType: Account,Expense Account,費用科目 apps/erpnext/erpnext/setup/setup_wizard/data/industry_type.py +49,Software,軟件 apps/erpnext/erpnext/setup/setup_wizard/operations/install_fixtures.py +155,Colour,顏色 DocType: Assessment Plan Criteria,Assessment Plan Criteria,評估計劃標準 @@ -3860,7 +3860,7 @@ DocType: Contract,Fulfilment Terms,履行條款 DocType: Sales Invoice,Time Sheet List,時間表列表 DocType: Employee,You can enter any date manually,您可以手動輸入任何日期 DocType: Healthcare Settings,Result Printed,結果打印 -DocType: Asset Category Account,Depreciation Expense Account,折舊費用帳戶 +DocType: Asset Category Account,Depreciation Expense Account,折舊費用科目 apps/erpnext/erpnext/setup/setup_wizard/operations/install_fixtures.py +191,Probationary Period,試用期 DocType: Purchase Taxes and Charges Template,Is Inter State,是國際 apps/erpnext/erpnext/config/hr.py +269,Shift Management,班次管理 @@ -3900,7 +3900,7 @@ apps/erpnext/erpnext/hr/utils.py +154,Future dates not allowed,未來的日期 apps/erpnext/erpnext/support/page/support_analytics/support_analytics.js +30,Select Fiscal Year,選擇財政年度 apps/erpnext/erpnext/selling/doctype/sales_order/sales_order.py +119,Expected Delivery Date should be after Sales Order Date,預計交貨日期應在銷售訂單日期之後 apps/erpnext/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py +43,Reorder Level,重新排序級別 -DocType: Company,Chart Of Accounts Template,圖表帳戶模板 +DocType: Company,Chart Of Accounts Template,會計科目模板 apps/erpnext/erpnext/assets/doctype/asset/asset.py +80,Update stock must be enable for the purchase invoice {0},必須為購買發票{0}啟用更新庫存 apps/erpnext/erpnext/stock/get_item_details.py +405,Item Price updated for {0} in Price List {1},項目價格更新{0}價格表{1} DocType: Salary Structure,Salary breakup based on Earning and Deduction.,工資分手基於盈利和演繹。 @@ -3957,7 +3957,7 @@ apps/erpnext/erpnext/accounts/doctype/cost_center/cost_center.py +40,Cost Center DocType: QuickBooks Migrator,Authorization URL,授權URL apps/erpnext/erpnext/accounts/doctype/payment_entry/payment_entry.py +376,Amount {0} {1} {2} {3},金額{0} {1} {2} {3} DocType: Account,Depreciation,折舊 -apps/erpnext/erpnext/accounts/doctype/share_transfer/share_transfer.py +103,The number of shares and the share numbers are inconsistent,股份數量和股票數量不一致 +apps/erpnext/erpnext/accounts/doctype/share_transfer/share_transfer.py +103,The number of shares and the share numbers are inconsistent,股份數量和庫存數量不一致 apps/erpnext/erpnext/stock/report/supplier_wise_sales_analytics/supplier_wise_sales_analytics.py +50,Supplier(s),供應商(S) DocType: Employee Attendance Tool,Employee Attendance Tool,員工考勤工具 DocType: Guardian Student,Guardian Student,學生監護人 @@ -3981,8 +3981,8 @@ apps/erpnext/erpnext/manufacturing/doctype/production_planning_tool/production_p DocType: Restaurant Reservation,No of People,沒有人 apps/erpnext/erpnext/config/selling.py +164,Template of terms or contract.,模板條款或合同。 DocType: Bank Account,Address and Contact,地址和聯絡方式 -DocType: Cheque Print Template,Is Account Payable,為應付賬款 -apps/erpnext/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +306,Stock cannot be updated against Purchase Receipt {0},股票不能對外購入庫單進行更新{0} +DocType: Cheque Print Template,Is Account Payable,為應付帳款 +apps/erpnext/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +306,Stock cannot be updated against Purchase Receipt {0},庫存不能對外購入庫單進行更新{0} DocType: Support Settings,Auto close Issue after 7 days,7天之後自動關閉問題 apps/erpnext/erpnext/hr/doctype/leave_allocation/leave_allocation.py +85,"Leave cannot be allocated before {0}, as leave balance has already been carry-forwarded in the future leave allocation record {1}",假,不是之前分配{0},因為休假餘額已經結轉轉發在未來的假期分配記錄{1} apps/erpnext/erpnext/accounts/party.py +350,Note: Due / Reference Date exceeds allowed customer credit days by {0} day(s),註:由於/參考日期由{0}天超過了允許客戶的信用天數(S) @@ -4035,7 +4035,7 @@ apps/erpnext/erpnext/accounts/report/trial_balance/trial_balance.py +271,Closing apps/erpnext/erpnext/stock/doctype/serial_no/serial_no.py +284,Serial No {0} not in stock,序列號{0}無貨 apps/erpnext/erpnext/config/selling.py +169,Tax template for selling transactions.,稅務模板賣出的交易。 DocType: Sales Invoice,Write Off Outstanding Amount,核銷額(億元) -apps/erpnext/erpnext/hr/doctype/expense_claim_type/expense_claim_type.py +27,Account {0} does not match with Company {1},帳戶{0}與公司{1}不符 +apps/erpnext/erpnext/hr/doctype/expense_claim_type/expense_claim_type.py +27,Account {0} does not match with Company {1},科目{0}與公司{1}不符 DocType: Education Settings,Current Academic Year,當前學年 DocType: Education Settings,Current Academic Year,當前學年 DocType: Stock Settings,Default Stock UOM,預設庫存計量單位 @@ -4055,13 +4055,13 @@ apps/erpnext/erpnext/crm/report/lead_conversion_time/lead_conversion_time.py +59 apps/erpnext/erpnext/controllers/accounts_controller.py +703,'Update Stock' cannot be checked for fixed asset sale,"""更新庫存"" 無法檢查固定資產銷售" DocType: Bank Reconciliation,Bank Reconciliation,銀行對帳 apps/erpnext/erpnext/templates/includes/footer/footer_extension.html +7,Get Updates,獲取更新 -apps/erpnext/erpnext/accounts/doctype/gl_entry/gl_entry.py +97,{0} {1}: Account {2} does not belong to Company {3},{0} {1}帳戶{2}不屬於公司{3} +apps/erpnext/erpnext/accounts/doctype/gl_entry/gl_entry.py +97,{0} {1}: Account {2} does not belong to Company {3},{0} {1}科目{2}不屬於公司{3} apps/erpnext/erpnext/stock/doctype/item/item.js +452,Select at least one value from each of the attributes.,從每個屬性中至少選擇一個值。 apps/erpnext/erpnext/buying/doctype/purchase_order/purchase_order.py +164,Material Request {0} is cancelled or stopped,材料需求{0}被取消或停止 apps/erpnext/erpnext/regional/report/eway_bill/eway_bill.py +219,Dispatch State,派遣國 apps/erpnext/erpnext/config/hr.py +399,Leave Management,離開管理 apps/erpnext/erpnext/stock/doctype/item/item_dashboard.py +17,Groups,組 -apps/erpnext/erpnext/accounts/report/general_ledger/general_ledger.py +51,Group by Account,以帳戶分群組 +apps/erpnext/erpnext/accounts/report/general_ledger/general_ledger.py +51,Group by Account,以科目分群組 DocType: Purchase Invoice,Hold Invoice,保留發票 apps/erpnext/erpnext/hr/doctype/employee_promotion/employee_promotion.js +37,Please select Employee,請選擇員工 apps/erpnext/erpnext/setup/setup_wizard/operations/install_fixtures.py +214,Lower Income,較低的收入 @@ -4069,7 +4069,7 @@ DocType: Restaurant Order Entry,Current Order,當前訂單 apps/erpnext/erpnext/assets/doctype/asset_movement/asset_movement.py +25,Number of serial nos and quantity must be the same,序列號和數量必須相同 apps/erpnext/erpnext/stock/doctype/stock_entry/stock_entry.py +275,Source and target warehouse cannot be same for row {0},列{0}的來源和目標倉庫不可相同 DocType: Account,Asset Received But Not Billed,已收到但未收費的資產 -apps/erpnext/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +244,"Difference Account must be a Asset/Liability type account, since this Stock Reconciliation is an Opening Entry",差異帳戶必須是資產/負債類型的帳戶,因為此庫存調整是一個開始分錄 +apps/erpnext/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +244,"Difference Account must be a Asset/Liability type account, since this Stock Reconciliation is an Opening Entry",差異科目必須是資產/負債類型的科目,因為此庫存調整是一個開帳分錄 apps/erpnext/erpnext/hr/doctype/loan/loan.py +116,Disbursed Amount cannot be greater than Loan Amount {0},支付額不能超過貸款金額較大的{0} apps/erpnext/erpnext/utilities/user_progress.py +176,Go to Programs,轉到程序 apps/erpnext/erpnext/hr/doctype/expense_claim/expense_claim.py +212,Row {0}# Allocated amount {1} cannot be greater than unclaimed amount {2},行{0}#分配的金額{1}不能大於無人認領的金額{2} @@ -4117,7 +4117,7 @@ apps/erpnext/erpnext/patches/v7_0/create_warehouse_nestedset.py +59,All Warehous apps/erpnext/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +1303,No {0} found for Inter Company Transactions.,Inter公司沒有找到{0}。 DocType: Travel Itinerary,Rented Car,租車 apps/erpnext/erpnext/public/js/hub/components/profile_dialog.js +15,About your Company,關於貴公司 -apps/erpnext/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +144,Credit To account must be a Balance Sheet account,信用帳戶必須是資產負債表科目 +apps/erpnext/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +144,Credit To account must be a Balance Sheet account,貸方科目必須是資產負債表科目 DocType: Donor,Donor,捐贈者 DocType: Global Defaults,Disable In Words,禁用詞 apps/erpnext/erpnext/stock/doctype/item/item.py +74,Item Code is mandatory because Item is not automatically numbered,產品編號是強制性的,因為項目沒有自動編號 @@ -4136,7 +4136,7 @@ apps/erpnext/erpnext/accounts/doctype/payment_entry/payment_entry.py +95,Row #{0 apps/erpnext/erpnext/manufacturing/doctype/bom/bom.js +84,Browse BOM,瀏覽BOM apps/erpnext/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py +165,Secured Loans,抵押貸款 DocType: Purchase Invoice,Edit Posting Date and Time,編輯投稿時間 -apps/erpnext/erpnext/assets/doctype/asset/depreciation.py +106,Please set Depreciation related Accounts in Asset Category {0} or Company {1},請設置在資產類別{0}或公司折舊相關帳戶{1} +apps/erpnext/erpnext/assets/doctype/asset/depreciation.py +106,Please set Depreciation related Accounts in Asset Category {0} or Company {1},請設置在資產類別{0}或公司折舊相關科目{1} DocType: Lab Test Groups,Normal Range,普通範圍 DocType: Academic Term,Academic Year,學年 apps/erpnext/erpnext/stock/report/item_variant_details/item_variant_details.py +79,Available Selling,可用銷售 @@ -4145,7 +4145,7 @@ apps/erpnext/erpnext/accounts/doctype/account/chart_of_accounts/verified/standar DocType: Purchase Invoice,N,ñ apps/erpnext/erpnext/education/report/assessment_plan_status/assessment_plan_status.py +175,Remaining,剩餘 DocType: Appraisal,Appraisal,評價 -DocType: Loan,Loan Account,貸款帳戶 +DocType: Loan,Loan Account,貸款科目 DocType: Purchase Invoice,GST Details,消費稅細節 apps/erpnext/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner_dashboard.py +6,This is based on transactions against this Healthcare Practitioner.,這是基於針對此醫療保健從業者的交易。 apps/erpnext/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py +156,Email sent to supplier {0},電子郵件發送到供應商{0} @@ -4168,9 +4168,9 @@ apps/erpnext/erpnext/buying/doctype/request_for_quotation/request_for_quotation. apps/erpnext/erpnext/manufacturing/doctype/bom/bom.py +187,{0} not found for Item {1},找不到項目{1} {0} apps/erpnext/erpnext/utilities/user_progress.py +197,Go to Courses,去課程 DocType: Accounts Settings,Show Inclusive Tax In Print,在打印中顯示包含稅 -apps/erpnext/erpnext/accounts/doctype/bank_reconciliation/bank_reconciliation.py +17,"Bank Account, From Date and To Date are Mandatory",銀行賬戶,從日期到日期是強制性的 +apps/erpnext/erpnext/accounts/doctype/bank_reconciliation/bank_reconciliation.py +17,"Bank Account, From Date and To Date are Mandatory",銀行科目,從日期到日期是強制性的 apps/erpnext/erpnext/accounts/doctype/payment_request/payment_request.js +29,Message Sent,發送訊息 -apps/erpnext/erpnext/accounts/doctype/account/account.py +105,Account with child nodes cannot be set as ledger,帳戶與子節點不能被設置為分類帳 +apps/erpnext/erpnext/accounts/doctype/account/account.py +105,Account with child nodes cannot be set as ledger,科目與子節點不能被設置為分類帳 DocType: Sales Invoice,Rate at which Price list currency is converted to customer's base currency,價目表貨幣被換算成客戶基礎貨幣的匯率 DocType: Purchase Invoice Item,Net Amount (Company Currency),淨金額(公司貨幣) apps/erpnext/erpnext/hr/doctype/expense_claim/expense_claim.py +223,Total advance amount cannot be greater than total sanctioned amount,總預付金額不得超過全部認可金額 @@ -4178,7 +4178,7 @@ DocType: Salary Slip,Hour Rate,小時率 DocType: Stock Settings,Item Naming By,產品命名規則 apps/erpnext/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py +46,Another Period Closing Entry {0} has been made after {1},另一個期末錄入{0}作出後{1} DocType: Work Order,Material Transferred for Manufacturing,物料轉倉用於製造 -apps/erpnext/erpnext/accounts/report/general_ledger/general_ledger.py +49,Account {0} does not exists,帳戶{0}不存在 +apps/erpnext/erpnext/accounts/report/general_ledger/general_ledger.py +49,Account {0} does not exists,科目{0}不存在 apps/erpnext/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +1608,Select Loyalty Program,選擇忠誠度計劃 DocType: Project,Project Type,專案類型 apps/erpnext/erpnext/projects/doctype/task/task.py +157,Child Task exists for this Task. You can not delete this Task.,子任務存在這個任務。你不能刪除這個任務。 @@ -4199,7 +4199,7 @@ apps/erpnext/erpnext/accounts/doctype/shipping_rule/shipping_rule.py +101,Shippi apps/erpnext/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py +20,Cash In Hand,手頭現金 apps/erpnext/erpnext/selling/doctype/sales_order/sales_order.py +143,Delivery warehouse required for stock item {0},需要的庫存項目交割倉庫{0} DocType: Packing Slip,The gross weight of the package. Usually net weight + packaging material weight. (for print),包裹的總重量。通常為淨重+包裝材料的重量。 (用於列印) -DocType: Accounts Settings,Users with this role are allowed to set frozen accounts and create / modify accounting entries against frozen accounts,具有此角色的用戶可以設置凍結帳戶,並新增/修改對凍結帳戶的會計分錄 +DocType: Accounts Settings,Users with this role are allowed to set frozen accounts and create / modify accounting entries against frozen accounts,具有此角色的用戶可以設置凍結科目,並新增/修改對凍結科目的會計分錄 DocType: Vital Signs,Cuts,削減 DocType: Serial No,Is Cancelled,被註銷 DocType: Student Group,Group Based On,基於組 @@ -4218,7 +4218,7 @@ apps/erpnext/erpnext/education/doctype/student_attendance_tool/student_attendanc ,Issued Items Against Work Order,針對工單發布物品 ,BOM Stock Calculated,BOM庫存計算 DocType: Vehicle Log,Invoice Ref,發票編號 -DocType: Company,Default Income Account,預設之收入帳戶 +DocType: Company,Default Income Account,預設收入科目 apps/erpnext/erpnext/accounts/report/balance_sheet/balance_sheet.py +39,Unclosed Fiscal Years Profit / Loss (Credit),未關閉的財年利潤/損失(信用) DocType: Sales Invoice,Time Sheets,考勤表 DocType: Healthcare Service Unit Type,Change In Item,更改項目 @@ -4258,7 +4258,7 @@ DocType: Soil Texture,Silt Composition (%),粉塵成分(%) DocType: Journal Entry,Remark,備註 DocType: Healthcare Settings,Avoid Confirmation,避免確認 DocType: Purchase Receipt Item,Rate and Amount,率及金額 -apps/erpnext/erpnext/accounts/doctype/payment_entry/payment_entry.py +177,Account Type for {0} must be {1},帳戶類型為{0}必須{1} +apps/erpnext/erpnext/accounts/doctype/payment_entry/payment_entry.py +177,Account Type for {0} must be {1},科目類型為{0}必須{1} apps/erpnext/erpnext/config/hr.py +34,Leaves and Holiday,休假及假日 DocType: Education Settings,Current Academic Term,當前學術期限 DocType: Education Settings,Current Academic Term,當前學術期限 @@ -4272,7 +4272,7 @@ DocType: Purchase Invoice Item,Landed Cost Voucher Amount,到岸成本憑證金 apps/erpnext/erpnext/config/accounts.py +19,Bills raised by Suppliers.,由供應商提出的帳單。 DocType: POS Profile,Write Off Account,核銷帳戶 DocType: Patient Appointment,Get prescribed procedures,獲取規定的程序 -DocType: Sales Invoice,Redemption Account,贖回賬戶 +DocType: Sales Invoice,Redemption Account,贖回科目 DocType: Purchase Invoice Item,Discount Amount,折扣金額 DocType: Purchase Invoice,Return Against Purchase Invoice,回到對採購發票 DocType: Item,Warranty Period (in days),保修期限(天數) @@ -4372,7 +4372,7 @@ apps/erpnext/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py +3 apps/erpnext/erpnext/hr/report/salary_register/salary_register.py +47,Salary Slip ID,工資單編號 apps/erpnext/erpnext/hr/doctype/employee/employee.py +135,Date Of Retirement must be greater than Date of Joining,日期退休必須大於加入的日期 apps/erpnext/erpnext/stock/doctype/item/item.js +70,Multiple Variants,多種變體 -DocType: Sales Invoice,Against Income Account,對收入帳戶 +DocType: Sales Invoice,Against Income Account,對收入科目 DocType: Subscription,Trial Period Start Date,試用期開始日期 apps/erpnext/erpnext/buying/doctype/purchase_order/purchase_order.py +106,Item {0}: Ordered qty {1} cannot be less than minimum order qty {2} (defined in Item).,項目{0}:有序數量{1}不能低於最低訂貨量{2}(項中定義)。 DocType: Certification Application,Certified,認證 @@ -4402,7 +4402,7 @@ DocType: Asset,Journal Entry for Scrap,日記帳分錄報廢 apps/erpnext/erpnext/selling/doctype/installation_note/installation_note.py +83,Please pull items from Delivery Note,請送貨單拉項目 apps/erpnext/erpnext/manufacturing/doctype/work_order/work_order.py +242,Row {0}: select the workstation against the operation {1},行{0}:根據操作{1}選擇工作站 apps/erpnext/erpnext/accounts/utils.py +496,Journal Entries {0} are un-linked,日記條目{0}都是非聯 -apps/erpnext/erpnext/accounts/utils.py +825,{0} Number {1} already used in account {2},已在{2}帳戶中使用的{0}號碼{1} +apps/erpnext/erpnext/accounts/utils.py +825,{0} Number {1} already used in account {2},已在{2}科目中使用的{0}號碼{1} apps/erpnext/erpnext/config/crm.py +92,"Record of all communications of type email, phone, chat, visit, etc.",類型電子郵件,電話,聊天,訪問等所有通信記錄 DocType: Supplier Scorecard Scoring Standing,Supplier Scorecard Scoring Standing,供應商記分卡 DocType: Manufacturer,Manufacturers used in Items,在項目中使用製造商 @@ -4428,7 +4428,7 @@ DocType: Salary Component,"If selected, the value specified or calculated in thi DocType: Asset Settings,Number of Days in Fiscal Year,會計年度的天數 ,Stock Ledger,庫存總帳 apps/erpnext/erpnext/templates/includes/cart/cart_items.html +29,Rate: {0},價格:{0} -DocType: Company,Exchange Gain / Loss Account,兌換收益/損失帳戶 +DocType: Company,Exchange Gain / Loss Account,匯兌損益科目 DocType: Amazon MWS Settings,MWS Credentials,MWS憑證 apps/erpnext/erpnext/config/hr.py +7,Employee and Attendance,員工考勤 apps/erpnext/erpnext/stock/doctype/stock_entry/stock_entry.py +123,Purpose must be one of {0},目的必須是一個{0} @@ -4450,7 +4450,7 @@ DocType: Cash Flow Mapper,Section Name,部分名稱 apps/erpnext/erpnext/stock/report/stock_balance/stock_balance.py +92,Reorder Qty,再訂購數量 apps/erpnext/erpnext/assets/doctype/asset/asset.py +292,Depreciation Row {0}: Expected value after useful life must be greater than or equal to {1},折舊行{0}:使用壽命後的預期值必須大於或等於{1} apps/erpnext/erpnext/hr/doctype/job_opening/job_opening.py +55,Current Job Openings,當前職位空缺 -DocType: Company,Stock Adjustment Account,庫存調整帳戶 +DocType: Company,Stock Adjustment Account,庫存調整科目 apps/erpnext/erpnext/public/js/payment/pos_payment.html +17,Write Off,註銷項款 DocType: Healthcare Service Unit,Allow Overlap,允許重疊 DocType: Employee,"System User (login) ID. If set, it will become default for all HR forms.",系統用戶(登錄)的標識。如果設定,這將成為的所有人力資源的預設形式。 @@ -4497,7 +4497,7 @@ DocType: Purchase Order,Order Confirmation Date,訂單確認日期 apps/erpnext/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.js +47,Make Maintenance Visit,使維護訪問 DocType: Employee Transfer,Employee Transfer Details,員工轉移詳情 apps/erpnext/erpnext/selling/doctype/customer/customer.py +263,Please contact to the user who have Sales Master Manager {0} role,請聯絡,誰擁有碩士學位的銷售經理{0}角色的用戶 -DocType: Company,Default Cash Account,預設的現金帳戶 +DocType: Company,Default Cash Account,預設的現金科目 apps/erpnext/erpnext/config/accounts.py +40,Company (not Customer or Supplier) master.,公司(不是客戶或供應商)的主人。 apps/erpnext/erpnext/education/doctype/student/student_dashboard.py +6,This is based on the attendance of this Student,這是基於這名學生出席 apps/erpnext/erpnext/education/doctype/student_attendance_tool/student_attendance_tool.js +179,No Students in,沒有學生 @@ -4515,12 +4515,12 @@ DocType: Opportunity,Opportunity Type,機會型 DocType: Asset Movement,To Employee,給員工 DocType: Employee Transfer,New Company,新公司 apps/erpnext/erpnext/setup/doctype/company/delete_company_transactions.py +19,Transactions can only be deleted by the creator of the Company,交易只能由公司的創建者被刪除 -apps/erpnext/erpnext/accounts/general_ledger.py +21,Incorrect number of General Ledger Entries found. You might have selected a wrong Account in the transaction.,不正確的數字總帳條目中找到。你可能會在交易中選擇了錯誤的帳戶。 +apps/erpnext/erpnext/accounts/general_ledger.py +21,Incorrect number of General Ledger Entries found. You might have selected a wrong Account in the transaction.,不正確的數字總帳條目中找到。你可能會在交易中選擇了錯誤的科目。 DocType: Employee,Prefered Contact Email,首選聯繫郵箱 DocType: Cheque Print Template,Cheque Width,支票寬度 DocType: Selling Settings,Validate Selling Price for Item against Purchase Rate or Valuation Rate,驗證售價反對預訂價或估價RATE項目 DocType: Fee Schedule,Fee Schedule,收費表 -DocType: Company,Create Chart Of Accounts Based On,創建圖表的帳戶根據 +DocType: Company,Create Chart Of Accounts Based On,基於會計科目表創建 apps/erpnext/erpnext/hr/doctype/employee/employee.py +129,Date of Birth cannot be greater than today.,出生日期不能大於今天。 ,Stock Ageing,存貨帳齡分析表 DocType: Travel Request,"Partially Sponsored, Require Partial Funding",部分贊助,需要部分資金 @@ -4541,7 +4541,7 @@ DocType: Purchase Order,Customer Contact Email,客戶聯絡電子郵件 DocType: Warranty Claim,Item and Warranty Details,項目和保修細節 DocType: Chapter,Chapter Members,章節成員 DocType: Sales Team,Contribution (%),貢獻(%) -apps/erpnext/erpnext/controllers/accounts_controller.py +143,Note: Payment Entry will not be created since 'Cash or Bank Account' was not specified,注:付款項將不會被創建因為“現金或銀行帳戶”未指定 +apps/erpnext/erpnext/controllers/accounts_controller.py +143,Note: Payment Entry will not be created since 'Cash or Bank Account' was not specified,注:付款項將不會被創建因為“現金或銀行科目”未指定 apps/erpnext/erpnext/projects/doctype/project/project.py +79,Project {0} already exists,項目{0}已經存在 DocType: Clinical Procedure,Nursing User,護理用戶 DocType: Employee Benefit Application,Payroll Period,工資期 @@ -4549,7 +4549,7 @@ DocType: Plant Analysis,Plant Analysis Criterias,植物分析標準 apps/erpnext/erpnext/stock/doctype/serial_no/serial_no.py +239,Serial No {0} does not belong to Batch {1},序列號{0}不屬於批次{1} apps/erpnext/erpnext/setup/setup_wizard/operations/install_fixtures.py +197,Responsibilities,職責 apps/erpnext/erpnext/selling/doctype/quotation/quotation.py +125,Validity period of this quotation has ended.,此報價的有效期已經結束。 -DocType: Expense Claim Account,Expense Claim Account,報銷賬戶 +DocType: Expense Claim Account,Expense Claim Account,報銷科目 DocType: Account,Capital Work in Progress,資本工作正在進行中 DocType: Accounts Settings,Allow Stale Exchange Rates,允許陳舊的匯率 DocType: Sales Person,Sales Person Name,銷售人員的姓名 @@ -4565,7 +4565,7 @@ apps/erpnext/erpnext/hr/doctype/leave_application/leave_application_dashboard.ht apps/erpnext/erpnext/projects/doctype/task/task.py +57,Progress % for a task cannot be more than 100.,為任務進度百分比不能超過100個。 DocType: Stock Reconciliation Item,Before reconciliation,調整前 DocType: Purchase Invoice,Taxes and Charges Added (Company Currency),稅收和收費上架(公司貨幣) -apps/erpnext/erpnext/stock/doctype/item/item.py +507,Item Tax Row {0} must have account of type Tax or Income or Expense or Chargeable,商品稅行{0}必須有帳戶類型稅或收入或支出或課稅的 +apps/erpnext/erpnext/stock/doctype/item/item.py +507,Item Tax Row {0} must have account of type Tax or Income or Expense or Chargeable,商品稅行{0}必須有科目類型為"稅" 或 "收入" 或 "支出" 或 "課稅的" DocType: Sales Order,Partly Billed,天色帳單 apps/erpnext/erpnext/assets/doctype/asset/asset.py +54,Item {0} must be a Fixed Asset Item,項{0}必須是固定資產項目 apps/erpnext/erpnext/stock/doctype/item/item.js +427,Make Variants,變種 @@ -4577,13 +4577,13 @@ apps/erpnext/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_re apps/erpnext/erpnext/setup/doctype/company/company.js +109,Please re-type company name to confirm,請確認重新輸入公司名稱 apps/erpnext/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py +50,Total Outstanding Amt,總街貨量金額 DocType: Journal Entry,Printing Settings,列印設定 -DocType: Employee Advance,Advance Account,預付帳戶 +DocType: Employee Advance,Advance Account,預付款科目 DocType: Job Offer,Job Offer Terms,招聘條款 DocType: Sales Invoice,Include Payment (POS),包括支付(POS) apps/erpnext/erpnext/accounts/doctype/journal_entry/journal_entry.py +324,Total Debit must be equal to Total Credit. The difference is {0},借方總額必須等於貸方總額。差額為{0} apps/erpnext/erpnext/setup/setup_wizard/data/industry_type.py +11,Automotive,汽車 DocType: Vehicle,Insurance Company,保險公司 -DocType: Asset Category Account,Fixed Asset Account,固定資產帳戶 +DocType: Asset Category Account,Fixed Asset Account,固定資產科目 apps/erpnext/erpnext/hr/doctype/salary_structure/salary_structure.js +442,Variable,變量 apps/erpnext/erpnext/selling/doctype/installation_note/installation_note.js +47,From Delivery Note,從送貨單 DocType: Chapter,Members,會員 @@ -4595,14 +4595,14 @@ apps/erpnext/erpnext/public/js/pos/pos_bill_item.html +12,In Stock: ,有現貨 DocType: Notification Control,Custom Message,自定義訊息 apps/erpnext/erpnext/setup/setup_wizard/data/industry_type.py +33,Investment Banking,投資銀行業務 DocType: Purchase Invoice,input,輸入 -apps/erpnext/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +107,Cash or Bank Account is mandatory for making payment entry,製作付款分錄時,現金或銀行帳戶是強制性輸入的欄位。 +apps/erpnext/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +107,Cash or Bank Account is mandatory for making payment entry,製作付款分錄時,現金或銀行科目是強制性輸入的欄位。 DocType: Loyalty Program,Multiple Tier Program,多層計劃 apps/erpnext/erpnext/education/report/student_and_guardian_contact_details/student_and_guardian_contact_details.py +54,Student Address,學生地址 apps/erpnext/erpnext/education/report/student_and_guardian_contact_details/student_and_guardian_contact_details.py +54,Student Address,學生地址 DocType: Purchase Invoice,Price List Exchange Rate,價目表匯率 apps/erpnext/erpnext/patches/v11_0/rename_supplier_type_to_supplier_group.py +27,All Supplier Groups,所有供應商組織 DocType: Employee Boarding Activity,Required for Employee Creation,員工創建需要 -apps/erpnext/erpnext/accounts/doctype/account/account.py +214,Account Number {0} already used in account {1},已在帳戶{1}中使用的帳號{0} +apps/erpnext/erpnext/accounts/doctype/account/account.py +214,Account Number {0} already used in account {1},已在科目{1}中使用的帳號{0} DocType: Hotel Room Reservation,Booked,預訂 DocType: Detected Disease,Tasks Created,創建的任務 DocType: Purchase Invoice Item,Rate,單價 @@ -4840,7 +4840,7 @@ apps/erpnext/erpnext/setup/doctype/supplier_group/supplier_group.js +5,There is apps/erpnext/erpnext/selling/page/point_of_sale/point_of_sale.js +542,Form View,表單視圖 DocType: HR Settings,Expense Approver Mandatory In Expense Claim,費用審批人必須在費用索賠中 apps/erpnext/erpnext/setup/doctype/email_digest/email_digest.py +124,Summary for this month and pending activities,本月和待活動總結 -apps/erpnext/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py +91,Please set Unrealized Exchange Gain/Loss Account in Company {0},請在公司{0}中設置未實現的匯兌收益/損失帳戶 +apps/erpnext/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py +91,Please set Unrealized Exchange Gain/Loss Account in Company {0},請在公司{0}中設置未實現的匯兌收益/損失科目 apps/erpnext/erpnext/utilities/user_progress.py +248,"Add users to your organization, other than yourself.",將用戶添加到您的組織,而不是您自己。 DocType: Customer Group,Customer Group Name,客戶群組名稱 apps/erpnext/erpnext/public/js/pos/pos.html +109,No Customers yet!,還沒有客戶! @@ -4856,7 +4856,7 @@ DocType: Healthcare Practitioner,Phone (R),電話(R) apps/erpnext/erpnext/healthcare/doctype/practitioner_schedule/practitioner_schedule.js +104,Time slots added,添加時隙 DocType: Item,Attributes,屬性 apps/erpnext/erpnext/healthcare/doctype/clinical_procedure_template/clinical_procedure_template.js +36,Enable Template,啟用模板 -apps/erpnext/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +256,Please enter Write Off Account,請輸入核銷帳戶 +apps/erpnext/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +256,Please enter Write Off Account,請輸入核銷科目 apps/erpnext/erpnext/selling/report/inactive_customers/inactive_customers.py +71,Last Order Date,最後訂購日期 DocType: Salary Component,Is Payable,應付 DocType: Inpatient Record,B Negative,B負面 @@ -4865,7 +4865,7 @@ DocType: Amazon MWS Settings,US,我們 DocType: Holiday List,Add Weekly Holidays,添加每週假期 DocType: Staffing Plan Detail,Vacancies,職位空缺 DocType: Hotel Room,Hotel Room,旅館房間 -apps/erpnext/erpnext/accounts/doctype/budget/budget.py +57,Account {0} does not belongs to company {1},帳戶{0}不屬於公司{1} +apps/erpnext/erpnext/accounts/doctype/budget/budget.py +57,Account {0} does not belongs to company {1},科目{0}不屬於公司{1} DocType: Leave Type,Rounding,四捨五入 apps/erpnext/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +994,Serial Numbers in row {0} does not match with Delivery Note,行{0}中的序列號與交貨單不匹配 DocType: Employee Benefit Application,Dispensed Amount (Pro-rated),分配金額(按比例分配) @@ -4896,7 +4896,7 @@ DocType: Patient,Alcohol Current Use,酒精當前使用 DocType: Employee Tax Exemption Proof Submission,House Rent Payment Amount,房屋租金付款金額 DocType: Student Admission Program,Student Admission Program,學生入學計劃 DocType: Employee Tax Exemption Sub Category,Tax Exemption Category,免稅類別 -DocType: Payment Entry,Account Paid To,賬戶付至 +DocType: Payment Entry,Account Paid To,科目付至 DocType: Subscription Settings,Grace Period,寬限期 DocType: Item Alternative,Alternative Item Name,替代項目名稱 apps/erpnext/erpnext/selling/doctype/product_bundle/product_bundle.py +24,Parent Item {0} must not be a Stock Item,父項{0}不能是庫存產品 @@ -4905,7 +4905,7 @@ apps/erpnext/erpnext/config/selling.py +57,All Products or Services.,所有的 DocType: Email Digest,Open Quotations,打開報價單 DocType: Expense Claim,More Details,更多詳情 DocType: Supplier Quotation,Supplier Address,供應商地址 -apps/erpnext/erpnext/accounts/doctype/budget/budget.py +168,{0} Budget for Account {1} against {2} {3} is {4}. It will exceed by {5},{0}預算帳戶{1}對{2} {3}是{4}。這將超過{5} +apps/erpnext/erpnext/accounts/doctype/budget/budget.py +168,{0} Budget for Account {1} against {2} {3} is {4}. It will exceed by {5},{0}預算科目{1}對{2} {3}是{4}。這將超過{5} apps/erpnext/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py +37,Out Qty,輸出數量 apps/erpnext/erpnext/buying/doctype/supplier/supplier.py +49,Series is mandatory,系列是強制性的 apps/erpnext/erpnext/setup/setup_wizard/data/industry_type.py +28,Financial Services,金融服務 @@ -4991,7 +4991,7 @@ DocType: Job Offer,Awaiting Response,正在等待回應 DocType: Support Search Source,Link Options,鏈接選項 apps/erpnext/erpnext/selling/page/point_of_sale/point_of_sale.js +1557,Total Amount {0},總金額{0} apps/erpnext/erpnext/controllers/item_variant.py +323,Invalid attribute {0} {1},無效的屬性{0} {1} -DocType: Supplier,Mention if non-standard payable account,如果非標準應付賬款提到 +DocType: Supplier,Mention if non-standard payable account,如果非標準應付帳款提到 apps/erpnext/erpnext/education/report/course_wise_assessment_report/course_wise_assessment_report.py +25,Please select the assessment group other than 'All Assessment Groups',請選擇“所有評估組”以外的評估組 apps/erpnext/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py +67,Row {0}: Cost center is required for an item {1},行{0}:項目{1}需要費用中心 DocType: Training Event Employee,Optional,可選的 @@ -5047,7 +5047,7 @@ apps/erpnext/erpnext/selling/report/inactive_customers/inactive_customers.py +67 DocType: Item Group,HTML / Banner that will show on the top of product list.,HTML/橫幅,將顯示在產品列表的頂部。 DocType: Shipping Rule,Specify conditions to calculate shipping amount,指定條件來計算運費金額 DocType: Program Enrollment,Institute's Bus,學院的巴士 -DocType: Accounts Settings,Role Allowed to Set Frozen Accounts & Edit Frozen Entries,允許設定凍結帳戶和編輯凍結分錄的角色 +DocType: Accounts Settings,Role Allowed to Set Frozen Accounts & Edit Frozen Entries,允許設定凍結科目和編輯凍結分錄的角色 DocType: Supplier Scorecard Scoring Variable,Path,路徑 apps/erpnext/erpnext/accounts/doctype/cost_center/cost_center.py +30,Cannot convert Cost Center to ledger as it has child nodes,不能成本中心轉換為總賬,因為它有子節點 DocType: Production Plan,Total Planned Qty,總計劃數量 @@ -5055,7 +5055,7 @@ apps/erpnext/erpnext/stock/report/stock_balance/stock_balance.py +83,Opening Val DocType: Salary Component,Formula,式 apps/erpnext/erpnext/stock/report/stock_ledger/stock_ledger.py +59,Serial #,序列號 DocType: Lab Test Template,Lab Test Template,實驗室測試模板 -apps/erpnext/erpnext/setup/doctype/company/company.py +196,Sales Account,銷售帳戶 +apps/erpnext/erpnext/setup/doctype/company/company.py +196,Sales Account,銷售科目 DocType: Purchase Invoice Item,Total Weight,總重量 apps/erpnext/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py +101,Commission on Sales,銷售佣金 DocType: Job Offer Term,Value / Description,值/說明 @@ -5078,7 +5078,7 @@ DocType: Cash Flow Mapping,Select Maximum Of 1,選擇最多1個 apps/erpnext/erpnext/stock/doctype/packing_slip/packing_slip.js +84,Invalid quantity specified for item {0}. Quantity should be greater than 0.,為項目指定了無效的數量{0} 。量應大於0 。 DocType: Company,Default Employee Advance Account,默認員工高級帳戶 apps/erpnext/erpnext/selling/page/point_of_sale/point_of_sale.js +1181,Search Item (Ctrl + i),搜索項目(Ctrl + i) -apps/erpnext/erpnext/accounts/doctype/account/account.py +171,Account with existing transaction can not be deleted,帳戶與現有的交易不能被刪除 +apps/erpnext/erpnext/accounts/doctype/account/account.py +171,Account with existing transaction can not be deleted,科目與現有的交易不能被刪除 DocType: Vehicle,Last Carbon Check,最後檢查炭 apps/erpnext/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py +109,Legal Expenses,法律費用 apps/erpnext/erpnext/public/js/utils/serial_no_batch_selector.js +147,Please select quantity on row ,請選擇行數量 @@ -5100,7 +5100,7 @@ apps/erpnext/erpnext/controllers/accounts_controller.py +907,Account: {0} with c DocType: Bank Statement Transaction Settings Item,Bank Data,銀行數據 DocType: Purchase Receipt Item,Sample Quantity,樣品數量 DocType: Manufacturing Settings,"Update BOM cost automatically via Scheduler, based on latest valuation rate / price list rate / last purchase rate of raw materials.",根據最新的估值/價格清單率/最近的原材料採購率,通過計劃程序自動更新BOM成本。 -apps/erpnext/erpnext/accounts/doctype/account/account.py +57,Account {0}: Parent account {1} does not belong to company: {2},帳戶{0}:父帳戶{1}不屬於公司:{2} +apps/erpnext/erpnext/accounts/doctype/account/account.py +57,Account {0}: Parent account {1} does not belong to company: {2},科目{0}:上層科目{1}不屬於公司:{2} apps/erpnext/erpnext/setup/doctype/company/company.js +126,Successfully deleted all transactions related to this company!,成功刪除與該公司相關的所有交易! apps/erpnext/erpnext/accounts/report/accounts_payable/accounts_payable.js +22,As on Date,隨著對日 DocType: Program Enrollment,Enrollment Date,報名日期 @@ -5126,7 +5126,7 @@ DocType: Academic Year,Academic Year Name,學年名稱 apps/erpnext/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +1182,{0} not allowed to transact with {1}. Please change the Company.,不允許{0}與{1}進行交易。請更改公司。 DocType: Sales Partner,Contact Desc,聯絡倒序 DocType: Email Digest,Send regular summary reports via Email.,使用電子郵件發送定期匯總報告。 -apps/erpnext/erpnext/hr/doctype/expense_claim/expense_claim.py +289,Please set default account in Expense Claim Type {0},請報銷類型設置默認帳戶{0} +apps/erpnext/erpnext/hr/doctype/expense_claim/expense_claim.py +289,Please set default account in Expense Claim Type {0},請報銷類型設置默認科目{0} apps/erpnext/erpnext/hr/doctype/leave_application/leave_application_dashboard.html +11,Available Leaves,可用的葉子 DocType: Assessment Result,Student Name,學生姓名 DocType: Hub Tracked Item,Item Manager,項目經理 @@ -5169,17 +5169,17 @@ apps/erpnext/erpnext/accounts/doctype/budget/budget.py +155,Accumulated Monthly, apps/erpnext/erpnext/controllers/accounts_controller.py +864,{0} is mandatory. Maybe Currency Exchange record is not created for {1} to {2}.,{0}是強制性的。也許外幣兌換記錄為{1}到{2}尚未建立。 apps/erpnext/erpnext/hr/doctype/staffing_plan/staffing_plan.py +51,Staffing Plan {0} already exist for designation {1},已存在人員配置計劃{0}以用於指定{1} apps/erpnext/erpnext/accounts/doctype/tax_rule/tax_rule.py +46,Tax Template is mandatory.,稅務模板是強制性的。 -apps/erpnext/erpnext/accounts/doctype/account/account.py +51,Account {0}: Parent account {1} does not exist,帳戶{0}:父帳戶{1}不存在 +apps/erpnext/erpnext/accounts/doctype/account/account.py +51,Account {0}: Parent account {1} does not exist,科目{0}:上層科目{1}不存在 DocType: POS Closing Voucher,Period Start Date,期間開始日期 DocType: Purchase Invoice Item,Price List Rate (Company Currency),價格列表費率(公司貨幣) DocType: Products Settings,Products Settings,產品設置 -,Item Price Stock,項目價格股票 +,Item Price Stock,項目價格庫存 apps/erpnext/erpnext/config/accounts.py +550,To make Customer based incentive schemes.,制定基於客戶的激勵計劃。 DocType: Lab Prescription,Test Created,測試創建 DocType: Healthcare Settings,Custom Signature in Print,自定義簽名打印 DocType: Account,Temporary,臨時 apps/erpnext/erpnext/accounts/report/accounts_receivable/accounts_receivable.html +127,Customer LPO No.,客戶LPO號 -DocType: Amazon MWS Settings,Market Place Account Group,市場賬戶組 +DocType: Amazon MWS Settings,Market Place Account Group,市場科目組 apps/erpnext/erpnext/accounts/doctype/payment_order/payment_order.js +14,Make Payment Entries,付款條目 DocType: Program,Courses,培訓班 DocType: Monthly Distribution Percentage,Percentage Allocation,百分比分配 @@ -5215,7 +5215,7 @@ DocType: Selling Settings,Each Transaction,每筆交易 apps/erpnext/erpnext/stock/doctype/item/item.py +523,Barcode {0} already used in Item {1},條碼{0}已經用在項目{1} apps/erpnext/erpnext/config/selling.py +86,Rules for adding shipping costs.,增加運輸成本的規則。 apps/erpnext/erpnext/accounts/report/budget_variance_report/budget_variance_report.py +72,Varaiance ,Varaiance -DocType: Item,Opening Stock,打開股票 +DocType: Item,Opening Stock,打開庫存 apps/erpnext/erpnext/support/doctype/warranty_claim/warranty_claim.py +20,Customer is required,客戶是必需的 DocType: Lab Test,Result Date,結果日期 apps/erpnext/erpnext/accounts/report/accounts_receivable/accounts_receivable.html +129,PDC/LC Date,PDC / LC日期 @@ -5250,7 +5250,7 @@ apps/erpnext/erpnext/templates/includes/product_list.js +42,No products found., apps/erpnext/erpnext/accounts/doctype/journal_entry/journal_entry.py +396,{0} against Sales Invoice {1},{0}針對銷售發票{1} DocType: Antibiotic,Laboratory User,實驗室用戶 DocType: Request for Quotation Item,Project Name,專案名稱 -DocType: Customer,Mention if non-standard receivable account,提到如果不規範應收賬款 +DocType: Customer,Mention if non-standard receivable account,提到如果不規範應收帳款 apps/erpnext/erpnext/hr/doctype/employee_benefit_application/employee_benefit_application.py +63,Please add the remaining benefits {0} to any of the existing component,請將其餘好處{0}添加到任何現有組件 DocType: Journal Entry Account,If Income or Expense,如果收入或支出 DocType: Bank Statement Transaction Entry,Matching Invoices,匹配發票 @@ -5260,7 +5260,7 @@ apps/erpnext/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py +6 apps/erpnext/erpnext/config/learn.py +229,Human Resource,人力資源 DocType: Payment Reconciliation Payment,Payment Reconciliation Payment,付款方式付款對賬 DocType: Disease,Treatment Task,治療任務 -DocType: Payment Order Reference,Bank Account Details,銀行賬戶明細 +DocType: Payment Order Reference,Bank Account Details,銀行科目明細 DocType: Purchase Order Item,Blanket Order,總訂單 apps/erpnext/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py +39,Tax Assets,所得稅資產 apps/erpnext/erpnext/manufacturing/doctype/production_order/production_order.py +631,Production Order has been {0},生產訂單已經{0} @@ -5322,7 +5322,7 @@ apps/erpnext/erpnext/manufacturing/doctype/work_order/work_order_calendar.js +36 ,Employee Information,僱員資料 apps/erpnext/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py +242,Healthcare Practitioner not available on {0},醫療從業者在{0}上不可用 DocType: Stock Entry Detail,Additional Cost,額外費用 -apps/erpnext/erpnext/accounts/report/general_ledger/general_ledger.py +57,"Can not filter based on Voucher No, if grouped by Voucher",是冷凍的帳戶。要禁止該帳戶創建/編輯事務,你需要角色 +apps/erpnext/erpnext/accounts/report/general_ledger/general_ledger.py +57,"Can not filter based on Voucher No, if grouped by Voucher",是凍結的帳戶。要禁止該帳戶創建/編輯事務,你需要有指定的身份 apps/erpnext/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js +975,Make Supplier Quotation,讓供應商報價 DocType: Quality Inspection,Incoming,來 apps/erpnext/erpnext/setup/doctype/company/company.js +90,Default tax templates for sales and purchase are created.,銷售和採購的默認稅收模板被創建。 @@ -5340,7 +5340,7 @@ apps/erpnext/erpnext/setup/doctype/email_digest/email_digest.py +120,This Week's apps/erpnext/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py +24,In Stock Qty,庫存數量 ,Daily Work Summary Replies,日常工作總結回复 DocType: Delivery Trip,Calculate Estimated Arrival Times,計算預計到達時間 -apps/erpnext/erpnext/accounts/general_ledger.py +113,Account: {0} can only be updated via Stock Transactions,帳號:{0}只能通過股票的交易進行更新 +apps/erpnext/erpnext/accounts/general_ledger.py +113,Account: {0} can only be updated via Stock Transactions,帳號:{0}只能通過庫存的交易進行更新 DocType: Student Group Creation Tool,Get Courses,獲取課程 DocType: Shopify Settings,Webhooks,網絡掛接 DocType: Bank Account,Party,黨 @@ -5376,7 +5376,7 @@ apps/erpnext/erpnext/selling/doctype/sales_order/sales_order.py +87,Same item ha DocType: Department,Leave Block List,休假區塊清單 DocType: Purchase Invoice,Tax ID,稅號 apps/erpnext/erpnext/stock/doctype/serial_no/serial_no.py +198,Item {0} is not setup for Serial Nos. Column must be blank,項目{0}不是設定為序列號,此列必須為空白 -DocType: Accounts Settings,Accounts Settings,帳戶設定 +DocType: Accounts Settings,Accounts Settings,會計設定 DocType: Loyalty Program,Customer Territory,客戶地區 DocType: Email Digest,Sales Orders to Deliver,要交付的銷售訂單 apps/erpnext/erpnext/accounts/doctype/account/account_tree.js +28,"Number of new Account, it will be included in the account name as a prefix",新帳號的數量,將作為前綴包含在帳號名稱中 @@ -5388,7 +5388,7 @@ apps/erpnext/erpnext/hr/utils.py +152,To date can not be less than from date,迄 DocType: Opportunity,To Discuss,為了討論 apps/erpnext/erpnext/stock/stock_ledger.py +382,{0} units of {1} needed in {2} to complete this transaction.,{0}單位{1}在{2}完成此交易所需。 DocType: Support Settings,Forum URL,論壇URL -apps/erpnext/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py +75,Temporary Accounts,臨時帳戶 +apps/erpnext/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py +75,Temporary Accounts,臨時科目 apps/erpnext/erpnext/assets/doctype/asset_movement/asset_movement.py +40,Source Location is required for the asset {0},源位置對資產{0}是必需的 DocType: BOM Explosion Item,BOM Explosion Item,BOM展開項目 DocType: Shareholder,Contact List,聯繫人列表 @@ -5426,7 +5426,7 @@ apps/erpnext/erpnext/education/doctype/course/course.py +20,Total Weightage of a DocType: Purchase Order Item,Last Purchase Rate,最後預訂價 DocType: Account,Asset,財富 DocType: Project Task,Task ID,任務ID -apps/erpnext/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py +84,Stock cannot exist for Item {0} since has variants,股票可以為項目不存在{0},因為有變種 +apps/erpnext/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py +84,Stock cannot exist for Item {0} since has variants,庫存可以為項目不存在{0},因為有變種 DocType: Healthcare Practitioner,Mobile,移動 ,Sales Person-wise Transaction Summary,銷售人員相關的交易匯總 DocType: Training Event,Contact Number,聯繫電話 @@ -5444,7 +5444,7 @@ DocType: Employee,Reports to,隸屬於 DocType: Payment Entry,Paid Amount,支付的金額 apps/erpnext/erpnext/utilities/user_progress.py +158,Explore Sales Cycle,探索銷售週期 DocType: Assessment Plan,Supervisor,監 -apps/erpnext/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js +934,Retention Stock Entry,保留股票入場 +apps/erpnext/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js +934,Retention Stock Entry,保留庫存入場 ,Available Stock for Packing Items,可用庫存包裝項目 DocType: Item Variant,Item Variant,項目變 ,Work Order Stock Report,工單庫存報表 @@ -5454,7 +5454,7 @@ apps/erpnext/erpnext/education/doctype/instructor/instructor.js +45,As Superviso DocType: Leave Policy Detail,Leave Policy Detail,退出政策細節 DocType: BOM Scrap Item,BOM Scrap Item,BOM項目廢料 apps/erpnext/erpnext/accounts/page/pos/pos.js +906,Submitted orders can not be deleted,提交的訂單不能被刪除 -apps/erpnext/erpnext/accounts/doctype/account/account.py +121,"Account balance already in Debit, you are not allowed to set 'Balance Must Be' as 'Credit'",帳戶餘額已歸為借方帳戶,不允許設為信用帳戶 +apps/erpnext/erpnext/accounts/doctype/account/account.py +121,"Account balance already in Debit, you are not allowed to set 'Balance Must Be' as 'Credit'",科目餘額已歸為借方科目,不允許設為貸方 apps/erpnext/erpnext/setup/setup_wizard/operations/install_fixtures.py +391,Quality Management,品質管理 apps/erpnext/erpnext/assets/doctype/asset/asset.py +52,Item {0} has been disabled,項{0}已被禁用 DocType: Project,Total Billable Amount (via Timesheets),總計費用金額(通過時間表) @@ -5484,15 +5484,15 @@ apps/erpnext/erpnext/manufacturing/doctype/workstation/workstation.py +36,Row #{ DocType: Purchase Invoice Item,Allow Zero Valuation Rate,允許零估值 DocType: Purchase Invoice Item,Allow Zero Valuation Rate,允許零估值 DocType: Training Event Employee,Invited,邀請 -apps/erpnext/erpnext/config/accounts.py +276,Setup Gateway accounts.,設置網關帳戶。 +apps/erpnext/erpnext/config/accounts.py +276,Setup Gateway accounts.,設置閘道科目。 DocType: Employee,Employment Type,就業類型 apps/erpnext/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py +43,Fixed Assets,固定資產 DocType: Payment Entry,Set Exchange Gain / Loss,設置兌換收益/損失 ,GST Purchase Register,消費稅購買登記冊 ,Cash Flow,現金周轉 apps/erpnext/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.py +25,Combined invoice portion must equal 100%,合併發票部分必須等於100% -DocType: Item Default,Default Expense Account,預設費用帳戶 -DocType: GST Account,CGST Account,CGST賬戶 +DocType: Item Default,Default Expense Account,預設費用科目 +DocType: GST Account,CGST Account,CGST科目 apps/erpnext/erpnext/education/report/student_and_guardian_contact_details/student_and_guardian_contact_details.py +53,Student Email ID,學生的電子郵件ID DocType: POS Closing Voucher Invoices,POS Closing Voucher Invoices,POS關閉憑證發票 DocType: Tax Rule,Sales Tax Template,銷售稅模板 @@ -5523,7 +5523,7 @@ The package **Item** will have ""Is Stock Item"" as ""No"" and ""Is Sales Item"" For Example: If you are selling Laptops and Backpacks separately and have a special price if the customer buys both, then the Laptop + Backpack will be a new Product Bundle Item. -Note: BOM = Bill of Materials",聚合組** **項目到另一個項目** **的。如果你是捆綁了一定**項目你保持股票的包裝**項目的**,而不是聚集**項這是一個有用的**到一個包和**。包** **項目將有“是股票項目”為“否”和“是銷售項目”為“是”。例如:如果你是銷售筆記本電腦和背包分開,並有一個特殊的價格,如果客戶購買兩個,那麼筆記本電腦+背包將是一個新的產品包項目。注:物料BOM =比爾 +Note: BOM = Bill of Materials",聚合組** **項目到另一個項目** **的。如果你是捆綁了一定**項目你保持庫存的包裝**項目的**,而不是聚集**項這是一個有用的**到一個包和**。包** **項目將有“是庫存項目”為“否”和“是銷售項目”為“是”。例如:如果你是銷售筆記本電腦和背包分開,並有一個特殊的價格,如果客戶購買兩個,那麼筆記本電腦+背包將是一個新的產品包項目。注:物料BOM =比爾 apps/erpnext/erpnext/selling/doctype/installation_note/installation_note.py +42,Serial No is mandatory for Item {0},項目{0}的序列號是強制性的 DocType: Item Variant Attribute,Attribute,屬性 DocType: Staffing Plan Detail,Current Count,當前計數 @@ -5564,7 +5564,7 @@ apps/erpnext/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +75,Max disco apps/erpnext/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py +191,Net Asset value as on,淨資產值作為 DocType: Crop,Produce,生產 DocType: Hotel Settings,Default Taxes and Charges,默認稅費 -DocType: Account,Receivable,應收賬款 +DocType: Account,Receivable,應收帳款 apps/erpnext/erpnext/selling/doctype/sales_order/sales_order.py +325,Row #{0}: Not allowed to change Supplier as Purchase Order already exists,行#{0}:不能更改供應商的採購訂單已經存在 DocType: Stock Entry,Material Consumption for Manufacture,材料消耗製造 DocType: Item Alternative,Alternative Item Code,替代項目代碼 @@ -5603,7 +5603,7 @@ DocType: Asset,Booked Fixed Asset,預訂的固定資產 apps/erpnext/erpnext/accounts/report/trial_balance/trial_balance.py +49,To Date should be within the Fiscal Year. Assuming To Date = {0},日期應該是在財政年度內。假設終止日期= {0} DocType: Employee,"Here you can maintain height, weight, allergies, medical concerns etc",在這裡,你可以保持身高,體重,過敏,醫療問題等 DocType: Leave Block List,Applies to Company,適用於公司 -apps/erpnext/erpnext/manufacturing/doctype/production_order/production_order.py +222,Cannot cancel because submitted Stock Entry {0} exists,不能取消,因為提交股票輸入{0}存在 +apps/erpnext/erpnext/manufacturing/doctype/production_order/production_order.py +222,Cannot cancel because submitted Stock Entry {0} exists,不能取消,因為提交庫存輸入{0}存在 DocType: BOM Update Tool,Update latest price in all BOMs,更新所有BOM的最新價格 apps/erpnext/erpnext/healthcare/doctype/patient/patient.js +24,Medical Record,醫療記錄 DocType: Vehicle,Vehicle,車輛 @@ -5613,13 +5613,13 @@ apps/erpnext/erpnext/hr/doctype/training_result/training_result.py +15,{0} must DocType: POS Profile,Item Groups,項目組 DocType: Sales Order Item,For Production,對於生產 DocType: Payment Request,payment_url,payment_url -DocType: Exchange Rate Revaluation Account,Balance In Account Currency,賬戶貨幣餘額 -apps/erpnext/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py +188,Please add a Temporary Opening account in Chart of Accounts,請在會計科目表中添加一個臨時開戶賬戶 +DocType: Exchange Rate Revaluation Account,Balance In Account Currency,科目貨幣餘額 +apps/erpnext/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py +188,Please add a Temporary Opening account in Chart of Accounts,請在會計科目表中添加一個臨時開戶科目 DocType: Customer,Customer Primary Contact,客戶主要聯繫人 DocType: Project Task,View Task,查看任務 apps/erpnext/erpnext/crm/report/campaign_efficiency/campaign_efficiency.py +22,Opp/Lead %,Opp / Lead% apps/erpnext/erpnext/crm/report/campaign_efficiency/campaign_efficiency.py +22,Opp/Lead %,Opp / Lead% -DocType: Bank Guarantee,Bank Account Info,銀行賬戶信息 +DocType: Bank Guarantee,Bank Account Info,銀行科目信息 DocType: Bank Guarantee,Bank Guarantee Type,銀行擔保類型 DocType: Payment Schedule,Invoice Portion,發票部分 ,Asset Depreciations and Balances,資產折舊和平衡 @@ -5701,7 +5701,7 @@ DocType: Certification Application,Yet to appear,尚未出現 DocType: Delivery Stop,Email Sent To,電子郵件發送給 DocType: Job Card Item,Job Card Item,工作卡項目 DocType: Accounts Settings,Allow Cost Center In Entry of Balance Sheet Account,允許成本中心輸入資產負債表科目 -apps/erpnext/erpnext/accounts/doctype/account/account.js +102,Merge with Existing Account,與現有帳戶合併 +apps/erpnext/erpnext/accounts/doctype/account/account.js +102,Merge with Existing Account,與現有科目合併 apps/erpnext/erpnext/stock/doctype/stock_entry/stock_entry.py +992,All items have already been transferred for this Work Order.,所有項目已經為此工作單轉移。 DocType: Appraisal,"Any other remarks, noteworthy effort that should go in the records.",任何其他言論,值得一提的努力,應該在記錄中。 DocType: Asset Maintenance,Manufacturing User,製造業用戶 @@ -5739,14 +5739,14 @@ apps/erpnext/erpnext/manufacturing/doctype/work_order/work_order.py +203,{0} ({1 DocType: Certification Application,Name of Applicant,申請人名稱 apps/erpnext/erpnext/config/manufacturing.py +27,Time Sheet for manufacturing.,時間表製造。 apps/erpnext/erpnext/templates/pages/cart.html +37,Subtotal,小計 -apps/erpnext/erpnext/stock/doctype/item/item.py +731,Cannot change Variant properties after stock transaction. You will have to make a new Item to do this.,股票交易後不能更改Variant屬性。你將不得不做一個新的項目來做到這一點。 +apps/erpnext/erpnext/stock/doctype/item/item.py +731,Cannot change Variant properties after stock transaction. You will have to make a new Item to do this.,庫存交易後不能更改Variant屬性。你將不得不做一個新的項目來做到這一點。 apps/erpnext/erpnext/config/integrations.py +18,GoCardless SEPA Mandate,GoCardless SEPA授權 DocType: Healthcare Practitioner,Charges,收費 DocType: Production Plan,Get Items For Work Order,獲取工作訂單的物品 DocType: Salary Detail,Default Amount,預設數量 apps/erpnext/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +96,Warehouse not found in the system,倉庫系統中未找到 DocType: Quality Inspection Reading,Quality Inspection Reading,質量檢驗閱讀 -apps/erpnext/erpnext/stock/doctype/stock_settings/stock_settings.py +26,`Freeze Stocks Older Than` should be smaller than %d days.,`凍結股票早於`應該是少於%d天。 +apps/erpnext/erpnext/stock/doctype/stock_settings/stock_settings.py +26,`Freeze Stocks Older Than` should be smaller than %d days.,`凍結庫存早於`應該是少於%d天。 DocType: Tax Rule,Purchase Tax Template,購置稅模板 apps/erpnext/erpnext/utilities/user_progress.py +48,Set a sales goal you'd like to achieve for your company.,為您的公司設定您想要實現的銷售目標。 apps/erpnext/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +1553,Healthcare Services,醫療服務 @@ -5781,7 +5781,7 @@ apps/erpnext/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.js +5 DocType: Warranty Claim,Resolved By,議決 apps/erpnext/erpnext/healthcare/doctype/patient_encounter/patient_encounter.js +32,Schedule Discharge,附表卸貨 apps/erpnext/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py +42,Cheques and Deposits incorrectly cleared,支票及存款不正確清除 -apps/erpnext/erpnext/accounts/doctype/account/account.py +53,Account {0}: You can not assign itself as parent account,帳戶{0}:你不能指定自己為父帳戶 +apps/erpnext/erpnext/accounts/doctype/account/account.py +53,Account {0}: You can not assign itself as parent account,科目{0}:你不能指定自己為上層科目 DocType: Purchase Invoice Item,Price List Rate,價格列表費率 apps/erpnext/erpnext/utilities/activation.py +72,Create customer quotes,創建客戶報價 apps/erpnext/erpnext/public/js/controllers/transaction.js +885,Service Stop Date cannot be after Service End Date,服務停止日期不能在服務結束日期之後 @@ -5817,7 +5817,7 @@ DocType: Daily Work Summary Settings,"Emails will be sent to all Active Employee DocType: Employee Leave Approver,Employee Leave Approver,員工請假審批 apps/erpnext/erpnext/stock/doctype/item/item.py +541,Row {0}: An Reorder entry already exists for this warehouse {1},行{0}:一個重新排序條目已存在這個倉庫{1} apps/erpnext/erpnext/crm/doctype/opportunity/opportunity.py +99,"Cannot declare as lost, because Quotation has been made.",不能聲明為丟失,因為報價已經取得進展。 -apps/erpnext/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py +68,CWIP Account,CWIP賬戶 +apps/erpnext/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py +68,CWIP Account,CWIP科目 apps/erpnext/erpnext/hr/doctype/training_event/training_event.js +16,Training Feedback,培訓反饋 apps/erpnext/erpnext/config/accounts.py +184,Tax Withholding rates to be applied on transactions.,稅收預扣稅率適用於交易。 DocType: Supplier Scorecard Criteria,Supplier Scorecard Criteria,供應商記分卡標準 @@ -5850,7 +5850,7 @@ DocType: Agriculture Analysis Criteria,Agriculture User,農業用戶 apps/erpnext/erpnext/stock/stock_ledger.py +386,{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.,{0} {1}在需要{2}在{3} {4}:{5}來完成這一交易單位。 DocType: Fee Schedule,Student Category,學生組 DocType: Announcement,Student,學生 -apps/erpnext/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.js +98,Stock quantity to start procedure is not available in the warehouse. Do you want to record a Stock Transfer,倉庫中不提供開始操作的庫存數量。你想記錄股票轉移嗎? +apps/erpnext/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.js +98,Stock quantity to start procedure is not available in the warehouse. Do you want to record a Stock Transfer,倉庫中不提供開始操作的庫存數量。你想記錄庫存轉移嗎? DocType: Shipping Rule,Shipping Rule Type,運輸規則類型 apps/erpnext/erpnext/utilities/user_progress.py +239,Go to Rooms,去房間 apps/erpnext/erpnext/hr/doctype/payroll_entry/payroll_entry.js +259,"Company, Payment Account, From Date and To Date is mandatory",公司,付款帳戶,從日期和日期是強制性的 @@ -5871,7 +5871,7 @@ DocType: Purchase Receipt Item,Received and Accepted,收貨及允收 DocType: Staffing Plan,Staffing Plan Details,人員配置計劃詳情 ,Serial No Service Contract Expiry,序號服務合同到期 DocType: Employee Health Insurance,Employee Health Insurance,員工健康保險 -apps/erpnext/erpnext/accounts/doctype/journal_entry/journal_entry.py +331,You cannot credit and debit same account at the same time,你無法將貸方與借方在同一時間記在同一帳戶 +apps/erpnext/erpnext/accounts/doctype/journal_entry/journal_entry.py +331,You cannot credit and debit same account at the same time,你無法將貸方與借方在同一時間記在同一科目 DocType: Vital Signs,Adults' pulse rate is anywhere between 50 and 80 beats per minute.,成年人的脈率在每分鐘50到80次之間。 DocType: Naming Series,Help HTML,HTML幫助 DocType: Student Group Creation Tool,Student Group Creation Tool,學生組創建工具 @@ -5954,7 +5954,7 @@ DocType: Shopping Cart Settings,Checkout Settings,結帳設定 DocType: Student Attendance,Present,現在 apps/erpnext/erpnext/stock/doctype/packing_slip/packing_slip.py +37,Delivery Note {0} must not be submitted,送貨單{0}不能提交 DocType: Notification Control,Sales Invoice Message,銷售發票訊息 -apps/erpnext/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py +27,Closing Account {0} must be of type Liability / Equity,關閉帳戶{0}的類型必須是負債/權益 +apps/erpnext/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py +27,Closing Account {0} must be of type Liability / Equity,關閉科目{0}的類型必須是負債/權益 apps/erpnext/erpnext/hr/doctype/salary_slip/salary_slip.py +407,Salary Slip of employee {0} already created for time sheet {1},員工的工資單{0}已為時間表創建{1} DocType: Production Plan Item,Ordered Qty,訂購數量 apps/erpnext/erpnext/stock/doctype/item/item.py +822,Item {0} is disabled,項目{0}無效 @@ -5997,11 +5997,11 @@ DocType: Subscription Plan,Subscription Plan,訂閱計劃 DocType: Employee External Work History,Salary,薪水 DocType: Serial No,Delivery Document Type,交付文件類型 DocType: Item Variant Settings,Do not update variants on save,不要在保存時更新變體 -DocType: Email Digest,Receivables,應收賬款 +DocType: Email Digest,Receivables,應收帳款 DocType: Lead Source,Lead Source,主導來源 DocType: Customer,Additional information regarding the customer.,對於客戶的其他訊息。 DocType: Quality Inspection Reading,Reading 5,閱讀5 -apps/erpnext/erpnext/accounts/doctype/payment_entry/payment_entry.py +237,"{0} {1} is associated with {2}, but Party Account is {3}","{0} {1} 與 {2} 關聯, 但當事方帳戶為 {3}" +apps/erpnext/erpnext/accounts/doctype/payment_entry/payment_entry.py +237,"{0} {1} is associated with {2}, but Party Account is {3}","{0} {1} 與 {2} 關聯, 但當事方科目為 {3}" DocType: Bank Statement Settings Item,Bank Header,銀行標題 apps/erpnext/erpnext/healthcare/doctype/sample_collection/sample_collection.js +7,View Lab Tests,查看實驗室測試 DocType: Hub Users,Hub Users,Hub用戶 @@ -6060,12 +6060,12 @@ apps/erpnext/erpnext/education/doctype/program_enrollment_tool/program_enrollmen DocType: Fees,Student Details,學生細節 DocType: Purchase Invoice Item,Stock Qty,庫存數量 DocType: Purchase Invoice Item,Stock Qty,庫存數量 -DocType: QuickBooks Migrator,Default Shipping Account,默認運輸帳戶 +DocType: QuickBooks Migrator,Default Shipping Account,默認運輸科目 DocType: Loan,Repayment Period in Months,在月還款期 apps/erpnext/erpnext/templates/includes/footer/footer_extension.html +26,Error: Not a valid id?,錯誤:沒有有效的身份證? DocType: Naming Series,Update Series Number,更新序列號 DocType: Account,Equity,公平 -apps/erpnext/erpnext/accounts/doctype/gl_entry/gl_entry.py +79,{0} {1}: 'Profit and Loss' type account {2} not allowed in Opening Entry,{0} {1}:“損益”帳戶類型{2}不允許進入開 +apps/erpnext/erpnext/accounts/doctype/gl_entry/gl_entry.py +79,{0} {1}: 'Profit and Loss' type account {2} not allowed in Opening Entry,{0} {1}:“損益”科目類型{2}不允許進入開 DocType: Job Offer,Printing Details,印刷詳情 DocType: Task,Closing Date,截止日期 DocType: Sales Order Item,Produced Quantity,生產的產品數量 @@ -6075,14 +6075,14 @@ DocType: Employee Tax Exemption Category,Max Amount,最大金額 DocType: Journal Entry,Total Amount Currency,總金額幣種 apps/erpnext/erpnext/stock/report/bom_search/bom_search.js +38,Search Sub Assemblies,搜索子組件 apps/erpnext/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +200,Item Code required at Row No {0},於列{0}需要產品編號 -DocType: GST Account,SGST Account,SGST賬戶 +DocType: GST Account,SGST Account,SGST科目 apps/erpnext/erpnext/utilities/user_progress.py +154,Go to Items,轉到項目 DocType: Sales Partner,Partner Type,合作夥伴類型 apps/erpnext/erpnext/accounts/report/budget_variance_report/budget_variance_report.py +72,Actual,實際 DocType: Restaurant Menu,Restaurant Manager,餐廳經理 DocType: Authorization Rule,Customerwise Discount,Customerwise折扣 apps/erpnext/erpnext/config/projects.py +46,Timesheet for tasks.,時間表的任務。 -DocType: Purchase Invoice,Against Expense Account,對費用帳戶 +DocType: Purchase Invoice,Against Expense Account,對費用科目 apps/erpnext/erpnext/stock/doctype/delivery_note/delivery_note.py +287,Installation Note {0} has already been submitted,安裝注意{0}已提交 DocType: Bank Reconciliation,Get Payment Entries,獲取付款項 DocType: Quotation Item,Against Docname,對Docname @@ -6098,7 +6098,7 @@ DocType: Training Event,Employee Emails,員工電子郵件 apps/erpnext/erpnext/setup/doctype/naming_series/naming_series.py +67,Series Updated,系列更新 apps/erpnext/erpnext/accounts/doctype/account/account.py +166,Report Type is mandatory,報告類型是強制性的 DocType: Item,Serial Number Series,序列號系列 -apps/erpnext/erpnext/buying/utils.py +68,Warehouse is mandatory for stock Item {0} in row {1},倉庫是強制性的股票項目{0}行{1} +apps/erpnext/erpnext/buying/utils.py +68,Warehouse is mandatory for stock Item {0} in row {1},倉庫是強制性的庫存項目{0}行{1} apps/erpnext/erpnext/setup/setup_wizard/data/industry_type.py +45,Retail & Wholesale,零售及批發 DocType: Issue,First Responded On,首先作出回應 DocType: Website Item Group,Cross Listing of Item in multiple groups,在多組項目的交叉上市 @@ -6146,7 +6146,7 @@ DocType: Restaurant Reservation,Waitlisted,輪候 DocType: Employee Tax Exemption Declaration Category,Exemption Category,豁免類別 apps/erpnext/erpnext/accounts/doctype/account/account.py +131,Currency can not be changed after making entries using some other currency,貨幣不能使用其他貨幣進行輸入後更改 DocType: Vehicle Service,Clutch Plate,離合器壓盤 -DocType: Company,Round Off Account,四捨五入賬戶 +DocType: Company,Round Off Account,四捨五入科目 apps/erpnext/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py +100,Administrative Expenses,行政開支 apps/erpnext/erpnext/setup/setup_wizard/data/industry_type.py +18,Consulting,諮詢 DocType: Subscription Plan,Based on price list,基於價格表 @@ -6157,7 +6157,7 @@ DocType: Purchase Invoice,Contact Email,聯絡電郵 apps/erpnext/erpnext/education/doctype/fee_schedule/fee_schedule_list.js +11,Fee Creation Pending,費用創作待定 DocType: Appraisal Goal,Score Earned,得分 DocType: Asset Category,Asset Category Name,資產類別名稱 -apps/erpnext/erpnext/setup/doctype/territory/territory.js +13,This is a root territory and cannot be edited.,集團或Ledger ,借方或貸方,是特等帳戶 +apps/erpnext/erpnext/setup/doctype/territory/territory.js +13,This is a root territory and cannot be edited.,集團或Ledger ,借方或貸方,是特等科目 apps/erpnext/erpnext/setup/doctype/sales_person/sales_person_tree.js +5,New Sales Person Name,新銷售人員的姓名 DocType: Packing Slip,Gross Weight UOM,毛重計量單位 DocType: Employee Transfer,Create New Employee Id,創建新的員工ID @@ -6170,12 +6170,12 @@ DocType: Bin,Reserved Qty for Production,預留數量生產 DocType: Student Group Creation Tool,Leave unchecked if you don't want to consider batch while making course based groups. ,如果您不想在製作基於課程的組時考慮批量,請不要選中。 DocType: Student Group Creation Tool,Leave unchecked if you don't want to consider batch while making course based groups. ,如果您不想在製作基於課程的組時考慮批量,請不要選中。 DocType: Asset,Frequency of Depreciation (Months),折舊率(月) -apps/erpnext/erpnext/accounts/doctype/journal_entry/journal_entry.js +542,Credit Account,信用賬戶 +apps/erpnext/erpnext/accounts/doctype/journal_entry/journal_entry.js +542,Credit Account,信用科目 DocType: Landed Cost Item,Landed Cost Item,到岸成本項目 apps/erpnext/erpnext/accounts/report/profitability_analysis/profitability_analysis.js +58,Show zero values,顯示零值 DocType: BOM,Quantity of item obtained after manufacturing / repacking from given quantities of raw materials,製造/從原材料數量給予重新包裝後獲得的項目數量 DocType: Lab Test,Test Group,測試組 -DocType: Payment Reconciliation,Receivable / Payable Account,應收/應付賬款 +DocType: Payment Reconciliation,Receivable / Payable Account,應收/應付帳款 DocType: Delivery Note Item,Against Sales Order Item,對銷售訂單項目 DocType: Company,Company Logo,公司標誌 apps/erpnext/erpnext/stock/doctype/item/item.py +787,Please specify Attribute Value for attribute {0},請指定屬性值的屬性{0} @@ -6199,10 +6199,10 @@ apps/erpnext/erpnext/stock/doctype/item/item.js +29,Balance,餘額 apps/erpnext/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py +66,Please select the Company,請選擇公司 DocType: Room,Seating Capacity,座位數 DocType: Lab Test Groups,Lab Test Groups,實驗室測試組 -apps/erpnext/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py +151,Party Type and Party is mandatory for {0} account,{0}帳戶必須使用派對類型和派對 +apps/erpnext/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py +151,Party Type and Party is mandatory for {0} account,{0}科目的參與方以及類型為必填 DocType: Project,Total Expense Claim (via Expense Claims),總費用報銷(通過費用報銷) DocType: GST Settings,GST Summary,消費稅總結 -apps/erpnext/erpnext/hr/doctype/daily_work_summary_group/daily_work_summary_group.py +16,Please enable default incoming account before creating Daily Work Summary Group,請在創建日常工作摘要組之前啟用默認傳入帳戶 +apps/erpnext/erpnext/hr/doctype/daily_work_summary_group/daily_work_summary_group.py +16,Please enable default incoming account before creating Daily Work Summary Group,請在創建日常工作摘要組之前啟用默認傳入科目 DocType: Assessment Result,Total Score,總得分 DocType: Crop Cycle,ISO 8601 standard,ISO 8601標準 DocType: Journal Entry,Debit Note,繳費單 @@ -6222,7 +6222,7 @@ DocType: Manufacturing Settings,Default Finished Goods Warehouse,預設成品倉 apps/erpnext/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js +380,Please select Patient,請選擇患者 apps/erpnext/erpnext/accounts/report/gross_profit/gross_profit.py +74,Sales Person,銷售人員 DocType: Hotel Room Package,Amenities,設施 -DocType: QuickBooks Migrator,Undeposited Funds Account,未存入資金賬戶 +DocType: QuickBooks Migrator,Undeposited Funds Account,未存入資金科目 apps/erpnext/erpnext/config/accounts.py +201,Budget and Cost Center,預算和成本中心 apps/erpnext/erpnext/accounts/doctype/pos_profile/pos_profile.py +65,Multiple default mode of payment is not allowed,不允許多種默認付款方式 DocType: Sales Invoice,Loyalty Points Redemption,忠誠積分兌換 @@ -6230,7 +6230,7 @@ DocType: Sales Invoice,Loyalty Points Redemption,忠誠積分兌換 DocType: Lead,Blog Subscriber,網誌訂閱者 DocType: Guardian,Alternate Number,備用號碼 apps/erpnext/erpnext/config/setup.py +83,Create rules to restrict transactions based on values.,創建規則來限制基於價值的交易。 -DocType: Cash Flow Mapping Accounts,Cash Flow Mapping Accounts,現金流量映射賬戶 +DocType: Cash Flow Mapping Accounts,Cash Flow Mapping Accounts,現金流量映射科目 apps/erpnext/erpnext/education/report/student_and_guardian_contact_details/student_and_guardian_contact_details.py +49, Group Roll No,組卷號 DocType: Batch,Manufacturing Date,生產日期 apps/erpnext/erpnext/education/doctype/fee_schedule/fee_schedule_list.js +9,Fee Creation Failed,費用創作失敗 @@ -6283,7 +6283,7 @@ DocType: Fiscal Year,Year Start Date,年結開始日期 DocType: Additional Salary,Employee Name,員工姓名 DocType: Restaurant Order Entry Item,Restaurant Order Entry Item,餐廳訂單錄入項目 DocType: Purchase Invoice,Rounded Total (Company Currency),整數總計(公司貨幣) -apps/erpnext/erpnext/accounts/doctype/account/account.py +103,Cannot covert to Group because Account Type is selected.,不能隱蔽到組,因為帳戶類型選擇的。 +apps/erpnext/erpnext/accounts/doctype/account/account.py +103,Cannot covert to Group because Account Type is selected.,不能轉換到群組科目,因為科目類型選擇的。 apps/erpnext/erpnext/selling/doctype/sales_order/sales_order.py +276,{0} {1} has been modified. Please refresh.,{0} {1} 已修改。請更新。 DocType: Leave Block List,Stop users from making Leave Applications on following days.,停止用戶在下面日期提出休假申請。 apps/erpnext/erpnext/accounts/doctype/loyalty_program/loyalty_program.js +24,"If unlimited expiry for the Loyalty Points, keep the Expiry Duration empty or 0.",如果忠誠度積分無限期到期,請將到期時間保持為空或0。 @@ -6312,7 +6312,7 @@ DocType: Company,Basic Component,基本組件 apps/erpnext/erpnext/accounts/doctype/journal_entry/journal_entry.py +578,Row No {0}: Amount cannot be greater than Pending Amount against Expense Claim {1}. Pending Amount is {2},行無{0}:金額不能大於金額之前對報銷{1}。待審核金額為{2} DocType: Patient Service Unit,Medical Administrator,醫療管理員 DocType: Assessment Plan,Schedule,時間表 -DocType: Account,Parent Account,父帳戶 +DocType: Account,Parent Account,上層科目 DocType: Quality Inspection Reading,Reading 3,閱讀3 DocType: Stock Entry,Source Warehouse Address,來源倉庫地址 DocType: GL Entry,Voucher Type,憑證類型 @@ -6356,7 +6356,7 @@ apps/erpnext/erpnext/accounts/doctype/journal_entry/journal_entry.py +249,Row {0 DocType: Employee Promotion,Employee Promotion,員工晉升 DocType: Maintenance Team Member,Maintenance Team Member,維護團隊成員 apps/erpnext/erpnext/education/report/course_wise_assessment_report/course_wise_assessment_report.html +16,Course Code: ,課程編號: -apps/erpnext/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +241,Please enter Expense Account,請輸入您的費用帳戶 +apps/erpnext/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +241,Please enter Expense Account,請輸入您的費用科目 DocType: Account,Stock,庫存 apps/erpnext/erpnext/accounts/doctype/payment_entry/payment_entry.js +1128,"Row #{0}: Reference Document Type must be one of Purchase Order, Purchase Invoice or Journal Entry",行#{0}:參考文件類型必須是採購訂單之一,購買發票或日記帳分錄 DocType: Employee,Current Address,當前地址 @@ -6372,7 +6372,7 @@ DocType: Sales Order,Track this Sales Order against any Project,跟踪對任何 DocType: Bank Statement Transaction Entry,Bank Statement Transaction Entry,銀行對賬單交易分錄 DocType: Sales Invoice Item,Discount and Margin,折扣和保證金 DocType: Lab Test,Prescription,處方 -DocType: Company,Default Deferred Revenue Account,默認遞延收入賬戶 +DocType: Company,Default Deferred Revenue Account,默認遞延收入科目 DocType: Project,Second Email,第二封郵件 DocType: Budget,Action if Annual Budget Exceeded on Actual,年度預算超出實際的行動 DocType: Pricing Rule,Min Qty,最小數量 @@ -6394,7 +6394,7 @@ apps/erpnext/erpnext/config/manufacturing.py +18,Generate Material Requests (MRP apps/erpnext/erpnext/accounts/doctype/pos_profile/pos_profile.py +62,Set default mode of payment,設置默認付款方式 DocType: BOM,With Operations,加入作業 DocType: Support Search Source,Post Route Key List,發布路由密鑰列表 -apps/erpnext/erpnext/accounts/party.py +288,Accounting entries have already been made in currency {0} for company {1}. Please select a receivable or payable account with currency {0}.,會計分錄已取得貨幣{0}為公司{1}。請選擇一個應收或應付賬戶幣種{0}。 +apps/erpnext/erpnext/accounts/party.py +288,Accounting entries have already been made in currency {0} for company {1}. Please select a receivable or payable account with currency {0}.,會計分錄已取得貨幣{0}為公司{1}。請選擇一個應收或應付科目幣種{0}。 DocType: Asset,Is Existing Asset,是對現有資產 DocType: Salary Component,Statistical Component,統計組成部分 DocType: Salary Component,Statistical Component,統計組成部分 @@ -6482,7 +6482,7 @@ apps/erpnext/erpnext/public/js/hub/components/item_publish_dialog.js +3,Edit Pub DocType: Packing Slip,Package Weight Details,包裝重量詳情 DocType: Leave Type,Is Compensatory,是有補償的 DocType: Restaurant Reservation,Reservation Time,預訂時間 -DocType: Payment Gateway Account,Payment Gateway Account,網路支付閘道帳戶 +DocType: Payment Gateway Account,Payment Gateway Account,網路支付閘道科目 DocType: Shopping Cart Settings,After payment completion redirect user to selected page.,支付完成後重定向用戶選擇的頁面。 DocType: Company,Existing Company,現有的公司 DocType: Healthcare Settings,Result Emailed,電子郵件結果 @@ -6507,7 +6507,7 @@ DocType: Terms and Conditions,Terms and Conditions Help,條款和條件幫助 ,Item-wise Purchase Register,項目明智的購買登記 DocType: Loyalty Point Entry,Expiry Date,到期時間 DocType: Healthcare Settings,Employee name and designation in print,員工姓名和印刷品名稱 -,accounts-browser,賬戶瀏覽器 +,accounts-browser,科目瀏覽器 apps/erpnext/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +381,Please select Category first,請先選擇分類 apps/erpnext/erpnext/config/projects.py +13,Project master.,專案主持。 apps/erpnext/erpnext/controllers/status_updater.py +215,"To allow over-billing or over-ordering, update ""Allowance"" in Stock Settings or the Item.",要允許對賬單或過度訂貨,庫存設置或更新項目“津貼”。 @@ -6526,11 +6526,11 @@ apps/erpnext/erpnext/patches/v11_0/add_default_dispatch_notification_template.py apps/erpnext/erpnext/controllers/accounts_controller.py +693,Row #{0}: Posting Date must be same as purchase date {1} of asset {2},行#{0}:過帳日期必須是相同的購買日期{1}資產的{2} DocType: Program Enrollment,Check this if the Student is residing at the Institute's Hostel.,如果學生住在學院的旅館,請檢查。 apps/erpnext/erpnext/manufacturing/doctype/production_planning_tool/production_planning_tool.py +125,Please enter Sales Orders in the above table,請在上表中輸入銷售訂單 -,Stock Summary,股票摘要 +,Stock Summary,庫存摘要 apps/erpnext/erpnext/config/assets.py +62,Transfer an asset from one warehouse to another,從一個倉庫轉移資產到另一 DocType: Employee Benefit Application,Remaining Benefits (Yearly),剩餘福利(每年) apps/erpnext/erpnext/stock/doctype/material_request/material_request.js +873,Bill of Materials,材料清單 -apps/erpnext/erpnext/accounts/doctype/journal_entry/journal_entry.py +137,Row {0}: Party Type and Party is required for Receivable / Payable account {1},行{0}:黨的類型和黨的需要應收/應付帳戶{1} +apps/erpnext/erpnext/accounts/doctype/journal_entry/journal_entry.py +137,Row {0}: Party Type and Party is required for Receivable / Payable account {1},行{0}:參與方類型和參與方需要應收/應付科目{1} DocType: Employee,Leave Policy,離開政策 apps/erpnext/erpnext/buying/doctype/purchase_order/purchase_order.js +865,Update Items,更新項目 apps/erpnext/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py +94,Ref Date,參考日期 @@ -6542,7 +6542,7 @@ DocType: GL Entry,Is Opening,是開幕 DocType: Department,Expense Approvers,費用審批人 apps/erpnext/erpnext/accounts/doctype/journal_entry/journal_entry.py +228,Row {0}: Debit entry can not be linked with a {1},行{0}:借方條目不能與{1}連接 DocType: Journal Entry,Subscription Section,認購科 -apps/erpnext/erpnext/accounts/doctype/account/account.py +238,Account {0} does not exist,帳戶{0}不存在 +apps/erpnext/erpnext/accounts/doctype/account/account.py +238,Account {0} does not exist,科目{0}不存在 DocType: Training Event,Training Program,培訓計劃 DocType: Account,Cash,現金 DocType: Employee,Short biography for website and other publications.,網站和其他出版物的短的傳記。 diff --git a/erpnext/translations/zh.csv b/erpnext/translations/zh.csv index 716f1f2f31..2337bcb4da 100644 --- a/erpnext/translations/zh.csv +++ b/erpnext/translations/zh.csv @@ -453,11 +453,11 @@ Cancel Subscription,取消订阅, Cancel the journal entry {0} first,首先取消日记条目{0}, Canceled,取消, "Cannot Submit, Employees left to mark attendance",无法提交,不能为已离职员工登记考勤, -Cannot be a fixed asset item as Stock Ledger is created.,不能成为股票分类账创建的固定资产项目。, +Cannot be a fixed asset item as Stock Ledger is created.,不能成为库存分类账创建的固定资产项目。, Cannot cancel because submitted Stock Entry {0} exists,不能取消,因为提交的仓储记录{0}已经存在, Cannot cancel transaction for Completed Work Order.,无法取消已完成工单的交易。, Cannot cancel {0} {1} because Serial No {2} does not belong to the warehouse {3},无法取消{0} {1},因为序列号{2}不属于仓库{3}, -Cannot change Attributes after stock transaction. Make a new Item and transfer stock to the new Item,股票交易后不能更改属性。创建一个新项目并将库存转移到新项目, +Cannot change Attributes after stock transaction. Make a new Item and transfer stock to the new Item,库存交易后不能更改属性。创建一个新项目并将库存转移到新项目, Cannot change Fiscal Year Start Date and Fiscal Year End Date once the Fiscal Year is saved.,财年保存后便不能更改财年开始日期和结束日期, Cannot change Service Stop Date for item in row {0},无法更改行{0}中项目的服务停止日期, Cannot change Variant properties after stock transaction. You will have to make a new Item to do this.,存货业务发生后不能更改变体物料的属性。需要新建新物料。, @@ -2309,7 +2309,7 @@ Receivable Account,应收账款, Received,收到, Received On,收到的, Received Quantity,收到的数量, -Received Stock Entries,收到的股票条目, +Received Stock Entries,收到的库存条目, Receiver List is empty. Please create Receiver List,接收人列表为空。请创建接收人列表, Recipients,收件人, Reconcile,核消(对帐), @@ -2783,7 +2783,7 @@ State,州, State/UT Tax,州/ UT税, Statement of Account,对账单, Status must be one of {0},状态必须是{0}中的一个, -Stock,股票, +Stock,库存, Stock Adjustment,库存调整, Stock Analytics,库存分析, Stock Assets,库存资产, @@ -2963,7 +2963,7 @@ The folio numbers are not matching,作品集编号不匹配, The holiday on {0} is not between From Date and To Date,在{0}这个节日之间没有从日期和结束日期, The name of the institute for which you are setting up this system.,对于要为其建立这个系统的该机构的名称。, The name of your company for which you are setting up this system.,贵公司的名称, -The number of shares and the share numbers are inconsistent,股份数量和股票数量不一致, +The number of shares and the share numbers are inconsistent,股份数量和库存数量不一致, The payment gateway account in plan {0} is different from the payment gateway account in this payment request,计划{0}中的支付网关帐户与此付款请求中的支付网关帐户不同, The selected BOMs are not for the same item,所选物料清单不能用于同一个物料, The selected item cannot have Batch,所选物料不能有批次, @@ -3514,7 +3514,7 @@ or,或, Ageing Range 4,老化范围4, Allocated amount cannot be greater than unadjusted amount,分配的金额不能大于未调整的金额, Allocated amount cannot be negative,分配数量不能为负数, -"Difference Account must be a Asset/Liability type account, since this Stock Entry is an Opening Entry",差异账户必须是资产/负债类型账户,因为此股票分录是开仓分录, +"Difference Account must be a Asset/Liability type account, since this Stock Entry is an Opening Entry",差异账户必须是资产/负债类型账户,因为此库存分录是开仓分录, Error in some rows,某些行出错, Import Successful,导入成功, Please save first,请先保存, @@ -3531,13 +3531,12 @@ Duplicate entry against the item code {0} and manufacturer {1},项目代码{0} Invalid GSTIN! The input you've entered doesn't match the GSTIN format for UIN Holders or Non-Resident OIDAR Service Providers,GSTIN无效!您输入的输入与UIN持有人或非居民OIDAR服务提供商的GSTIN格式不符, Invoice Grand Total,发票总计, Last carbon check date cannot be a future date,最后的碳检查日期不能是未来的日期, -Make Stock Entry,进入股票, +Make Stock Entry,进入库存, Quality Feedback,质量反馈, Quality Feedback Template,质量反馈模板, Rules for applying different promotional schemes.,适用不同促销计划的规则。, Shift,转移, Show {0},显示{0}, -"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series",命名系列中不允许使用除“ - ”,“#”,“。”,“/”,“{”和“}”之外的特殊字符, Target Details,目标细节, {0} already has a Parent Procedure {1}.,{0}已有父程序{1}。, API,应用程序界面, @@ -3626,7 +3625,7 @@ BOM 2,BOM 2, BOM Comparison Tool,BOM比较工具, BOM recursion: {0} cannot be child of {1},BOM递归:{0}不能是{1}的子代, BOM recursion: {0} cannot be parent or child of {1},BOM递归:{0}不能是{1}的父级或子级, -Back to Home,回到家, +Back to Home,回到主页, Back to Messages,回到消息, Bank Data mapper doesn't exist,银行数据映射器不存在, Bank Details,银行明细, @@ -3786,7 +3785,7 @@ Help,帮助, Help Article,帮助文章, "Helps you keep tracks of Contracts based on Supplier, Customer and Employee",帮助您根据供应商,客户和员工记录合同, Helps you manage appointments with your leads,帮助您管理潜在客户的约会, -Home,家, +Home,主页, IBAN is not valid,IBAN无效, Import Data from CSV / Excel files.,从CSV / Excel文件导入数据。, In Progress,进行中, @@ -4064,8 +4063,8 @@ Start Time,开始时间, Status,状态, Status must be Cancelled or Completed,状态必须已取消或已完成, Stock Balance Report,库存余额报告, -Stock Entry has been already created against this Pick List,已经根据此选择列表创建了股票输入, -Stock Ledger ID,股票分类帐编号, +Stock Entry has been already created against this Pick List,已经根据此选择列表创建了库存输入, +Stock Ledger ID,库存分类帐编号, Stock Value ({0}) and Account Balance ({1}) are out of sync for account {2} and it's linked warehouses.,库存值({0})和帐户余额({1})与帐户{2}及其链接的仓库不同步。, Stores - {0},商店-{0}, Student with email {0} does not exist,电子邮件{0}的学生不存在, @@ -6127,7 +6126,7 @@ Procedure Template,程序模板, Procedure Prescription,程序处方, Service Unit,服务单位, Consumables,耗材, -Consume Stock,消费股票, +Consume Stock,消费库存, Invoice Consumables Separately,发票耗材分开, Consumption Invoiced,消费发票, Consumable Total Amount,耗材总量, @@ -8285,8 +8284,8 @@ Out of AMC,出资产管理公司, Warranty Period (Days),保修期限(天数), Serial No Details,序列号信息, MAT-STE-.YYYY.-,MAT-STE-.YYYY.-, -Stock Entry Type,股票进入类型, -Stock Entry (Outward GIT),股票进入(外向GIT), +Stock Entry Type,库存进入类型, +Stock Entry (Outward GIT),库存进入(外向GIT), Material Consumption for Manufacture,生产所需的材料消耗, Repack,包装, Send to Subcontractor,发送给分包商, @@ -8318,8 +8317,8 @@ Serial No / Batch,序列号/批次, BOM No. for a Finished Good Item,成品物料的物料清单编号, Material Request used to make this Stock Entry,创建此手工库存移动的材料申请, Subcontracted Item,外包物料, -Against Stock Entry,反对股票进入, -Stock Entry Child,股票入境儿童, +Against Stock Entry,反对库存进入, +Stock Entry Child,库存入境儿童, PO Supplied Item,PO提供的物品, Reference Purchase Receipt,参考购买收据, Stock Ledger Entry,库存分类帐分录, @@ -8571,7 +8570,7 @@ Serial No Service Contract Expiry,序列号/年度保养合同过期, Serial No Status,序列号状态, Serial No Warranty Expiry,序列号/保修到期, Stock Ageing,库存账龄, -Stock and Account Value Comparison,股票和账户价值比较, +Stock and Account Value Comparison,库存和账户价值比较, Stock Projected Qty,预期可用库存, Student and Guardian Contact Details,学生和监护人联系方式, Student Batch-Wise Attendance,学生按批考勤, @@ -9655,7 +9654,7 @@ Raise Material Request When Stock Reaches Re-order Level,库存达到再订购 Notify by Email on Creation of Automatic Material Request,通过电子邮件通知创建自动物料请求, Allow Material Transfer from Delivery Note to Sales Invoice,允许物料从交货单转移到销售发票, Allow Material Transfer from Purchase Receipt to Purchase Invoice,允许从收货到采购发票的物料转移, -Freeze Stocks Older Than (Days),冻结大于(天)的股票, +Freeze Stocks Older Than (Days),冻结大于(天)的库存, Role Allowed to Edit Frozen Stock,允许角色编辑冻结库存, The unallocated amount of Payment Entry {0} is greater than the Bank Transaction's unallocated amount,付款条目{0}的未分配金额大于银行交易的未分配金额, Payment Received,已收到付款, @@ -9698,7 +9697,7 @@ Creating {} out of {} {},在{} {}中创建{}, Item {0} {1},项目{0} {1}, Last Stock Transaction for item {0} under warehouse {1} was on {2}.,仓库{1}下项目{0}的上次库存交易在{2}上。, Stock Transactions for Item {0} under warehouse {1} cannot be posted before this time.,在此之前,不能过帐仓库{1}下物料{0}的库存交易。, -Posting future stock transactions are not allowed due to Immutable Ledger,由于总帐不可变,不允许过帐未来的股票交易, +Posting future stock transactions are not allowed due to Immutable Ledger,由于总帐不可变,不允许过帐未来的库存交易, A BOM with name {0} already exists for item {1}.,项目{1}的名称为{0}的BOM已存在。, {0}{1} Did you rename the item? Please contact Administrator / Tech support,{0} {1}您是否重命名了该项目?请联系管理员/技术支持, At row #{0}: the sequence id {1} cannot be less than previous row sequence id {2},在第{0}行:序列ID {1}不能小于上一行的序列ID {2}, diff --git a/erpnext/translations/zh_tw.csv b/erpnext/translations/zh_tw.csv index 1cc7d8773e..1b7e18637d 100644 --- a/erpnext/translations/zh_tw.csv +++ b/erpnext/translations/zh_tw.csv @@ -3311,7 +3311,6 @@ Quality Feedback Template,質量反饋模板, Rules for applying different promotional schemes.,適用不同促銷計劃的規則。, Shift,轉移, Show {0},顯示{0}, -"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series",命名系列中不允許使用除“ - ”,“#”,“。”,“/”,“{”和“}”之外的特殊字符, Target Details,目標細節, API,API, Annual,年刊, diff --git a/erpnext/utilities/__init__.py b/erpnext/utilities/__init__.py index c2b4229f17..24bfdc63af 100644 --- a/erpnext/utilities/__init__.py +++ b/erpnext/utilities/__init__.py @@ -1,6 +1,9 @@ ## temp utility +from contextlib import contextmanager + import frappe +from frappe import _ from frappe.utils import cstr from erpnext.utilities.activation import get_level @@ -35,3 +38,16 @@ def get_site_info(site_info): domain = frappe.get_cached_value("Company", cstr(company), "domain") return {"company": company, "domain": domain, "activation": get_level()} + + +@contextmanager +def payment_app_import_guard(): + marketplace_link = 'Marketplace' + github_link = 'GitHub' + msg = _("payments app is not installed. Please install it from {} or {}").format( + marketplace_link, github_link + ) + try: + yield + except ImportError: + frappe.throw(msg, title=_("Missing Payments App")) diff --git a/erpnext/utilities/bulk_transaction.py b/erpnext/utilities/bulk_transaction.py index bfcba0766e..c1579b3cbc 100644 --- a/erpnext/utilities/bulk_transaction.py +++ b/erpnext/utilities/bulk_transaction.py @@ -9,7 +9,6 @@ from frappe import _ def transaction_processing(data, from_doctype, to_doctype): if isinstance(data, str): deserialized_data = json.loads(data) - else: deserialized_data = data @@ -30,30 +29,29 @@ def transaction_processing(data, from_doctype, to_doctype): def job(deserialized_data, from_doctype, to_doctype): - failed_history = [] - i = 0 + fail_count = 0 for d in deserialized_data: - failed = [] - try: - i += 1 doc_name = d.get("name") frappe.db.savepoint("before_creation_state") task(doc_name, from_doctype, to_doctype) - except Exception as e: frappe.db.rollback(save_point="before_creation_state") - failed_history.append(e) - failed.append(e) + fail_count += 1 update_logger( - doc_name, e, from_doctype, to_doctype, status="Failed", log_date=str(date.today()) + doc_name, + str(frappe.get_traceback()), + from_doctype, + to_doctype, + status="Failed", + log_date=str(date.today()), ) - if not failed: + else: update_logger( doc_name, None, from_doctype, to_doctype, status="Success", log_date=str(date.today()) ) - show_job_status(failed_history, deserialized_data, to_doctype) + show_job_status(fail_count, len(deserialized_data), to_doctype) def task(doc_name, from_doctype, to_doctype): @@ -75,7 +73,7 @@ def task(doc_name, from_doctype, to_doctype): }, "Sales Invoice": { "Delivery Note": sales_invoice.make_delivery_note, - "Payment": payment_entry.get_payment_entry, + "Payment Entry": payment_entry.get_payment_entry, }, "Delivery Note": { "Sales Invoice": delivery_note.make_sales_invoice, @@ -94,13 +92,13 @@ def task(doc_name, from_doctype, to_doctype): "Purchase Invoice": purchase_order.make_purchase_invoice, "Purchase Receipt": purchase_order.make_purchase_receipt, }, - "Purhcase Invoice": { + "Purchase Invoice": { "Purchase Receipt": purchase_invoice.make_purchase_receipt, - "Payment": payment_entry.get_payment_entry, + "Payment Entry": payment_entry.get_payment_entry, }, "Purchase Receipt": {"Purchase Invoice": purchase_receipt.make_purchase_invoice}, } - if to_doctype in ["Advance Payment", "Payment"]: + if to_doctype in ["Advance Payment", "Payment Entry"]: obj = mapper[from_doctype][to_doctype](from_doctype, doc_name) else: obj = mapper[from_doctype][to_doctype](doc_name) @@ -150,15 +148,16 @@ def update_logger(doc_name, e, from_doctype, to_doctype, status, log_date=None, log_doc.save() -def show_job_status(failed_history, deserialized_data, to_doctype): - if not failed_history: +def show_job_status(fail_count, deserialized_data_count, to_doctype): + if not fail_count: frappe.msgprint( - _("Creation of {0} successful").format(to_doctype), + _("Creation of {1}(s) successful").format( + to_doctype.lower().replace(" ", "-"), to_doctype + ), title="Successful", indicator="green", ) - - if len(failed_history) != 0 and len(failed_history) < len(deserialized_data): + elif fail_count != 0 and fail_count < deserialized_data_count: frappe.msgprint( _( """Creation of {0} partially successful. @@ -167,8 +166,7 @@ def show_job_status(failed_history, deserialized_data, to_doctype): title="Partially successful", indicator="orange", ) - - if len(failed_history) == len(deserialized_data): + else: frappe.msgprint( _( """Creation of {0} failed. @@ -180,9 +178,7 @@ def show_job_status(failed_history, deserialized_data, to_doctype): def record_exists(log_doc, doc_name, status): - record = mark_retrired_transaction(log_doc, doc_name) - if record and status == "Failed": return False elif record and status == "Success": diff --git a/erpnext/utilities/doctype/video/video.py b/erpnext/utilities/doctype/video/video.py index 15dbccde89..13b7877b21 100644 --- a/erpnext/utilities/doctype/video/video.py +++ b/erpnext/utilities/doctype/video/video.py @@ -59,7 +59,7 @@ def update_youtube_data(): "Video Settings", "Video Settings", ["enable_youtube_tracking", "frequency"] ) - if not enable_youtube_tracking: + if not cint(enable_youtube_tracking): return frequency = get_frequency(frequency) diff --git a/erpnext/utilities/product.py b/erpnext/utilities/product.py index 04ee0b3b1e..afe9654e8e 100644 --- a/erpnext/utilities/product.py +++ b/erpnext/utilities/product.py @@ -110,6 +110,7 @@ def get_price(item_code, price_list, customer_group, company, qty=1): "conversion_rate": 1, "for_shopping_cart": True, "currency": frappe.db.get_value("Price List", price_list, "currency"), + "doctype": "Quotation", } ) diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py index cd1bf9f321..21a0a551b6 100644 --- a/erpnext/utilities/transaction_base.py +++ b/erpnext/utilities/transaction_base.py @@ -71,6 +71,9 @@ class TransactionBase(StatusUpdater): self.validate_value(field, condition, prevdoc_values[field], doc) def validate_rate_with_reference_doc(self, ref_details): + if self.get("is_internal_supplier"): + return + buying_doctypes = ["Purchase Order", "Purchase Invoice", "Purchase Receipt"] if self.doctype in buying_doctypes: diff --git a/erpnext/www/book_appointment/index.css b/erpnext/www/book_appointment/index.css index 277610876f..3b1b97c7b8 100644 --- a/erpnext/www/book_appointment/index.css +++ b/erpnext/www/book_appointment/index.css @@ -45,7 +45,7 @@ .time-slot.selected { color: white; - background: #5e64ff; + background: var(--primary-color); } .time-slot.selected .text-muted { diff --git a/erpnext/www/book_appointment/index.html b/erpnext/www/book_appointment/index.html index 207175f89d..ad964d9370 100644 --- a/erpnext/www/book_appointment/index.html +++ b/erpnext/www/book_appointment/index.html @@ -12,8 +12,8 @@
-

Book an appointment

-

Select the date and your timezone

+

{{ _("Book an appointment") }}

+

{{ _("Select the date and your timezone") }}

@@ -31,7 +31,7 @@
- +
@@ -39,24 +39,24 @@
-

Add details

-

Selected date is at +

{{ _("Add details") }}

+

{{ _("Selected date is") }} {{ _("at") }}

- + - + + placeholder="{{ _('Notes') }}">
-
-
+
+
diff --git a/erpnext/www/book_appointment/index.js b/erpnext/www/book_appointment/index.js index 5562cbd471..d02cdad4cf 100644 --- a/erpnext/www/book_appointment/index.js +++ b/erpnext/www/book_appointment/index.js @@ -2,8 +2,6 @@ frappe.ready(async () => { initialise_select_date(); }) -window.holiday_list = []; - async function initialise_select_date() { navigate_to_page(1); await get_global_variables(); @@ -20,7 +18,6 @@ async function get_global_variables() { window.timezones = (await frappe.call({ method:'erpnext.www.book_appointment.index.get_timezones' })).message; - window.holiday_list = window.appointment_settings.holiday_list; } function setup_timezone_selector() { @@ -69,7 +66,7 @@ function on_date_or_timezone_select() { window.selected_timezone = timezone.value; update_time_slots(date_picker.value, timezone.value); let lead_text = document.getElementById('lead-text'); - lead_text.innerHTML = "Select Time" + lead_text.innerHTML = __("Select Time") } async function get_time_slots(date, timezone) { @@ -89,7 +86,7 @@ async function update_time_slots(selected_date, selected_timezone) { clear_time_slots(); if (window.slots.length <= 0) { let message_div = document.createElement('p'); - message_div.innerHTML = "There are no slots available on this date"; + message_div.innerHTML = __("There are no slots available on this date"); timeslot_container.appendChild(message_div); return } @@ -128,7 +125,7 @@ function get_slot_layout(time) { let start_time_string = moment(time).tz(timezone).format("LT"); let end_time = moment(time).tz(timezone).add(window.appointment_settings.appointment_duration, 'minutes'); let end_time_string = end_time.format("LT"); - return `${start_time_string}
to ${end_time_string}`; + return `${start_time_string}
${__("to") } ${end_time_string}`; } function select_time() { @@ -227,9 +224,9 @@ async function submit() { }, callback: (response)=>{ if (response.message.status == "Unverified") { - frappe.show_alert("Please check your email to confirm the appointment") + frappe.show_alert(__("Please check your email to confirm the appointment")) } else { - frappe.show_alert("Appointment Created Successfully"); + frappe.show_alert(__("Appointment Created Successfully")); } setTimeout(()=>{ let redirect_url = "/"; @@ -239,7 +236,7 @@ async function submit() { window.location.href = redirect_url;},5000) }, error: (err)=>{ - frappe.show_alert("Something went wrong please try again"); + frappe.show_alert(__("Something went wrong please try again")); button.disabled = false; } }); diff --git a/erpnext/www/book_appointment/index.py b/erpnext/www/book_appointment/index.py index 06e99da3f9..dfca9465ed 100644 --- a/erpnext/www/book_appointment/index.py +++ b/erpnext/www/book_appointment/index.py @@ -26,8 +26,12 @@ def get_context(context): @frappe.whitelist(allow_guest=True) def get_appointment_settings(): - settings = frappe.get_doc("Appointment Booking Settings") - settings.holiday_list = frappe.get_doc("Holiday List", settings.holiday_list) + settings = frappe.get_cached_value( + "Appointment Booking Settings", + None, + ["advance_booking_days", "appointment_duration", "success_redirect_url"], + as_dict=True, + ) return settings @@ -106,7 +110,7 @@ def create_appointment(date, time, tz, contact): appointment.customer_details = contact.get("notes", None) appointment.customer_email = contact.get("email", None) appointment.status = "Open" - appointment.insert() + appointment.insert(ignore_permissions=True) return appointment diff --git a/erpnext/www/book_appointment/verify/index.html b/erpnext/www/book_appointment/verify/index.html index 9bcd3d202e..58c07e85cc 100644 --- a/erpnext/www/book_appointment/verify/index.html +++ b/erpnext/www/book_appointment/verify/index.html @@ -8,11 +8,11 @@ {% if success==True %}
- Your email has been verified and your appointment has been scheduled + {{ _("Your email has been verified and your appointment has been scheduled") }}
{% else %}
- Verification failed please check the link + {{ _("Verification failed please check the link") }}
{% endif %} {% endblock%} diff --git a/erpnext/www/book_appointment/verify/index.py b/erpnext/www/book_appointment/verify/index.py index 1a5ba9de7e..3beb8667ae 100644 --- a/erpnext/www/book_appointment/verify/index.py +++ b/erpnext/www/book_appointment/verify/index.py @@ -2,7 +2,6 @@ import frappe from frappe.utils.verified_command import verify_request -@frappe.whitelist(allow_guest=True) def get_context(context): if not verify_request(): context.success = False diff --git a/erpnext/www/shop-by-category/index.py b/erpnext/www/shop-by-category/index.py index 8a92418d25..219747c9f8 100644 --- a/erpnext/www/shop-by-category/index.py +++ b/erpnext/www/shop-by-category/index.py @@ -51,21 +51,31 @@ def get_tabs(categories): return tab_values -def get_category_records(categories): +def get_category_records(categories: list): categorical_data = {} - for category in categories: - if category == "item_group": + + for c in categories: + if c == "item_group": categorical_data["item_group"] = frappe.db.get_all( "Item Group", filters={"parent_item_group": "All Item Groups", "show_in_website": 1}, fields=["name", "parent_item_group", "is_group", "image", "route"], ) - else: - doctype = frappe.unscrub(category) - fields = ["name"] - if frappe.get_meta(doctype, cached=True).get_field("image"): + + continue + + doctype = frappe.unscrub(c) + fields = ["name"] + + try: + meta = frappe.get_meta(doctype, cached=True) + if meta.get_field("image"): fields += ["image"] - categorical_data[category] = frappe.db.get_all(doctype, fields=fields) + data = frappe.db.get_all(doctype, fields=fields) + categorical_data[c] = data + except BaseException: + frappe.throw(_("DocType {} not found").format(doctype)) + continue return categorical_data diff --git a/license.txt b/license.txt index a238a97b06..f288702d2f 100644 --- a/license.txt +++ b/license.txt @@ -1,205 +1,200 @@ -### GNU GENERAL PUBLIC LICENSE + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 -Version 3, 29 June 2007 + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. -Copyright (C) 2007 Free Software Foundation, Inc. - + Preamble -Everyone is permitted to copy and distribute verbatim copies of this -license document, but changing it is not allowed. - -### Preamble - -The GNU General Public License is a free, copyleft license for + The GNU General Public License is a free, copyleft license for software and other kinds of works. -The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom -to share and change all versions of a program--to make sure it remains -free software for all its users. We, the Free Software Foundation, use -the GNU General Public License for most of our software; it applies -also to any other work released this way by its authors. You can apply -it to your programs, too. + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. -When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. -To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you -have certain responsibilities if you distribute copies of the -software, or if you modify it: responsibilities to respect the freedom -of others. + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. -For example, if you distribute copies of such a program, whether + For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they know their rights. -Developers that use the GNU GPL protect your rights with two steps: + Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. -For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. -Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the -manufacturer can do so. This is fundamentally incompatible with the -aim of protecting users' freedom to change the software. The -systematic pattern of such abuse occurs in the area of products for -individuals to use, which is precisely where it is most unacceptable. -Therefore, we have designed this version of the GPL to prohibit the -practice for those products. If such problems arise substantially in -other domains, we stand ready to extend this provision to those -domains in future versions of the GPL, as needed to protect the -freedom of users. + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. -Finally, every program is threatened constantly by software patents. + Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish -to avoid the special danger that patents applied to a free program -could make it effectively proprietary. To prevent this, the GPL -assures that patents cannot be used to render the program non-free. +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. -The precise terms and conditions for copying, distribution and + The precise terms and conditions for copying, distribution and modification follow. -### TERMS AND CONDITIONS + TERMS AND CONDITIONS -#### 0. Definitions. + 0. Definitions. -"This License" refers to version 3 of the GNU General Public License. + "This License" refers to version 3 of the GNU General Public License. -"Copyright" also means copyright-like laws that apply to other kinds -of works, such as semiconductor masks. + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. -"The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. -To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of -an exact copy. The resulting work is called a "modified version" of -the earlier work or a work "based on" the earlier work. + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. -A "covered work" means either the unmodified Program or a work based + A "covered work" means either the unmodified Program or a work based on the Program. -To "propagate" a work means to do anything with it that, without + To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, +computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. -To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user -through a computer network, with no transfer of a copy, is not -conveying. + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. -An interactive user interface displays "Appropriate Legal Notices" to -the extent that it includes a convenient and prominently visible + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If +work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. -#### 1. Source Code. + 1. Source Code. -The "source code" for a work means the preferred form of the work for -making modifications to it. "Object code" means any non-source form of -a work. + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. -A "Standard Interface" means an interface that either is an official + A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. -The "System Libraries" of an executable work include anything, other + The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A +implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. -The "Corresponding Source" for a work in object code form means all + The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's +control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source +which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. -The Corresponding Source need not include anything that users can -regenerate automatically from other parts of the Corresponding Source. + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. -The Corresponding Source for a work in source code form is that same -work. + The Corresponding Source for a work in source code form is that +same work. -#### 2. Basic Permissions. + 2. Basic Permissions. -All rights granted under this License are granted for the term of + All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your +content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. -You may make, run and propagate covered works that you do not convey, -without conditions so long as your license otherwise remains in force. -You may convey covered works to others for the sole purpose of having -them make modifications exclusively for you, or provide you with -facilities for running those works, provided that you comply with the -terms of this License in conveying all material for which you do not -control copyright. Those thus making or running the covered works for -you must do so exclusively on your behalf, under your direction and -control, on terms that prohibit them from making any copies of your -copyrighted material outside their relationship with you. + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. -Conveying under any other circumstances is permitted solely under the -conditions stated below. Sublicensing is not allowed; section 10 makes -it unnecessary. + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. -#### 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. -No covered work shall be deemed part of an effective technological + No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. -When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such -circumvention is effected by exercising rights under this License with -respect to the covered work, and you disclaim any intention to limit -operation or modification of the work as a means of enforcing, against -the work's users, your or third parties' legal rights to forbid -circumvention of technological measures. + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. -#### 4. Conveying Verbatim Copies. + 4. Conveying Verbatim Copies. -You may convey verbatim copies of the Program's source code as you + You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any @@ -207,56 +202,59 @@ non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. -You may charge any price or no price for each copy that you convey, + You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. -#### 5. Conveying Modified Source Versions. + 5. Conveying Modified Source Versions. -You may convey a work based on the Program, or the modifications to + You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these -conditions: +terms of section 4, provided that you also meet all of these conditions: -- a) The work must carry prominent notices stating that you modified + a) The work must carry prominent notices stating that you modified it, and giving a relevant date. -- b) The work must carry prominent notices stating that it is - released under this License and any conditions added under - section 7. This requirement modifies the requirement in section 4 - to "keep intact all notices". -- c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no + regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. -- d) If the work has interactive user interfaces, each must display + + d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. -A compilation of a covered work with other separate and independent + A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work +beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. -#### 6. Conveying Non-Source Forms. + 6. Conveying Non-Source Forms. -You may convey a covered work in object code form under the terms of -sections 4 and 5, provided that you also convey the machine-readable -Corresponding Source under the terms of this License, in one of these -ways: + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: -- a) Convey the object code in, or embodied in, a physical product + a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. -- b) Convey the object code in, or embodied in, a physical product + + b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product @@ -265,190 +263,196 @@ ways: product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the Corresponding - Source from a network server at no charge. -- c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. -- d) Convey the object code by offering access from a designated + + d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the + Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. -- e) Convey the object code using peer-to-peer transmission, - provided you inform other peers where the object code and - Corresponding Source of the work are being offered to the general - public at no charge under subsection 6d. -A separable portion of the object code, whose source code is excluded + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. -A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, -family, or household purposes, or (2) anything designed or sold for -incorporation into a dwelling. In determining whether a product is a -consumer product, doubtful cases shall be resolved in favor of -coverage. For a particular product received by a particular user, -"normally used" refers to a typical or common use of that class of -product, regardless of the status of the particular user or of the way -in which the particular user actually uses, or expects or is expected -to use, the product. A product is a consumer product regardless of -whether the product has substantial commercial, industrial or -non-consumer uses, unless such uses represent the only significant -mode of use of the product. + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. -"Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to -install and execute modified versions of a covered work in that User -Product from a modified version of its Corresponding Source. The -information must suffice to ensure that the continued functioning of -the modified object code is in no case prevented or interfered with -solely because modification has been made. + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. -If you convey an object code work under this section in, or with, or + If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply +by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). -The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or -updates for a work that has been modified or installed by the -recipient, or for the User Product in which it has been modified or -installed. Access to a network may be denied when the modification -itself materially and adversely affects the operation of the network -or violates the rules and protocols for communication across the -network. + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. -Corresponding Source conveyed, and Installation Information provided, + Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. -#### 7. Additional Terms. + 7. Additional Terms. -"Additional permissions" are terms that supplement the terms of this + "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions +that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. -When you convey a copy of a covered work, you may at your option + When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. -Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders -of that material) supplement the terms of this License with terms: + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: -- a) Disclaiming warranty or limiting liability differently from the + a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or -- b) Requiring preservation of specified reasonable legal notices or + + b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or -- c) Prohibiting misrepresentation of the origin of that material, - or requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or -- d) Limiting the use for publicity purposes of names of licensors - or authors of the material; or -- e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or -- f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions - of it) with contractual assumptions of liability to the recipient, - for any liability that these contractual assumptions directly - impose on those licensors and authors. -All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains +restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. -If you add terms to a covered work in accord with this section, you + If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. -Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; the -above requirements apply either way. + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. -#### 8. Termination. + 8. Termination. -You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). -However, if you cease all violation of this License, then your license -from a particular copyright holder is reinstated (a) provisionally, -unless and until the copyright holder explicitly and finally -terminates your license, and (b) permanently, if the copyright holder -fails to notify you of the violation by some reasonable means prior to -60 days after the cessation. + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. -Moreover, your license from a particular copyright holder is + Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. -Termination of your rights under this section does not terminate the + Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently +this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. -#### 9. Acceptance Not Required for Having Copies. + 9. Acceptance Not Required for Having Copies. -You are not required to accept this License in order to receive or run -a copy of the Program. Ancillary propagation of a covered work + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, +to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. -#### 10. Automatic Licensing of Downstream Recipients. + 10. Automatic Licensing of Downstream Recipients. -Each time you convey a covered work, the recipient automatically + Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible +propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. -An "entity transaction" is a transaction transferring control of an + An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered +organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could @@ -456,43 +460,43 @@ give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. -You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. -#### 11. Patents. + 11. Patents. -A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". -A contributor's "essential patent claims" are all patent claims owned -or controlled by the contributor, whether already acquired or + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For +consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. -Each contributor grants you a non-exclusive, worldwide, royalty-free + Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. -In the following three paragraphs, a "patent license" is any express + In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a +sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. -If you convey a covered work, knowingly relying on a patent license, + If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, @@ -500,13 +504,13 @@ then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have +license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. -If, pursuant to or in connection with a single transaction or + If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify @@ -514,162 +518,157 @@ or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. -A patent license is "discriminatory" if it does not include within the -scope of its coverage, prohibits the exercise of, or is conditioned on -the non-exercise of one or more of the rights that are specifically -granted under this License. You may not convey a covered work if you -are a party to an arrangement with a third party that is in the -business of distributing software, under which you make payment to the -third party based on the extent of your activity of conveying the -work, and under which the third party grants, to any of the parties -who would receive the covered work from you, a discriminatory patent -license (a) in connection with copies of the covered work conveyed by -you (or copies made from those copies), or (b) primarily for and in -connection with specific products or compilations that contain the -covered work, unless you entered into that arrangement, or that patent -license was granted, prior to 28 March 2007. + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. -Nothing in this License shall be construed as excluding or limiting + Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. -#### 12. No Surrender of Others' Freedom. + 12. No Surrender of Others' Freedom. -If conditions are imposed on you (whether by court order, agreement or + If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under -this License and any other pertinent obligations, then as a -consequence you may not convey it at all. For example, if you agree to -terms that obligate you to collect a royalty for further conveying -from those to whom you convey the Program, the only way you could -satisfy both those terms and this License would be to refrain entirely -from conveying the Program. +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. -#### 13. Use with the GNU Affero General Public License. + 13. Use with the GNU Affero General Public License. -Notwithstanding any other provision of this License, you have + Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this +combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. -#### 14. Revised Versions of this License. + 14. Revised Versions of this License. -The Free Software Foundation may publish revised and/or new versions -of the GNU General Public License from time to time. Such new versions -will be similar in spirit to the present version, but may differ in -detail to address new problems or concerns. + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. -Each version is given a distinguishing version number. If the Program -specifies that a certain numbered version of the GNU General Public -License "or any later version" applies to it, you have the option of -following the terms and conditions either of that numbered version or -of any later version published by the Free Software Foundation. If the -Program does not specify a version number of the GNU General Public -License, you may choose any version ever published by the Free -Software Foundation. + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. -If the Program specifies that a proxy can decide which future versions -of the GNU General Public License can be used, that proxy's public -statement of acceptance of a version permanently authorizes you to -choose that version for the Program. + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. -Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. -#### 15. Disclaimer of Warranty. + 15. Disclaimer of Warranty. -THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT -WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND -PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE -DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR -CORRECTION. + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. -#### 16. Limitation of Liability. + 16. Limitation of Liability. -IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR -CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, -INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES -ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT -NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR -LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM -TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER -PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. -#### 17. Interpretation of Sections 15 and 16. + 17. Interpretation of Sections 15 and 16. -If the disclaimer of warranty and limitation of liability provided + If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. -END OF TERMS AND CONDITIONS + END OF TERMS AND CONDITIONS -### How to Apply These Terms to Your New Programs + How to Apply These Terms to Your New Programs -If you develop a new program, and you want it to be of the greatest + If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these -terms. +free software which everyone can redistribute and change under these terms. -To do so, attach the following notices to the program. It is safest to -attach them to the start of each source file to most effectively state -the exclusion of warranty; and each file should have at least the -"copyright" line and a pointer to where the full notice is found. + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. - - Copyright (C) + + Copyright (C) - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. - You should have received a copy of the GNU General Public License - along with this program. If not, see . + You should have received a copy of the GNU General Public License + along with this program. If not, see . -Also add information on how to contact you by electronic and paper -mail. +Also add information on how to contact you by electronic and paper mail. -If the program does terminal interaction, make it output a short + If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. -The hypothetical commands \`show w' and \`show c' should show the -appropriate parts of the General Public License. Of course, your -program's commands might be different; for a GUI interface, you would -use an "about box". +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". -You should also get your employer (if you work as a programmer) or -institute, if any, to sign a "copyright disclaimer" for the program, if -necessary. For more information on this, and how to apply and follow -the GNU GPL, see . + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. -The GNU General Public License does not permit incorporating your -program into proprietary programs. If your program is a subroutine -library, you may consider it more useful to permit linking proprietary -applications with the library. If this is what you want to do, use the -GNU Lesser General Public License instead of this License. But first, -please read . \ No newline at end of file + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/pyproject.toml b/pyproject.toml index 5acfd39272..1c93eed651 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,17 +10,18 @@ dynamic = ["version"] dependencies = [ # Core dependencies "pycountry~=20.7.3", - "python-stdnum~=1.16", "Unidecode~=1.2.0", - "redisearch~=2.1.0", + "barcodenumber~=0.5.0", # integration dependencies "gocardless-pro~=1.22.0", "googlemaps", "plaid-python~=7.2.1", "python-youtube~=0.8.0", - "taxjar~=1.9.2", "tweepy~=3.10.0", + + # Not used directly - required by PyQRCode for PNG generation + "pypng~=0.20220715.0", ] [build-system]