Merge remote-tracking branch 'upstream/develop' into remove-india
This commit is contained in:
commit
ec9861266e
@ -5,7 +5,7 @@
|
||||
"es6": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 9,
|
||||
"ecmaVersion": 11,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"extends": "eslint:recommended",
|
||||
|
||||
3
.flake8
3
.flake8
@ -29,6 +29,9 @@ ignore =
|
||||
B950,
|
||||
W191,
|
||||
E124, # closing bracket, irritating while writing QB code
|
||||
E131, # continuation line unaligned for hanging indent
|
||||
E123, # closing bracket does not match indentation of opening bracket's line
|
||||
E101, # ensured by use of black
|
||||
|
||||
max-line-length = 200
|
||||
exclude=.github/helper/semgrep_rules
|
||||
|
||||
@ -23,3 +23,9 @@ b147b85e6ac19a9220cd1e2958a6ebd99373283a
|
||||
|
||||
# removing six compatibility layer
|
||||
8fe5feb6a4372bf5f2dfaf65fca41bbcc25c8ce7
|
||||
|
||||
# bulk format python code with black
|
||||
494bd9ef78313436f0424b918f200dab8fc7c20b
|
||||
|
||||
# bulk format python code with black
|
||||
baec607ff5905b1c67531096a9cf50ec7ff00a5d
|
||||
12
.github/ISSUE_TEMPLATE/feature_request.md
vendored
12
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -12,10 +12,18 @@ Welcome to ERPNext issue tracker! Before creating an issue, please heed the foll
|
||||
|
||||
1. This tracker should only be used to report bugs and request features / enhancements to ERPNext
|
||||
- For questions and general support, checkout the manual https://erpnext.com/docs/user/manual/en or use https://discuss.erpnext.com
|
||||
- For documentation issues, refer to https://github.com/frappe/erpnext_com
|
||||
2. Use the search function before creating a new issue. Duplicates will be closed and directed to
|
||||
the original discussion.
|
||||
3. When making a feature request, make sure to be as verbose as possible. The better you convey your message, the greater the drive to make it happen.
|
||||
3. When making a feature request, make sure to be as verbose as possible. The better you convey your message, the greater the drive to make it happen.
|
||||
|
||||
|
||||
Please keep in mind that we get many many requests and we can't possibly work on all of them, we prioritize development based on the goals of the product and organization. Feature requests are still welcome as it helps us in research when we do decide to work on the requested feature.
|
||||
|
||||
If you're in urgent need to a feature, please try the following channels to get paid developments done quickly:
|
||||
1. Certified ERPNext partners: https://erpnext.com/partners
|
||||
2. Developer community on ERPNext forums: https://discuss.erpnext.com/c/developers/5
|
||||
3. Telegram group for ERPNext/Frappe development work: https://t.me/erpnext_opps
|
||||
|
||||
-->
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
|
||||
9
.github/helper/install.sh
vendored
9
.github/helper/install.sh
vendored
@ -2,9 +2,16 @@
|
||||
|
||||
set -e
|
||||
|
||||
# Check for merge conflicts before proceeding
|
||||
python -m compileall -f "${GITHUB_WORKSPACE}"
|
||||
if grep -lr --exclude-dir=node_modules "^<<<<<<< " "${GITHUB_WORKSPACE}"
|
||||
then echo "Found merge conflicts"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd ~ || exit
|
||||
|
||||
sudo apt-get install redis-server libcups2-dev
|
||||
sudo apt update && sudo apt install redis-server libcups2-dev
|
||||
|
||||
pip install frappe-bench
|
||||
|
||||
|
||||
12
.github/stale.yml
vendored
12
.github/stale.yml
vendored
@ -24,14 +24,4 @@ pulls:
|
||||
:) Also, even if it is closed, you can always reopen the PR when you're
|
||||
ready. Thank you for contributing.
|
||||
|
||||
issues:
|
||||
daysUntilStale: 60
|
||||
daysUntilClose: 7
|
||||
exemptLabels:
|
||||
- valid
|
||||
- to-validate
|
||||
- QA
|
||||
markComment: >
|
||||
This issue has been automatically marked as inactive because it has not had
|
||||
recent activity and it wasn't validated by maintainer team. It will be
|
||||
closed within a week if no further activity occurs.
|
||||
only: pulls
|
||||
|
||||
2
.github/try-on-f-cloud-button.svg
vendored
2
.github/try-on-f-cloud-button.svg
vendored
@ -1,4 +1,4 @@
|
||||
<svg width="201" height="60" viewBox="0 0 201 60" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="4 2 193 52">
|
||||
<g filter="url(#filter0_dd)">
|
||||
<rect x="4" y="2" width="193" height="52" rx="6" fill="#2490EF"/>
|
||||
<path d="M28 22.2891H32.8786V35.5H36.2088V22.2891H41.0874V19.5H28V22.2891Z" fill="white"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.3 KiB |
4
.github/workflows/patch.yml
vendored
4
.github/workflows/patch.yml
vendored
@ -4,7 +4,10 @@ on:
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- '**.js'
|
||||
- '**.css'
|
||||
- '**.md'
|
||||
- '**.html'
|
||||
- '**.csv'
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
@ -112,4 +115,5 @@ jobs:
|
||||
echo "Updating to latest version"
|
||||
git -C "apps/frappe" checkout -q -f "${GITHUB_BASE_REF:-${GITHUB_REF##*/}}"
|
||||
git -C "apps/erpnext" checkout -q -f "$GITHUB_SHA"
|
||||
bench setup requirements --python
|
||||
bench --site test_site migrate
|
||||
|
||||
31
.github/workflows/release.yml
vendored
Normal file
31
.github/workflows/release.yml
vendored
Normal file
@ -0,0 +1,31 @@
|
||||
name: Generate Semantic Release
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- version-13
|
||||
jobs:
|
||||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Entire Repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- name: Setup Node.js v14
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 14
|
||||
- name: Setup dependencies
|
||||
run: |
|
||||
npm install @semantic-release/git @semantic-release/exec --no-save
|
||||
- name: Create Release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
GIT_AUTHOR_NAME: "Frappe PR Bot"
|
||||
GIT_AUTHOR_EMAIL: "developers@frappe.io"
|
||||
GIT_COMMITTER_NAME: "Frappe PR Bot"
|
||||
GIT_COMMITTER_EMAIL: "developers@frappe.io"
|
||||
run: npx semantic-release
|
||||
20
.github/workflows/server-tests-mariadb.yml
vendored
20
.github/workflows/server-tests-mariadb.yml
vendored
@ -4,8 +4,10 @@ on:
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- '**.js'
|
||||
- '**.css'
|
||||
- '**.md'
|
||||
- '**.html'
|
||||
- '**.csv'
|
||||
push:
|
||||
branches: [ develop ]
|
||||
paths-ignore:
|
||||
@ -116,10 +118,26 @@ jobs:
|
||||
CI_BUILD_ID: ${{ github.run_id }}
|
||||
ORCHESTRATOR_URL: http://test-orchestrator.frappe.io
|
||||
|
||||
- name: Upload coverage data
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: coverage-${{ matrix.container }}
|
||||
path: /home/runner/frappe-bench/sites/coverage.xml
|
||||
|
||||
coverage:
|
||||
name: Coverage Wrap Up
|
||||
needs: test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Clone
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
|
||||
- name: Upload coverage data
|
||||
uses: codecov/codecov-action@v2
|
||||
with:
|
||||
name: MariaDB
|
||||
fail_ci_if_error: true
|
||||
files: /home/runner/frappe-bench/sites/coverage.xml
|
||||
verbose: true
|
||||
|
||||
117
.github/workflows/ui-tests.yml
vendored
117
.github/workflows/ui-tests.yml
vendored
@ -1,117 +0,0 @@
|
||||
name: UI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ui-develop-${{ github.event.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
name: UI Tests (Cypress)
|
||||
|
||||
services:
|
||||
mysql:
|
||||
image: mariadb:10.3
|
||||
env:
|
||||
MYSQL_ALLOW_EMPTY_PASSWORD: YES
|
||||
ports:
|
||||
- 3306:3306
|
||||
options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
|
||||
|
||||
steps:
|
||||
- name: Clone
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.8
|
||||
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 14
|
||||
check-latest: true
|
||||
|
||||
- name: Add to Hosts
|
||||
run: |
|
||||
echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
|
||||
|
||||
- name: Cache pip
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
${{ runner.os }}-
|
||||
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v2
|
||||
env:
|
||||
cache-name: cache-node-modules
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-build-${{ env.cache-name }}-
|
||||
${{ runner.os }}-build-
|
||||
${{ runner.os }}-
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- uses: actions/cache@v2
|
||||
id: yarn-cache
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
|
||||
- name: Cache cypress binary
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.cache
|
||||
key: ${{ runner.os }}-cypress-
|
||||
restore-keys: |
|
||||
${{ runner.os }}-cypress-
|
||||
${{ runner.os }}-
|
||||
|
||||
- name: Install
|
||||
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
|
||||
env:
|
||||
DB: mariadb
|
||||
TYPE: ui
|
||||
|
||||
- name: Site Setup
|
||||
run: cd ~/frappe-bench/ && bench --site test_site execute erpnext.setup.utils.before_tests
|
||||
|
||||
- name: cypress pre-requisites
|
||||
run: cd ~/frappe-bench/apps/frappe && yarn add cypress-file-upload@^5 @testing-library/cypress@^8 --no-lockfile
|
||||
|
||||
|
||||
- name: Build Assets
|
||||
run: cd ~/frappe-bench/ && bench build
|
||||
env:
|
||||
CI: Yes
|
||||
|
||||
- name: UI Tests
|
||||
run: cd ~/frappe-bench/ && bench --site test_site run-ui-tests erpnext --headless
|
||||
env:
|
||||
CYPRESS_RECORD_KEY: 60a8e3bf-08f5-45b1-9269-2b207d7d30cd
|
||||
|
||||
- name: Show bench console if tests failed
|
||||
if: ${{ failure() }}
|
||||
run: cat ~/frappe-bench/bench_run_logs.txt
|
||||
48
.mergify.yml
48
.mergify.yml
@ -7,6 +7,10 @@ pull_request_rules:
|
||||
- author!=gavindsouza
|
||||
- author!=rohitwaghchaure
|
||||
- author!=nabinhait
|
||||
- author!=ankush
|
||||
- author!=deepeshgarg007
|
||||
- author!=mergify[bot]
|
||||
|
||||
- or:
|
||||
- base=version-13
|
||||
- base=version-12
|
||||
@ -17,6 +21,16 @@ pull_request_rules:
|
||||
@{{author}}, thanks for the contribution, but we do not accept pull requests on a stable branch. Please raise PR on an appropriate hotfix branch.
|
||||
https://github.com/frappe/erpnext/wiki/Pull-Request-Checklist#which-branch
|
||||
|
||||
- name: Auto-close PRs on pre-release branch
|
||||
conditions:
|
||||
- base=version-13-pre-release
|
||||
actions:
|
||||
close:
|
||||
comment:
|
||||
message: |
|
||||
@{{author}}, pre-release branch is not maintained anymore. Releases are directly done by merging hotfix branch to stable branches.
|
||||
|
||||
|
||||
- name: backport to develop
|
||||
conditions:
|
||||
- label="backport develop"
|
||||
@ -86,3 +100,37 @@ pull_request_rules:
|
||||
- version-12-pre-release
|
||||
assignees:
|
||||
- "{{ author }}"
|
||||
|
||||
- name: Automatic merge on CI success and review
|
||||
conditions:
|
||||
- status-success=linters
|
||||
- status-success=Sider
|
||||
- status-success=Semantic Pull Request
|
||||
- status-success=Patch Test
|
||||
- status-success=Python Unit Tests (1)
|
||||
- status-success=Python Unit Tests (2)
|
||||
- status-success=Python Unit Tests (3)
|
||||
- label!=dont-merge
|
||||
- label!=squash
|
||||
- "#approved-reviews-by>=1"
|
||||
actions:
|
||||
merge:
|
||||
method: merge
|
||||
- name: Automatic squash on CI success and review
|
||||
conditions:
|
||||
- status-success=linters
|
||||
- status-success=Sider
|
||||
- status-success=Patch Test
|
||||
- status-success=Python Unit Tests (1)
|
||||
- status-success=Python Unit Tests (2)
|
||||
- status-success=Python Unit Tests (3)
|
||||
- label!=dont-merge
|
||||
- label=squash
|
||||
- "#approved-reviews-by>=1"
|
||||
actions:
|
||||
merge:
|
||||
method: squash
|
||||
commit_message_template: |
|
||||
{{ title }} (#{{ number }})
|
||||
|
||||
{{ body }}
|
||||
|
||||
@ -26,12 +26,19 @@ repos:
|
||||
args: ['--config', '.github/helper/.flake8_strict']
|
||||
exclude: ".*setup.py$"
|
||||
|
||||
- repo: https://github.com/adityahase/black
|
||||
rev: 9cb0a69f4d0030cdf687eddf314468b39ed54119
|
||||
hooks:
|
||||
- id: black
|
||||
additional_dependencies: ['click==8.0.4']
|
||||
|
||||
- repo: https://github.com/timothycrosley/isort
|
||||
rev: 5.9.1
|
||||
hooks:
|
||||
- id: isort
|
||||
exclude: ".*setup.py$"
|
||||
|
||||
|
||||
ci:
|
||||
autoupdate_schedule: weekly
|
||||
skip: []
|
||||
|
||||
24
.releaserc
Normal file
24
.releaserc
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"branches": ["version-13"],
|
||||
"plugins": [
|
||||
"@semantic-release/commit-analyzer", {
|
||||
"preset": "angular",
|
||||
"releaseRules": [
|
||||
{"breaking": true, "release": false}
|
||||
]
|
||||
},
|
||||
"@semantic-release/release-notes-generator",
|
||||
[
|
||||
"@semantic-release/exec", {
|
||||
"prepareCmd": 'sed -ir "s/[0-9]*\.[0-9]*\.[0-9]*/${nextRelease.version}/" erpnext/__init__.py'
|
||||
}
|
||||
],
|
||||
[
|
||||
"@semantic-release/git", {
|
||||
"assets": ["erpnext/__init__.py"],
|
||||
"message": "chore(release): Bumped to Version ${nextRelease.version}\n\n${nextRelease.notes}"
|
||||
}
|
||||
],
|
||||
"@semantic-release/github"
|
||||
]
|
||||
}
|
||||
79
README.md
79
README.md
@ -1,11 +1,14 @@
|
||||
<div align="center">
|
||||
<img src="https://raw.githubusercontent.com/frappe/erpnext/develop/erpnext/public/images/erpnext-logo.png" height="128">
|
||||
<a href="https://erpnext.com">
|
||||
<img src="https://raw.githubusercontent.com/frappe/erpnext/develop/erpnext/public/images/erpnext-logo.png" height="128">
|
||||
</a>
|
||||
<h2>ERPNext</h2>
|
||||
<p align="center">
|
||||
<p>ERP made simple</p>
|
||||
</p>
|
||||
|
||||
[](https://github.com/frappe/erpnext/actions/workflows/server-tests.yml)
|
||||
[](https://github.com/erpnext/erpnext_ui_tests/actions/workflows/ui-tests.yml)
|
||||
[](https://www.codetriage.com/frappe/erpnext)
|
||||
[](https://codecov.io/gh/frappe/erpnext)
|
||||
[](https://hub.docker.com/r/frappe/erpnext-worker)
|
||||
@ -31,40 +34,39 @@ ERPNext as a monolith includes the following areas for managing businesses:
|
||||
1. [Customize ERPNext](https://erpnext.com/docs/user/manual/en/customize-erpnext)
|
||||
1. [And More](https://erpnext.com/docs/user/manual/en/)
|
||||
|
||||
ERPNext requires MariaDB.
|
||||
|
||||
ERPNext is built on the [Frappe Framework](https://github.com/frappe/frappe), a full-stack web app framework built with Python & JavaScript.
|
||||
|
||||
- [User Guide](https://erpnext.com/docs/user)
|
||||
- [Discussion Forum](https://discuss.erpnext.com/)
|
||||
## Installation
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
<a href="https://frappecloud.com/deploy?apps=frappe,erpnext&source=erpnext_readme">
|
||||
<div align="center" style="max-height: 40px;">
|
||||
<a href="https://frappecloud.com/erpnext/signup">
|
||||
<img src=".github/try-on-f-cloud-button.svg" height="40">
|
||||
</a>
|
||||
<a href="https://labs.play-with-docker.com/?stack=https://raw.githubusercontent.com/frappe/frappe_docker/main/pwd.yml">
|
||||
<img src="https://raw.githubusercontent.com/play-with-docker/stacks/master/assets/images/button.png" alt="Try in PWD" height="37"/>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
> Login for the PWD site: (username: Administrator, password: admin)
|
||||
|
||||
### Containerized Installation
|
||||
|
||||
Use docker to deploy ERPNext in production or for development of [Frappe](https://github.com/frappe/frappe) apps. See https://github.com/frappe/frappe_docker for more details.
|
||||
|
||||
### Full Install
|
||||
### Manual Install
|
||||
|
||||
The Easy Way: our install script for bench will install all dependencies (e.g. MariaDB). See https://github.com/frappe/bench for more details.
|
||||
|
||||
New passwords will be created for the ERPNext "Administrator" user, the MariaDB root user, and the frappe user (the script displays the passwords and saves them to ~/frappe_passwords.txt).
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
## Learning and community
|
||||
|
||||
GNU/General Public License (see [license.txt](license.txt))
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## Contributing
|
||||
|
||||
@ -72,49 +74,14 @@ The ERPNext code is licensed as GNU General Public License (v3) and the Document
|
||||
1. [Report Security Vulnerabilities](https://erpnext.com/security)
|
||||
1. [Pull Request Requirements](https://github.com/frappe/erpnext/wiki/Contribution-Guidelines)
|
||||
1. [Translations](https://translate.erpnext.com)
|
||||
1. [Chart of Accounts](https://charts.erpnext.com)
|
||||
|
||||
---
|
||||
|
||||
## Learning
|
||||
## License
|
||||
|
||||
1. [Frappe School](https://frappe.school) - Learn Frappe Framework and ERPNext from the various courses by the maintainers or from the community.
|
||||
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.
|
||||
|
||||
## Logo and Trademark
|
||||
## Logo and Trademark Policy
|
||||
|
||||
The brand name ERPNext and the logo are trademarks of Frappe Technologies Pvt. Ltd.
|
||||
|
||||
### Introduction
|
||||
|
||||
Frappe Technologies Pvt. Ltd. (Frappe) owns and oversees the trademarks for the ERPNext name and logos. We have developed this trademark usage policy with the following goals in mind:
|
||||
|
||||
- We’d like to make it easy for anyone to use the ERPNext name or logo for community-oriented efforts that help spread and improve ERPNext.
|
||||
- We’d like to make it clear how ERPNext-related businesses and projects can (and cannot) use the ERPNext name and logo.
|
||||
- We’d like to make it hard for anyone to use the ERPNext name and logo to unfairly profit from, trick or confuse people who are looking for official ERPNext resources.
|
||||
|
||||
### Frappe Trademark Usage Policy
|
||||
|
||||
Permission from Frappe is required to use the ERPNext name or logo as part of any project, product, service, domain or company name.
|
||||
|
||||
We will grant permission to use the ERPNext name and logo for projects that meet the following criteria:
|
||||
|
||||
- The primary purpose of your project is to promote the spread and improvement of the ERPNext software.
|
||||
- Your project is non-commercial in nature (it can make money to cover its costs or contribute to non-profit entities, but it cannot be run as a for-profit project or business).
|
||||
Your project neither promotes nor is associated with entities that currently fail to comply with the GPL license under which ERPNext is distributed.
|
||||
- If your project meets these criteria, you will be permitted to use the ERPNext name and logo to promote your project in any way you see fit with one exception: Please do not use ERPNext as part of a domain name.
|
||||
|
||||
Use of the ERPNext name and logo is additionally allowed in the following situations:
|
||||
|
||||
All other ERPNext-related businesses or projects can use the ERPNext name and logo to refer to and explain their services, but they cannot use them as part of a product, project, service, domain, or company name and they cannot use them in any way that suggests an affiliation with or endorsement by ERPNext or Frappe Technologies or the ERPNext open source project. For example, a consulting company can describe its business as “123 Web Services, offering ERPNext consulting for small businesses,” but cannot call its business “The ERPNext Consulting Company.”
|
||||
|
||||
Similarly, it’s OK to use the ERPNext logo as part of a page that describes your products or services, but it is not OK to use it as part of your company or product logo or branding itself. Under no circumstances is it permitted to use ERPNext as part of a top-level domain name.
|
||||
|
||||
We do not allow the use of the trademark in advertising, including AdSense/AdWords.
|
||||
|
||||
Please note that it is not the goal of this policy to limit commercial activity around ERPNext. We encourage ERPNext-based businesses, and we would love to see hundreds of them.
|
||||
|
||||
When in doubt about your use of the ERPNext name or logo, please contact Frappe Technologies for clarification.
|
||||
|
||||
(inspired by WordPress)
|
||||
Please read our [Logo and Trademark Policy](TRADEMARK_POLICY.md).
|
||||
|
||||
36
TRADEMARK_POLICY.md
Normal file
36
TRADEMARK_POLICY.md
Normal file
@ -0,0 +1,36 @@
|
||||
## Logo and Trademark Policy
|
||||
|
||||
The brand name ERPNext and the logo are trademarks of Frappe Technologies Pvt. Ltd.
|
||||
|
||||
### Introduction
|
||||
|
||||
Frappe Technologies Pvt. Ltd. (Frappe) owns and oversees the trademarks for the ERPNext name and logos. We have developed this trademark usage policy with the following goals in mind:
|
||||
|
||||
- We’d like to make it easy for anyone to use the ERPNext name or logo for community-oriented efforts that help spread and improve ERPNext.
|
||||
- We’d like to make it clear how ERPNext-related businesses and projects can (and cannot) use the ERPNext name and logo.
|
||||
- We’d like to make it hard for anyone to use the ERPNext name and logo to unfairly profit from, trick or confuse people who are looking for official ERPNext resources.
|
||||
|
||||
### Frappe Trademark Usage Policy
|
||||
|
||||
Permission from Frappe is required to use the ERPNext name or logo as part of any project, product, service, domain or company name.
|
||||
|
||||
We will grant permission to use the ERPNext name and logo for projects that meet the following criteria:
|
||||
|
||||
- The primary purpose of your project is to promote the spread and improvement of the ERPNext software.
|
||||
- Your project is non-commercial in nature (it can make money to cover its costs or contribute to non-profit entities, but it cannot be run as a for-profit project or business).
|
||||
Your project neither promotes nor is associated with entities that currently fail to comply with the GPL license under which ERPNext is distributed.
|
||||
- If your project meets these criteria, you will be permitted to use the ERPNext name and logo to promote your project in any way you see fit with one exception: Please do not use ERPNext as part of a domain name.
|
||||
|
||||
Use of the ERPNext name and logo is additionally allowed in the following situations:
|
||||
|
||||
All other ERPNext-related businesses or projects can use the ERPNext name and logo to refer to and explain their services, but they cannot use them as part of a product, project, service, domain, or company name and they cannot use them in any way that suggests an affiliation with or endorsement by ERPNext or Frappe Technologies or the ERPNext open source project. For example, a consulting company can describe its business as “123 Web Services, offering ERPNext consulting for small businesses,” but cannot call its business “The ERPNext Consulting Company.”
|
||||
|
||||
Similarly, it’s OK to use the ERPNext logo as part of a page that describes your products or services, but it is not OK to use it as part of your company or product logo or branding itself. Under no circumstances is it permitted to use ERPNext as part of a top-level domain name.
|
||||
|
||||
We do not allow the use of the trademark in advertising, including AdSense/AdWords.
|
||||
|
||||
Please note that it is not the goal of this policy to limit commercial activity around ERPNext. We encourage ERPNext-based businesses, and we would love to see hundreds of them.
|
||||
|
||||
When in doubt about your use of the ERPNext name or logo, please contact Frappe Technologies for clarification.
|
||||
|
||||
(inspired by WordPress)
|
||||
@ -21,7 +21,6 @@ coverage:
|
||||
comment:
|
||||
layout: "diff, files"
|
||||
require_changes: true
|
||||
after_n_builds: 3
|
||||
|
||||
ignore:
|
||||
- "erpnext/demo"
|
||||
|
||||
11
cypress.json
11
cypress.json
@ -1,11 +0,0 @@
|
||||
{
|
||||
"baseUrl": "http://test_site:8000/",
|
||||
"projectId": "da59y9",
|
||||
"adminPassword": "admin",
|
||||
"defaultCommandTimeout": 20000,
|
||||
"pageLoadTimeout": 15000,
|
||||
"retries": {
|
||||
"runMode": 2,
|
||||
"openMode": 2
|
||||
}
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
{
|
||||
"name": "Using fixtures to represent data",
|
||||
"email": "hello@cypress.io",
|
||||
"body": "Fixtures are a great way to mock data for responses to routes"
|
||||
}
|
||||
@ -1,44 +0,0 @@
|
||||
describe("Bulk Transaction Processing", () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit("/app/website");
|
||||
});
|
||||
|
||||
it("Creates To Sales Order", () => {
|
||||
cy.visit("/app/sales-order");
|
||||
cy.url().should("include", "/sales-order");
|
||||
cy.window()
|
||||
.its("frappe.csrf_token")
|
||||
.then((csrf_token) => {
|
||||
return cy
|
||||
.request({
|
||||
url: "/api/method/erpnext.tests.ui_test_bulk_transaction_processing.create_records",
|
||||
method: "POST",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
"X-Frappe-CSRF-Token": csrf_token,
|
||||
},
|
||||
timeout: 60000,
|
||||
})
|
||||
.then((res) => {
|
||||
expect(res.status).eq(200);
|
||||
});
|
||||
});
|
||||
cy.wait(5000);
|
||||
cy.get(
|
||||
".list-row-head > .list-header-subject > .list-row-col > .list-check-all"
|
||||
).check({ force: true });
|
||||
cy.wait(3000);
|
||||
cy.get(".actions-btn-group > .btn-primary").click({ force: true });
|
||||
cy.wait(3000);
|
||||
cy.get(".dropdown-menu-right > .user-action > .dropdown-item")
|
||||
.contains("Sales Invoice")
|
||||
.click({ force: true });
|
||||
cy.wait(3000);
|
||||
cy.get(".modal-content > .modal-footer > .standard-actions")
|
||||
.contains("Yes")
|
||||
.click({ force: true });
|
||||
cy.contains("Creation of Sales Invoice successful");
|
||||
});
|
||||
});
|
||||
@ -1,13 +0,0 @@
|
||||
|
||||
context('Customer', () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
});
|
||||
it('Check Customer Group', () => {
|
||||
cy.visit(`app/customer/`);
|
||||
cy.get('.primary-action').click();
|
||||
cy.wait(500);
|
||||
cy.get('.custom-actions > .btn').click();
|
||||
cy.get_field('customer_group', 'Link').should('have.value', 'All Customer Groups');
|
||||
});
|
||||
});
|
||||
@ -1,44 +0,0 @@
|
||||
describe("Test Item Dashboard", () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit("/app/item");
|
||||
cy.insert_doc(
|
||||
"Item",
|
||||
{
|
||||
item_code: "e2e_test_item",
|
||||
item_group: "All Item Groups",
|
||||
opening_stock: 42,
|
||||
valuation_rate: 100,
|
||||
},
|
||||
true
|
||||
);
|
||||
cy.go_to_doc("item", "e2e_test_item");
|
||||
});
|
||||
|
||||
it("should show dashboard with correct data on first load", () => {
|
||||
cy.get(".stock-levels").contains("Stock Levels").should("be.visible");
|
||||
cy.get(".stock-levels").contains("e2e_test_item").should("exist");
|
||||
|
||||
// reserved and available qty
|
||||
cy.get(".stock-levels .inline-graph-count")
|
||||
.eq(0)
|
||||
.contains("0")
|
||||
.should("exist");
|
||||
cy.get(".stock-levels .inline-graph-count")
|
||||
.eq(1)
|
||||
.contains("42")
|
||||
.should("exist");
|
||||
});
|
||||
|
||||
it("should persist on field change", () => {
|
||||
cy.get('input[data-fieldname="disabled"]').check();
|
||||
cy.wait(500);
|
||||
cy.get(".stock-levels").contains("Stock Levels").should("be.visible");
|
||||
cy.get(".stock-levels").should("have.length", 1);
|
||||
});
|
||||
|
||||
it("should persist on reload", () => {
|
||||
cy.reload();
|
||||
cy.get(".stock-levels").contains("Stock Levels").should("be.visible");
|
||||
});
|
||||
});
|
||||
@ -1,116 +0,0 @@
|
||||
context('Organizational Chart', () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/app/website');
|
||||
});
|
||||
|
||||
it('navigates to org chart', () => {
|
||||
cy.visit('/app');
|
||||
cy.visit('/app/organizational-chart');
|
||||
cy.url().should('include', '/organizational-chart');
|
||||
|
||||
cy.window().its('frappe.csrf_token').then(csrf_token => {
|
||||
return cy.request({
|
||||
url: `/api/method/erpnext.tests.ui_test_helpers.create_employee_records`,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'X-Frappe-CSRF-Token': csrf_token
|
||||
},
|
||||
timeout: 60000
|
||||
}).then(res => {
|
||||
expect(res.status).eq(200);
|
||||
cy.get('.frappe-control[data-fieldname=company] input').focus().as('input');
|
||||
cy.get('@input')
|
||||
.clear({ force: true })
|
||||
.type('Test Org Chart{downarrow}{enter}', { force: true })
|
||||
.blur({ force: true });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('renders root nodes and loads children for the first expandable node', () => {
|
||||
// check rendered root nodes and the node name, title, connections
|
||||
cy.get('.hierarchy').find('.root-level ul.node-children').children()
|
||||
.should('have.length', 2)
|
||||
.first()
|
||||
.as('first-child');
|
||||
|
||||
cy.get('@first-child').get('.node-name').contains('Test Employee 1');
|
||||
cy.get('@first-child').get('.node-info').find('.node-title').contains('CEO');
|
||||
cy.get('@first-child').get('.node-info').find('.node-connections').contains('· 2 Connections');
|
||||
|
||||
cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => {
|
||||
// children of 1st root visible
|
||||
cy.get(`div[data-parent="${employee_records.message[0]}"]`).as('child-node');
|
||||
cy.get('@child-node')
|
||||
.should('have.length', 1)
|
||||
.should('be.visible');
|
||||
cy.get('@child-node').get('.node-name').contains('Test Employee 3');
|
||||
|
||||
// connectors between first root node and immediate child
|
||||
cy.get(`path[data-parent="${employee_records.message[0]}"]`)
|
||||
.should('be.visible')
|
||||
.invoke('attr', 'data-child')
|
||||
.should('equal', employee_records.message[2]);
|
||||
});
|
||||
});
|
||||
|
||||
it('hides active nodes children and connectors on expanding sibling node', () => {
|
||||
cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => {
|
||||
// click sibling
|
||||
cy.get(`#${employee_records.message[1]}`)
|
||||
.click()
|
||||
.should('have.class', 'active');
|
||||
|
||||
// child nodes and connectors hidden
|
||||
cy.get(`[data-parent="${employee_records.message[0]}"]`).should('not.be.visible');
|
||||
cy.get(`path[data-parent="${employee_records.message[0]}"]`).should('not.be.visible');
|
||||
});
|
||||
});
|
||||
|
||||
it('collapses previous level nodes and refreshes connectors on expanding child node', () => {
|
||||
cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => {
|
||||
// click child node
|
||||
cy.get(`#${employee_records.message[3]}`)
|
||||
.click()
|
||||
.should('have.class', 'active');
|
||||
|
||||
// previous level nodes: parent should be on active-path; other nodes should be collapsed
|
||||
cy.get(`#${employee_records.message[0]}`).should('have.class', 'collapsed');
|
||||
cy.get(`#${employee_records.message[1]}`).should('have.class', 'active-path');
|
||||
|
||||
// previous level connectors refreshed
|
||||
cy.get(`path[data-parent="${employee_records.message[1]}"]`)
|
||||
.should('have.class', 'collapsed-connector');
|
||||
|
||||
// child node's children and connectors rendered
|
||||
cy.get(`[data-parent="${employee_records.message[3]}"]`).should('be.visible');
|
||||
cy.get(`path[data-parent="${employee_records.message[3]}"]`).should('be.visible');
|
||||
});
|
||||
});
|
||||
|
||||
it('expands previous level nodes', () => {
|
||||
cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => {
|
||||
cy.get(`#${employee_records.message[0]}`)
|
||||
.click()
|
||||
.should('have.class', 'active');
|
||||
|
||||
cy.get(`[data-parent="${employee_records.message[0]}"]`)
|
||||
.should('be.visible');
|
||||
|
||||
cy.get('ul.hierarchy').children().should('have.length', 2);
|
||||
cy.get(`#connectors`).children().should('have.length', 1);
|
||||
});
|
||||
});
|
||||
|
||||
it('edit node navigates to employee master', () => {
|
||||
cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => {
|
||||
cy.get(`#${employee_records.message[0]}`).find('.btn-edit-node')
|
||||
.click();
|
||||
|
||||
cy.url().should('include', `/employee/${employee_records.message[0]}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -1,195 +0,0 @@
|
||||
context('Organizational Chart Mobile', () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/app/website');
|
||||
});
|
||||
|
||||
it('navigates to org chart', () => {
|
||||
cy.viewport(375, 667);
|
||||
cy.visit('/app');
|
||||
cy.visit('/app/organizational-chart');
|
||||
cy.url().should('include', '/organizational-chart');
|
||||
|
||||
cy.window().its('frappe.csrf_token').then(csrf_token => {
|
||||
return cy.request({
|
||||
url: `/api/method/erpnext.tests.ui_test_helpers.create_employee_records`,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'X-Frappe-CSRF-Token': csrf_token
|
||||
},
|
||||
timeout: 60000
|
||||
}).then(res => {
|
||||
expect(res.status).eq(200);
|
||||
cy.get('.frappe-control[data-fieldname=company] input').focus().as('input');
|
||||
cy.get('@input')
|
||||
.clear({ force: true })
|
||||
.type('Test Org Chart{downarrow}{enter}', { force: true })
|
||||
.blur({ force: true });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('renders root nodes', () => {
|
||||
// check rendered root nodes and the node name, title, connections
|
||||
cy.get('.hierarchy-mobile').find('.root-level').children()
|
||||
.should('have.length', 2)
|
||||
.first()
|
||||
.as('first-child');
|
||||
|
||||
cy.get('@first-child').get('.node-name').contains('Test Employee 1');
|
||||
cy.get('@first-child').get('.node-info').find('.node-title').contains('CEO');
|
||||
cy.get('@first-child').get('.node-info').find('.node-connections').contains('· 2');
|
||||
});
|
||||
|
||||
it('expands root node', () => {
|
||||
cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => {
|
||||
cy.get(`#${employee_records.message[1]}`)
|
||||
.click()
|
||||
.should('have.class', 'active');
|
||||
|
||||
// other root node removed
|
||||
cy.get(`#${employee_records.message[0]}`).should('not.exist');
|
||||
|
||||
// children of active root node
|
||||
cy.get('.hierarchy-mobile').find('.level').first().find('ul.node-children').children()
|
||||
.should('have.length', 2);
|
||||
|
||||
cy.get(`div[data-parent="${employee_records.message[1]}"]`).first().as('child-node');
|
||||
cy.get('@child-node').should('be.visible');
|
||||
|
||||
cy.get('@child-node')
|
||||
.get('.node-name')
|
||||
.contains('Test Employee 4');
|
||||
|
||||
// connectors between root node and immediate children
|
||||
cy.get(`path[data-parent="${employee_records.message[1]}"]`).as('connectors');
|
||||
cy.get('@connectors')
|
||||
.should('have.length', 2)
|
||||
.should('be.visible');
|
||||
|
||||
cy.get('@connectors')
|
||||
.first()
|
||||
.invoke('attr', 'data-child')
|
||||
.should('eq', employee_records.message[3]);
|
||||
});
|
||||
});
|
||||
|
||||
it('expands child node', () => {
|
||||
cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => {
|
||||
cy.get(`#${employee_records.message[3]}`)
|
||||
.click()
|
||||
.should('have.class', 'active')
|
||||
.as('expanded_node');
|
||||
|
||||
// 2 levels on screen; 1 on active path; 1 collapsed
|
||||
cy.get('.hierarchy-mobile').children().should('have.length', 2);
|
||||
cy.get(`#${employee_records.message[1]}`).should('have.class', 'active-path');
|
||||
|
||||
// children of expanded node visible
|
||||
cy.get('@expanded_node')
|
||||
.next()
|
||||
.should('have.class', 'node-children')
|
||||
.as('node-children');
|
||||
|
||||
cy.get('@node-children').children().should('have.length', 1);
|
||||
cy.get('@node-children')
|
||||
.first()
|
||||
.get('.node-card')
|
||||
.should('have.class', 'active-child')
|
||||
.contains('Test Employee 7');
|
||||
|
||||
// orphan connectors removed
|
||||
cy.get(`#connectors`).children().should('have.length', 2);
|
||||
});
|
||||
});
|
||||
|
||||
it('renders sibling group', () => {
|
||||
cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => {
|
||||
// sibling group visible for parent
|
||||
cy.get(`#${employee_records.message[1]}`)
|
||||
.next()
|
||||
.as('sibling_group');
|
||||
|
||||
cy.get('@sibling_group')
|
||||
.should('have.attr', 'data-parent', 'undefined')
|
||||
.should('have.class', 'node-group')
|
||||
.and('have.class', 'collapsed');
|
||||
|
||||
cy.get('@sibling_group').get('.avatar-group').children().as('siblings');
|
||||
cy.get('@siblings').should('have.length', 1);
|
||||
cy.get('@siblings')
|
||||
.first()
|
||||
.should('have.attr', 'title', 'Test Employee 1');
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
it('expands previous level nodes', () => {
|
||||
cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => {
|
||||
cy.get(`#${employee_records.message[6]}`)
|
||||
.click()
|
||||
.should('have.class', 'active');
|
||||
|
||||
// clicking on previous level node should remove all the nodes ahead
|
||||
// and expand that node
|
||||
cy.get(`#${employee_records.message[3]}`).click();
|
||||
cy.get(`#${employee_records.message[3]}`)
|
||||
.should('have.class', 'active')
|
||||
.should('not.have.class', 'active-path');
|
||||
|
||||
cy.get(`#${employee_records.message[6]}`).should('have.class', 'active-child');
|
||||
cy.get('.hierarchy-mobile').children().should('have.length', 2);
|
||||
cy.get(`#connectors`).children().should('have.length', 2);
|
||||
});
|
||||
});
|
||||
|
||||
it('expands sibling group', () => {
|
||||
cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => {
|
||||
// sibling group visible for parent
|
||||
cy.get(`#${employee_records.message[6]}`).click();
|
||||
|
||||
cy.get(`#${employee_records.message[3]}`)
|
||||
.next()
|
||||
.click();
|
||||
|
||||
// siblings of parent should be visible
|
||||
cy.get('.hierarchy-mobile').prev().as('sibling_group');
|
||||
cy.get('@sibling_group')
|
||||
.should('exist')
|
||||
.should('have.class', 'sibling-group')
|
||||
.should('not.have.class', 'collapsed');
|
||||
|
||||
cy.get(`#${employee_records.message[1]}`)
|
||||
.should('be.visible')
|
||||
.should('have.class', 'active');
|
||||
|
||||
cy.get(`[data-parent="${employee_records.message[1]}"]`)
|
||||
.should('be.visible')
|
||||
.should('have.length', 2)
|
||||
.should('have.class', 'active-child');
|
||||
});
|
||||
});
|
||||
|
||||
it('goes to the respective level after clicking on non-collapsed sibling group', () => {
|
||||
cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(() => {
|
||||
// click on non-collapsed sibling group
|
||||
cy.get('.hierarchy-mobile')
|
||||
.prev()
|
||||
.click();
|
||||
|
||||
// should take you to that level
|
||||
cy.get('.hierarchy-mobile').find('li.level .node-card').should('have.length', 2);
|
||||
});
|
||||
});
|
||||
|
||||
it('edit node navigates to employee master', () => {
|
||||
cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => {
|
||||
cy.get(`#${employee_records.message[0]}`).find('.btn-edit-node')
|
||||
.click();
|
||||
|
||||
cy.url().should('include', `/employee/${employee_records.message[0]}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -1,17 +0,0 @@
|
||||
// ***********************************************************
|
||||
// This example plugins/index.js can be used to load plugins
|
||||
//
|
||||
// You can change the location of this file or turn off loading
|
||||
// the plugins file with the 'pluginsFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/plugins-guide
|
||||
// ***********************************************************
|
||||
|
||||
// This function is called when a project is opened or re-opened (e.g. due to
|
||||
// the project's config changing)
|
||||
|
||||
module.exports = () => {
|
||||
// `on` is used to hook into various events Cypress emits
|
||||
// `config` is the resolved Cypress config
|
||||
};
|
||||
@ -1,31 +0,0 @@
|
||||
// ***********************************************
|
||||
// This example commands.js shows you how to
|
||||
// create various custom commands and overwrite
|
||||
// existing commands.
|
||||
//
|
||||
// For more comprehensive examples of custom
|
||||
// commands please read more here:
|
||||
// https://on.cypress.io/custom-commands
|
||||
// ***********************************************
|
||||
//
|
||||
//
|
||||
// -- This is a parent command --
|
||||
// Cypress.Commands.add("login", (email, password) => { ... });
|
||||
//
|
||||
//
|
||||
// -- This is a child command --
|
||||
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... });
|
||||
//
|
||||
//
|
||||
// -- This is a dual command --
|
||||
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... });
|
||||
//
|
||||
//
|
||||
// -- This is will overwrite an existing command --
|
||||
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... });
|
||||
|
||||
const slug = (name) => name.toLowerCase().replace(" ", "-");
|
||||
|
||||
Cypress.Commands.add("go_to_doc", (doctype, name) => {
|
||||
cy.visit(`/app/${slug(doctype)}/${encodeURIComponent(name)}`);
|
||||
});
|
||||
@ -1,26 +0,0 @@
|
||||
// ***********************************************************
|
||||
// This example support/index.js is processed and
|
||||
// loaded automatically before your test files.
|
||||
//
|
||||
// This is a great place to put global configuration and
|
||||
// behavior that modifies Cypress.
|
||||
//
|
||||
// You can change the location of this file or turn off
|
||||
// automatically serving support files with the
|
||||
// 'supportFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/configuration
|
||||
// ***********************************************************
|
||||
|
||||
// Import commands.js using ES2015 syntax:
|
||||
import './commands';
|
||||
import '../../../frappe/cypress/support/commands' // eslint-disable-line
|
||||
|
||||
|
||||
// Alternatively you can use CommonJS syntax:
|
||||
// require('./commands')
|
||||
|
||||
Cypress.Cookies.defaults({
|
||||
preserve: 'sid'
|
||||
});
|
||||
@ -1,12 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"baseUrl": "../node_modules",
|
||||
"types": [
|
||||
"cypress"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.*"
|
||||
]
|
||||
}
|
||||
@ -2,49 +2,57 @@ import inspect
|
||||
|
||||
import frappe
|
||||
|
||||
__version__ = '14.0.0-dev'
|
||||
__version__ = "14.0.0-dev"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
'''Get default company for user'''
|
||||
"""Get default company for user"""
|
||||
from frappe.defaults import get_user_default_as_list
|
||||
|
||||
if not user:
|
||||
user = frappe.session.user
|
||||
|
||||
companies = get_user_default_as_list(user, 'company')
|
||||
companies = get_user_default_as_list(user, "company")
|
||||
if companies:
|
||||
default_company = companies[0]
|
||||
else:
|
||||
default_company = frappe.db.get_single_value('Global Defaults', 'default_company')
|
||||
default_company = frappe.db.get_single_value("Global Defaults", "default_company")
|
||||
|
||||
return default_company
|
||||
|
||||
|
||||
def get_default_currency():
|
||||
'''Returns the currency of the default company'''
|
||||
"""Returns the currency of the default company"""
|
||||
company = get_default_company()
|
||||
if company:
|
||||
return frappe.get_cached_value('Company', company, 'default_currency')
|
||||
return frappe.get_cached_value("Company", company, "default_currency")
|
||||
|
||||
|
||||
def get_default_cost_center(company):
|
||||
'''Returns the default cost center of the company'''
|
||||
"""Returns the default cost center of the company"""
|
||||
if not company:
|
||||
return None
|
||||
|
||||
if not frappe.flags.company_cost_center:
|
||||
frappe.flags.company_cost_center = {}
|
||||
if not company in frappe.flags.company_cost_center:
|
||||
frappe.flags.company_cost_center[company] = frappe.get_cached_value('Company', company, 'cost_center')
|
||||
frappe.flags.company_cost_center[company] = frappe.get_cached_value(
|
||||
"Company", company, "cost_center"
|
||||
)
|
||||
return frappe.flags.company_cost_center[company]
|
||||
|
||||
|
||||
def get_company_currency(company):
|
||||
'''Returns the default company currency'''
|
||||
"""Returns the default company currency"""
|
||||
if not frappe.flags.company_currency:
|
||||
frappe.flags.company_currency = {}
|
||||
if not company in frappe.flags.company_currency:
|
||||
frappe.flags.company_currency[company] = frappe.db.get_value('Company', company, 'default_currency', cache=True)
|
||||
frappe.flags.company_currency[company] = frappe.db.get_value(
|
||||
"Company", company, "default_currency", cache=True
|
||||
)
|
||||
return frappe.flags.company_currency[company]
|
||||
|
||||
|
||||
def set_perpetual_inventory(enable=1, company=None):
|
||||
if not company:
|
||||
company = "_Test Company" if frappe.flags.in_test else get_default_company()
|
||||
@ -53,9 +61,10 @@ def set_perpetual_inventory(enable=1, company=None):
|
||||
company.enable_perpetual_inventory = enable
|
||||
company.save()
|
||||
|
||||
|
||||
def encode_company_abbr(name, company=None, abbr=None):
|
||||
'''Returns name encoded with company abbreviation'''
|
||||
company_abbr = abbr or frappe.get_cached_value('Company', company, "abbr")
|
||||
"""Returns name encoded with company abbreviation"""
|
||||
company_abbr = abbr or frappe.get_cached_value("Company", company, "abbr")
|
||||
parts = name.rsplit(" - ", 1)
|
||||
|
||||
if parts[-1].lower() != company_abbr.lower():
|
||||
@ -63,62 +72,69 @@ def encode_company_abbr(name, company=None, abbr=None):
|
||||
|
||||
return " - ".join(parts)
|
||||
|
||||
|
||||
def is_perpetual_inventory_enabled(company):
|
||||
if not company:
|
||||
company = "_Test Company" if frappe.flags.in_test else get_default_company()
|
||||
|
||||
if not hasattr(frappe.local, 'enable_perpetual_inventory'):
|
||||
if not hasattr(frappe.local, "enable_perpetual_inventory"):
|
||||
frappe.local.enable_perpetual_inventory = {}
|
||||
|
||||
if not company in frappe.local.enable_perpetual_inventory:
|
||||
frappe.local.enable_perpetual_inventory[company] = frappe.get_cached_value('Company',
|
||||
company, "enable_perpetual_inventory") or 0
|
||||
frappe.local.enable_perpetual_inventory[company] = (
|
||||
frappe.get_cached_value("Company", company, "enable_perpetual_inventory") or 0
|
||||
)
|
||||
|
||||
return frappe.local.enable_perpetual_inventory[company]
|
||||
|
||||
|
||||
def get_default_finance_book(company=None):
|
||||
if not company:
|
||||
company = get_default_company()
|
||||
|
||||
if not hasattr(frappe.local, 'default_finance_book'):
|
||||
if not hasattr(frappe.local, "default_finance_book"):
|
||||
frappe.local.default_finance_book = {}
|
||||
|
||||
if not company in frappe.local.default_finance_book:
|
||||
frappe.local.default_finance_book[company] = frappe.get_cached_value('Company',
|
||||
company, "default_finance_book")
|
||||
frappe.local.default_finance_book[company] = frappe.get_cached_value(
|
||||
"Company", company, "default_finance_book"
|
||||
)
|
||||
|
||||
return frappe.local.default_finance_book[company]
|
||||
|
||||
|
||||
def get_party_account_type(party_type):
|
||||
if not hasattr(frappe.local, 'party_account_types'):
|
||||
if not hasattr(frappe.local, "party_account_types"):
|
||||
frappe.local.party_account_types = {}
|
||||
|
||||
if not party_type in frappe.local.party_account_types:
|
||||
frappe.local.party_account_types[party_type] = frappe.db.get_value("Party Type",
|
||||
party_type, "account_type") or ''
|
||||
frappe.local.party_account_types[party_type] = (
|
||||
frappe.db.get_value("Party Type", party_type, "account_type") or ""
|
||||
)
|
||||
|
||||
return frappe.local.party_account_types[party_type]
|
||||
|
||||
|
||||
def get_region(company=None):
|
||||
'''Return the default country based on flag, company or global settings
|
||||
"""Return the default country based on flag, company or global settings
|
||||
|
||||
You can also set global company flag in `frappe.flags.company`
|
||||
'''
|
||||
"""
|
||||
if company or frappe.flags.company:
|
||||
return frappe.get_cached_value('Company',
|
||||
company or frappe.flags.company, 'country')
|
||||
return frappe.get_cached_value("Company", company or frappe.flags.company, "country")
|
||||
elif frappe.flags.country:
|
||||
return frappe.flags.country
|
||||
else:
|
||||
return frappe.get_system_settings('country')
|
||||
return frappe.get_system_settings("country")
|
||||
|
||||
|
||||
def allow_regional(fn):
|
||||
'''Decorator to make a function regionally overridable
|
||||
"""Decorator to make a function regionally overridable
|
||||
|
||||
Example:
|
||||
@erpnext.allow_regional
|
||||
def myfunction():
|
||||
pass'''
|
||||
pass"""
|
||||
|
||||
def caller(*args, **kwargs):
|
||||
overrides = frappe.get_hooks("regional_overrides", {}).get(get_region())
|
||||
|
||||
@ -10,4 +10,42 @@ Entries are:
|
||||
- Sales Invoice (Itemised)
|
||||
- Purchase Invoice (Itemised)
|
||||
|
||||
All accounting entries are stored in the `General Ledger`
|
||||
All accounting entries are stored in the `General Ledger`
|
||||
|
||||
## Payment Ledger
|
||||
Transactions on Receivable and Payable Account types will also be stored in `Payment Ledger`. This is so that payment reconciliation process only requires update on this ledger.
|
||||
|
||||
### Key Fields
|
||||
| Field | Description |
|
||||
|----------------------|----------------------------------|
|
||||
| `account_type` | Receivable/Payable |
|
||||
| `account` | Accounting head |
|
||||
| `party` | Party Name |
|
||||
| `voucher_no` | Voucher No |
|
||||
| `against_voucher_no` | Linked voucher(secondary effect) |
|
||||
| `amount` | can be +ve/-ve |
|
||||
|
||||
### Design
|
||||
`debit` and `credit` have been replaced with `account_type` and `amount`. `against_voucher_no` is populated for all entries. So, outstanding amount can be calculated by summing up amount only using `against_voucher_no`.
|
||||
|
||||
Ex:
|
||||
1. Consider an invoice for ₹100 and a partial payment of ₹80 against that invoice. Payment Ledger will have following entries.
|
||||
|
||||
| voucher_no | against_voucher_no | amount |
|
||||
|------------|--------------------|--------|
|
||||
| SINV-01 | SINV-01 | 100 |
|
||||
| PAY-01 | SINV-01 | -80 |
|
||||
|
||||
|
||||
2. Reconcile a Credit Note against an invoice using a Journal Entry
|
||||
|
||||
An invoice for ₹100 partially reconciled against a credit of ₹70 using a Journal Entry. Payment Ledger will have the following entries.
|
||||
|
||||
| voucher_no | against_voucher_no | amount |
|
||||
|------------|--------------------|--------|
|
||||
| SINV-01 | SINV-01 | 100 |
|
||||
| | | |
|
||||
| CR-NOTE-01 | CR-NOTE-01 | -70 |
|
||||
| | | |
|
||||
| JE-01 | CR-NOTE-01 | +70 |
|
||||
| JE-01 | SINV-01 | -70 |
|
||||
|
||||
@ -21,37 +21,39 @@ class ERPNextAddress(Address):
|
||||
return super(ERPNextAddress, self).link_address()
|
||||
|
||||
def update_compnay_address(self):
|
||||
for link in self.get('links'):
|
||||
if link.link_doctype == 'Company':
|
||||
for link in self.get("links"):
|
||||
if link.link_doctype == "Company":
|
||||
self.is_your_company_address = 1
|
||||
|
||||
def validate_reference(self):
|
||||
if self.is_your_company_address and not [
|
||||
row for row in self.links if row.link_doctype == "Company"
|
||||
]:
|
||||
frappe.throw(_("Address needs to be linked to a Company. Please add a row for Company in the Links table."),
|
||||
title=_("Company Not Linked"))
|
||||
frappe.throw(
|
||||
_("Address needs to be linked to a Company. Please add a row for Company in the Links table."),
|
||||
title=_("Company Not Linked"),
|
||||
)
|
||||
|
||||
def on_update(self):
|
||||
"""
|
||||
After Address is updated, update the related 'Primary Address' on Customer.
|
||||
"""
|
||||
address_display = get_address_display(self.as_dict())
|
||||
filters = { "customer_primary_address": self.name }
|
||||
filters = {"customer_primary_address": self.name}
|
||||
customers = frappe.db.get_all("Customer", filters=filters, as_list=True)
|
||||
for customer_name in customers:
|
||||
frappe.db.set_value("Customer", customer_name[0], "primary_address", address_display)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_shipping_address(company, address = None):
|
||||
def get_shipping_address(company, address=None):
|
||||
filters = [
|
||||
["Dynamic Link", "link_doctype", "=", "Company"],
|
||||
["Dynamic Link", "link_name", "=", company],
|
||||
["Address", "is_your_company_address", "=", 1]
|
||||
["Address", "is_your_company_address", "=", 1],
|
||||
]
|
||||
fields = ["*"]
|
||||
if address and frappe.db.get_value('Dynamic Link',
|
||||
{'parent': address, 'link_name': company}):
|
||||
if address and frappe.db.get_value("Dynamic Link", {"parent": address, "link_name": company}):
|
||||
filters.append(["Address", "name", "=", address])
|
||||
if not address:
|
||||
filters.append(["Address", "is_shipping_address", "=", 1])
|
||||
|
||||
@ -12,15 +12,24 @@ from frappe.utils.nestedset import get_descendants_of
|
||||
|
||||
@frappe.whitelist()
|
||||
@cache_source
|
||||
def get(chart_name = None, chart = None, no_cache = None, filters = None, from_date = None,
|
||||
to_date = None, timespan = None, time_interval = None, heatmap_year = None):
|
||||
def get(
|
||||
chart_name=None,
|
||||
chart=None,
|
||||
no_cache=None,
|
||||
filters=None,
|
||||
from_date=None,
|
||||
to_date=None,
|
||||
timespan=None,
|
||||
time_interval=None,
|
||||
heatmap_year=None,
|
||||
):
|
||||
if chart_name:
|
||||
chart = frappe.get_doc('Dashboard Chart', chart_name)
|
||||
chart = frappe.get_doc("Dashboard Chart", chart_name)
|
||||
else:
|
||||
chart = frappe._dict(frappe.parse_json(chart))
|
||||
timespan = chart.timespan
|
||||
|
||||
if chart.timespan == 'Select Date Range':
|
||||
if chart.timespan == "Select Date Range":
|
||||
from_date = chart.from_date
|
||||
to_date = chart.to_date
|
||||
|
||||
@ -31,17 +40,23 @@ def get(chart_name = None, chart = None, no_cache = None, filters = None, from_d
|
||||
company = filters.get("company")
|
||||
|
||||
if not account and chart_name:
|
||||
frappe.throw(_("Account is not set for the dashboard chart {0}")
|
||||
.format(get_link_to_form("Dashboard Chart", chart_name)))
|
||||
frappe.throw(
|
||||
_("Account is not set for the dashboard chart {0}").format(
|
||||
get_link_to_form("Dashboard Chart", chart_name)
|
||||
)
|
||||
)
|
||||
|
||||
if not frappe.db.exists("Account", account) and chart_name:
|
||||
frappe.throw(_("Account {0} does not exists in the dashboard chart {1}")
|
||||
.format(account, get_link_to_form("Dashboard Chart", chart_name)))
|
||||
frappe.throw(
|
||||
_("Account {0} does not exists in the dashboard chart {1}").format(
|
||||
account, get_link_to_form("Dashboard Chart", chart_name)
|
||||
)
|
||||
)
|
||||
|
||||
if not to_date:
|
||||
to_date = nowdate()
|
||||
if not from_date:
|
||||
if timegrain in ('Monthly', 'Quarterly'):
|
||||
if timegrain in ("Monthly", "Quarterly"):
|
||||
from_date = get_from_date_from_timespan(to_date, timespan)
|
||||
|
||||
# fetch dates to plot
|
||||
@ -54,16 +69,14 @@ def get(chart_name = None, chart = None, no_cache = None, filters = None, from_d
|
||||
result = build_result(account, dates, gl_entries)
|
||||
|
||||
return {
|
||||
"labels": [formatdate(r[0].strftime('%Y-%m-%d')) for r in result],
|
||||
"datasets": [{
|
||||
"name": account,
|
||||
"values": [r[1] for r in result]
|
||||
}]
|
||||
"labels": [formatdate(r[0].strftime("%Y-%m-%d")) for r in result],
|
||||
"datasets": [{"name": account, "values": [r[1] for r in result]}],
|
||||
}
|
||||
|
||||
|
||||
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.db.get_value("Account", account, "root_type")
|
||||
|
||||
# start with the first date
|
||||
date_index = 0
|
||||
@ -78,30 +91,34 @@ def build_result(account, dates, gl_entries):
|
||||
result[date_index][1] += entry.debit - entry.credit
|
||||
|
||||
# if account type is credit, switch balances
|
||||
if root_type not in ('Asset', 'Expense'):
|
||||
if root_type not in ("Asset", "Expense"):
|
||||
for r in result:
|
||||
r[1] = -1 * r[1]
|
||||
|
||||
# for balance sheet accounts, the totals are cumulative
|
||||
if root_type in ('Asset', 'Liability', 'Equity'):
|
||||
if root_type in ("Asset", "Liability", "Equity"):
|
||||
for i, r in enumerate(result):
|
||||
if i > 0:
|
||||
r[1] = r[1] + result[i-1][1]
|
||||
r[1] = r[1] + result[i - 1][1]
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def get_gl_entries(account, to_date):
|
||||
child_accounts = get_descendants_of('Account', account, ignore_permissions=True)
|
||||
child_accounts = get_descendants_of("Account", account, ignore_permissions=True)
|
||||
child_accounts.append(account)
|
||||
|
||||
return frappe.db.get_all('GL Entry',
|
||||
fields = ['posting_date', 'debit', 'credit'],
|
||||
filters = [
|
||||
dict(posting_date = ('<', to_date)),
|
||||
dict(account = ('in', child_accounts)),
|
||||
dict(voucher_type = ('!=', 'Period Closing Voucher'))
|
||||
return frappe.db.get_all(
|
||||
"GL Entry",
|
||||
fields=["posting_date", "debit", "credit"],
|
||||
filters=[
|
||||
dict(posting_date=("<", to_date)),
|
||||
dict(account=("in", child_accounts)),
|
||||
dict(voucher_type=("!=", "Period Closing Voucher")),
|
||||
],
|
||||
order_by = 'posting_date asc')
|
||||
order_by="posting_date asc",
|
||||
)
|
||||
|
||||
|
||||
def get_dates_from_timegrain(from_date, to_date, timegrain):
|
||||
days = months = years = 0
|
||||
@ -116,6 +133,8 @@ def get_dates_from_timegrain(from_date, to_date, timegrain):
|
||||
|
||||
dates = [get_period_ending(from_date, timegrain)]
|
||||
while getdate(dates[-1]) < getdate(to_date):
|
||||
date = get_period_ending(add_to_date(dates[-1], years=years, months=months, days=days), timegrain)
|
||||
date = get_period_ending(
|
||||
add_to_date(dates[-1], years=years, months=months, days=days), timegrain
|
||||
)
|
||||
dates.append(date)
|
||||
return dates
|
||||
|
||||
@ -22,20 +22,23 @@ from erpnext.accounts.utils import get_account_currency
|
||||
|
||||
|
||||
def validate_service_stop_date(doc):
|
||||
''' Validates service_stop_date for Purchase Invoice and Sales Invoice '''
|
||||
"""Validates service_stop_date for Purchase Invoice and Sales Invoice"""
|
||||
|
||||
enable_check = "enable_deferred_revenue" \
|
||||
if doc.doctype=="Sales Invoice" else "enable_deferred_expense"
|
||||
enable_check = (
|
||||
"enable_deferred_revenue" if doc.doctype == "Sales Invoice" else "enable_deferred_expense"
|
||||
)
|
||||
|
||||
old_stop_dates = {}
|
||||
old_doc = frappe.db.get_all("{0} Item".format(doc.doctype),
|
||||
{"parent": doc.name}, ["name", "service_stop_date"])
|
||||
old_doc = frappe.db.get_all(
|
||||
"{0} Item".format(doc.doctype), {"parent": doc.name}, ["name", "service_stop_date"]
|
||||
)
|
||||
|
||||
for d in old_doc:
|
||||
old_stop_dates[d.name] = d.service_stop_date or ""
|
||||
|
||||
for item in doc.items:
|
||||
if not item.get(enable_check): continue
|
||||
if not item.get(enable_check):
|
||||
continue
|
||||
|
||||
if item.service_stop_date:
|
||||
if date_diff(item.service_stop_date, item.service_start_date) < 0:
|
||||
@ -44,21 +47,31 @@ def validate_service_stop_date(doc):
|
||||
if date_diff(item.service_stop_date, item.service_end_date) > 0:
|
||||
frappe.throw(_("Service Stop Date cannot be after Service End Date"))
|
||||
|
||||
if old_stop_dates and old_stop_dates.get(item.name) and item.service_stop_date!=old_stop_dates.get(item.name):
|
||||
if (
|
||||
old_stop_dates
|
||||
and old_stop_dates.get(item.name)
|
||||
and item.service_stop_date != old_stop_dates.get(item.name)
|
||||
):
|
||||
frappe.throw(_("Cannot change Service Stop Date for item in row {0}").format(item.idx))
|
||||
|
||||
|
||||
def build_conditions(process_type, account, company):
|
||||
conditions=''
|
||||
deferred_account = "item.deferred_revenue_account" if process_type=="Income" else "item.deferred_expense_account"
|
||||
conditions = ""
|
||||
deferred_account = (
|
||||
"item.deferred_revenue_account" if process_type == "Income" else "item.deferred_expense_account"
|
||||
)
|
||||
|
||||
if account:
|
||||
conditions += "AND %s='%s'"%(deferred_account, account)
|
||||
conditions += "AND %s='%s'" % (deferred_account, account)
|
||||
elif company:
|
||||
conditions += f"AND p.company = {frappe.db.escape(company)}"
|
||||
|
||||
return conditions
|
||||
|
||||
def convert_deferred_expense_to_expense(deferred_process, start_date=None, end_date=None, conditions=''):
|
||||
|
||||
def convert_deferred_expense_to_expense(
|
||||
deferred_process, start_date=None, end_date=None, conditions=""
|
||||
):
|
||||
# book the expense/income on the last day, but it will be trigger on the 1st of month at 12:00 AM
|
||||
|
||||
if not start_date:
|
||||
@ -67,14 +80,19 @@ def convert_deferred_expense_to_expense(deferred_process, start_date=None, end_d
|
||||
end_date = add_days(today(), -1)
|
||||
|
||||
# check for the purchase invoice for which GL entries has to be done
|
||||
invoices = frappe.db.sql_list('''
|
||||
invoices = frappe.db.sql_list(
|
||||
"""
|
||||
select distinct item.parent
|
||||
from `tabPurchase Invoice Item` item, `tabPurchase Invoice` p
|
||||
where item.service_start_date<=%s and item.service_end_date>=%s
|
||||
and item.enable_deferred_expense = 1 and item.parent=p.name
|
||||
and item.docstatus = 1 and ifnull(item.amount, 0) > 0
|
||||
{0}
|
||||
'''.format(conditions), (end_date, start_date)) #nosec
|
||||
""".format(
|
||||
conditions
|
||||
),
|
||||
(end_date, start_date),
|
||||
) # nosec
|
||||
|
||||
# For each invoice, book deferred expense
|
||||
for invoice in invoices:
|
||||
@ -84,7 +102,10 @@ def convert_deferred_expense_to_expense(deferred_process, start_date=None, end_d
|
||||
if frappe.flags.deferred_accounting_error:
|
||||
send_mail(deferred_process)
|
||||
|
||||
def convert_deferred_revenue_to_income(deferred_process, start_date=None, end_date=None, conditions=''):
|
||||
|
||||
def convert_deferred_revenue_to_income(
|
||||
deferred_process, start_date=None, end_date=None, conditions=""
|
||||
):
|
||||
# book the expense/income on the last day, but it will be trigger on the 1st of month at 12:00 AM
|
||||
|
||||
if not start_date:
|
||||
@ -93,14 +114,19 @@ def convert_deferred_revenue_to_income(deferred_process, start_date=None, end_da
|
||||
end_date = add_days(today(), -1)
|
||||
|
||||
# check for the sales invoice for which GL entries has to be done
|
||||
invoices = frappe.db.sql_list('''
|
||||
invoices = frappe.db.sql_list(
|
||||
"""
|
||||
select distinct item.parent
|
||||
from `tabSales Invoice Item` item, `tabSales Invoice` p
|
||||
where item.service_start_date<=%s and item.service_end_date>=%s
|
||||
and item.enable_deferred_revenue = 1 and item.parent=p.name
|
||||
and item.docstatus = 1 and ifnull(item.amount, 0) > 0
|
||||
{0}
|
||||
'''.format(conditions), (end_date, start_date)) #nosec
|
||||
""".format(
|
||||
conditions
|
||||
),
|
||||
(end_date, start_date),
|
||||
) # nosec
|
||||
|
||||
for invoice in invoices:
|
||||
doc = frappe.get_doc("Sales Invoice", invoice)
|
||||
@ -109,31 +135,43 @@ def convert_deferred_revenue_to_income(deferred_process, start_date=None, end_da
|
||||
if frappe.flags.deferred_accounting_error:
|
||||
send_mail(deferred_process)
|
||||
|
||||
|
||||
def get_booking_dates(doc, item, posting_date=None):
|
||||
if not posting_date:
|
||||
posting_date = add_days(today(), -1)
|
||||
|
||||
last_gl_entry = False
|
||||
|
||||
deferred_account = "deferred_revenue_account" if doc.doctype=="Sales Invoice" else "deferred_expense_account"
|
||||
deferred_account = (
|
||||
"deferred_revenue_account" if doc.doctype == "Sales Invoice" else "deferred_expense_account"
|
||||
)
|
||||
|
||||
prev_gl_entry = frappe.db.sql('''
|
||||
prev_gl_entry = frappe.db.sql(
|
||||
"""
|
||||
select name, posting_date from `tabGL Entry` where company=%s and account=%s and
|
||||
voucher_type=%s and voucher_no=%s and voucher_detail_no=%s
|
||||
and is_cancelled = 0
|
||||
order by posting_date desc limit 1
|
||||
''', (doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name), as_dict=True)
|
||||
""",
|
||||
(doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name),
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
prev_gl_via_je = frappe.db.sql('''
|
||||
prev_gl_via_je = frappe.db.sql(
|
||||
"""
|
||||
SELECT p.name, p.posting_date FROM `tabJournal Entry` p, `tabJournal Entry Account` c
|
||||
WHERE p.name = c.parent and p.company=%s and c.account=%s
|
||||
and c.reference_type=%s and c.reference_name=%s
|
||||
and c.reference_detail_no=%s and c.docstatus < 2 order by posting_date desc limit 1
|
||||
''', (doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name), as_dict=True)
|
||||
""",
|
||||
(doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name),
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
if prev_gl_via_je:
|
||||
if (not prev_gl_entry) or (prev_gl_entry and
|
||||
prev_gl_entry[0].posting_date < prev_gl_via_je[0].posting_date):
|
||||
if (not prev_gl_entry) or (
|
||||
prev_gl_entry and prev_gl_entry[0].posting_date < prev_gl_via_je[0].posting_date
|
||||
):
|
||||
prev_gl_entry = prev_gl_via_je
|
||||
|
||||
if prev_gl_entry:
|
||||
@ -157,66 +195,94 @@ def get_booking_dates(doc, item, posting_date=None):
|
||||
else:
|
||||
return None, None, None
|
||||
|
||||
def calculate_monthly_amount(doc, item, last_gl_entry, start_date, end_date, total_days, total_booking_days, account_currency):
|
||||
|
||||
def calculate_monthly_amount(
|
||||
doc, item, last_gl_entry, start_date, end_date, total_days, total_booking_days, account_currency
|
||||
):
|
||||
amount, base_amount = 0, 0
|
||||
|
||||
if not last_gl_entry:
|
||||
total_months = (item.service_end_date.year - item.service_start_date.year) * 12 + \
|
||||
(item.service_end_date.month - item.service_start_date.month) + 1
|
||||
total_months = (
|
||||
(item.service_end_date.year - item.service_start_date.year) * 12
|
||||
+ (item.service_end_date.month - item.service_start_date.month)
|
||||
+ 1
|
||||
)
|
||||
|
||||
prorate_factor = flt(date_diff(item.service_end_date, item.service_start_date)) \
|
||||
/ flt(date_diff(get_last_day(item.service_end_date), get_first_day(item.service_start_date)))
|
||||
prorate_factor = flt(date_diff(item.service_end_date, item.service_start_date)) / flt(
|
||||
date_diff(get_last_day(item.service_end_date), get_first_day(item.service_start_date))
|
||||
)
|
||||
|
||||
actual_months = rounded(total_months * prorate_factor, 1)
|
||||
|
||||
already_booked_amount, already_booked_amount_in_account_currency = get_already_booked_amount(doc, item)
|
||||
already_booked_amount, already_booked_amount_in_account_currency = get_already_booked_amount(
|
||||
doc, item
|
||||
)
|
||||
base_amount = flt(item.base_net_amount / actual_months, item.precision("base_net_amount"))
|
||||
|
||||
if base_amount + already_booked_amount > item.base_net_amount:
|
||||
base_amount = item.base_net_amount - already_booked_amount
|
||||
|
||||
if account_currency==doc.company_currency:
|
||||
if account_currency == doc.company_currency:
|
||||
amount = base_amount
|
||||
else:
|
||||
amount = flt(item.net_amount/actual_months, item.precision("net_amount"))
|
||||
amount = flt(item.net_amount / actual_months, item.precision("net_amount"))
|
||||
if amount + already_booked_amount_in_account_currency > item.net_amount:
|
||||
amount = item.net_amount - already_booked_amount_in_account_currency
|
||||
|
||||
if not (get_first_day(start_date) == start_date and get_last_day(end_date) == end_date):
|
||||
partial_month = flt(date_diff(end_date, start_date)) \
|
||||
/ flt(date_diff(get_last_day(end_date), get_first_day(start_date)))
|
||||
partial_month = flt(date_diff(end_date, start_date)) / flt(
|
||||
date_diff(get_last_day(end_date), get_first_day(start_date))
|
||||
)
|
||||
|
||||
base_amount = rounded(partial_month, 1) * base_amount
|
||||
amount = rounded(partial_month, 1) * amount
|
||||
else:
|
||||
already_booked_amount, already_booked_amount_in_account_currency = get_already_booked_amount(doc, item)
|
||||
base_amount = flt(item.base_net_amount - already_booked_amount, item.precision("base_net_amount"))
|
||||
if account_currency==doc.company_currency:
|
||||
already_booked_amount, already_booked_amount_in_account_currency = get_already_booked_amount(
|
||||
doc, item
|
||||
)
|
||||
base_amount = flt(
|
||||
item.base_net_amount - already_booked_amount, item.precision("base_net_amount")
|
||||
)
|
||||
if account_currency == doc.company_currency:
|
||||
amount = base_amount
|
||||
else:
|
||||
amount = flt(item.net_amount - already_booked_amount_in_account_currency, item.precision("net_amount"))
|
||||
amount = flt(
|
||||
item.net_amount - already_booked_amount_in_account_currency, item.precision("net_amount")
|
||||
)
|
||||
|
||||
return amount, base_amount
|
||||
|
||||
|
||||
def calculate_amount(doc, item, last_gl_entry, total_days, total_booking_days, account_currency):
|
||||
amount, base_amount = 0, 0
|
||||
if not last_gl_entry:
|
||||
base_amount = flt(item.base_net_amount*total_booking_days/flt(total_days), item.precision("base_net_amount"))
|
||||
if account_currency==doc.company_currency:
|
||||
base_amount = flt(
|
||||
item.base_net_amount * total_booking_days / flt(total_days), item.precision("base_net_amount")
|
||||
)
|
||||
if account_currency == doc.company_currency:
|
||||
amount = base_amount
|
||||
else:
|
||||
amount = flt(item.net_amount*total_booking_days/flt(total_days), item.precision("net_amount"))
|
||||
amount = flt(
|
||||
item.net_amount * total_booking_days / flt(total_days), item.precision("net_amount")
|
||||
)
|
||||
else:
|
||||
already_booked_amount, already_booked_amount_in_account_currency = get_already_booked_amount(doc, item)
|
||||
already_booked_amount, already_booked_amount_in_account_currency = get_already_booked_amount(
|
||||
doc, item
|
||||
)
|
||||
|
||||
base_amount = flt(item.base_net_amount - already_booked_amount, item.precision("base_net_amount"))
|
||||
if account_currency==doc.company_currency:
|
||||
base_amount = flt(
|
||||
item.base_net_amount - already_booked_amount, item.precision("base_net_amount")
|
||||
)
|
||||
if account_currency == doc.company_currency:
|
||||
amount = base_amount
|
||||
else:
|
||||
amount = flt(item.net_amount - already_booked_amount_in_account_currency, item.precision("net_amount"))
|
||||
amount = flt(
|
||||
item.net_amount - already_booked_amount_in_account_currency, item.precision("net_amount")
|
||||
)
|
||||
|
||||
return amount, base_amount
|
||||
|
||||
|
||||
def get_already_booked_amount(doc, item):
|
||||
if doc.doctype == "Sales Invoice":
|
||||
total_credit_debit, total_credit_debit_currency = "debit", "debit_in_account_currency"
|
||||
@ -225,21 +291,31 @@ def get_already_booked_amount(doc, item):
|
||||
total_credit_debit, total_credit_debit_currency = "credit", "credit_in_account_currency"
|
||||
deferred_account = "deferred_expense_account"
|
||||
|
||||
gl_entries_details = frappe.db.sql('''
|
||||
gl_entries_details = frappe.db.sql(
|
||||
"""
|
||||
select sum({0}) as total_credit, sum({1}) as total_credit_in_account_currency, voucher_detail_no
|
||||
from `tabGL Entry` where company=%s and account=%s and voucher_type=%s and voucher_no=%s and voucher_detail_no=%s
|
||||
and is_cancelled = 0
|
||||
group by voucher_detail_no
|
||||
'''.format(total_credit_debit, total_credit_debit_currency),
|
||||
(doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name), as_dict=True)
|
||||
""".format(
|
||||
total_credit_debit, total_credit_debit_currency
|
||||
),
|
||||
(doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name),
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
journal_entry_details = frappe.db.sql('''
|
||||
journal_entry_details = frappe.db.sql(
|
||||
"""
|
||||
SELECT sum(c.{0}) as total_credit, sum(c.{1}) as total_credit_in_account_currency, reference_detail_no
|
||||
FROM `tabJournal Entry` p , `tabJournal Entry Account` c WHERE p.name = c.parent and
|
||||
p.company = %s and c.account=%s and c.reference_type=%s and c.reference_name=%s and c.reference_detail_no=%s
|
||||
and p.docstatus < 2 group by reference_detail_no
|
||||
'''.format(total_credit_debit, total_credit_debit_currency),
|
||||
(doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name), as_dict=True)
|
||||
""".format(
|
||||
total_credit_debit, total_credit_debit_currency
|
||||
),
|
||||
(doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name),
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
already_booked_amount = gl_entries_details[0].total_credit if gl_entries_details else 0
|
||||
already_booked_amount += journal_entry_details[0].total_credit if journal_entry_details else 0
|
||||
@ -247,20 +323,29 @@ def get_already_booked_amount(doc, item):
|
||||
if doc.currency == doc.company_currency:
|
||||
already_booked_amount_in_account_currency = already_booked_amount
|
||||
else:
|
||||
already_booked_amount_in_account_currency = gl_entries_details[0].total_credit_in_account_currency if gl_entries_details else 0
|
||||
already_booked_amount_in_account_currency += journal_entry_details[0].total_credit_in_account_currency if journal_entry_details else 0
|
||||
already_booked_amount_in_account_currency = (
|
||||
gl_entries_details[0].total_credit_in_account_currency if gl_entries_details else 0
|
||||
)
|
||||
already_booked_amount_in_account_currency += (
|
||||
journal_entry_details[0].total_credit_in_account_currency if journal_entry_details else 0
|
||||
)
|
||||
|
||||
return already_booked_amount, already_booked_amount_in_account_currency
|
||||
|
||||
|
||||
def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
|
||||
enable_check = "enable_deferred_revenue" \
|
||||
if doc.doctype=="Sales Invoice" else "enable_deferred_expense"
|
||||
enable_check = (
|
||||
"enable_deferred_revenue" if doc.doctype == "Sales Invoice" else "enable_deferred_expense"
|
||||
)
|
||||
|
||||
accounts_frozen_upto = frappe.get_cached_value('Accounts Settings', 'None', 'acc_frozen_upto')
|
||||
accounts_frozen_upto = frappe.get_cached_value("Accounts Settings", "None", "acc_frozen_upto")
|
||||
|
||||
def _book_deferred_revenue_or_expense(item, via_journal_entry, submit_journal_entry, book_deferred_entries_based_on):
|
||||
def _book_deferred_revenue_or_expense(
|
||||
item, via_journal_entry, submit_journal_entry, book_deferred_entries_based_on
|
||||
):
|
||||
start_date, end_date, last_gl_entry = get_booking_dates(doc, item, posting_date=posting_date)
|
||||
if not (start_date and end_date): return
|
||||
if not (start_date and end_date):
|
||||
return
|
||||
|
||||
account_currency = get_account_currency(item.expense_account or item.income_account)
|
||||
if doc.doctype == "Sales Invoice":
|
||||
@ -273,12 +358,21 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
|
||||
total_days = date_diff(item.service_end_date, item.service_start_date) + 1
|
||||
total_booking_days = date_diff(end_date, start_date) + 1
|
||||
|
||||
if book_deferred_entries_based_on == 'Months':
|
||||
amount, base_amount = calculate_monthly_amount(doc, item, last_gl_entry,
|
||||
start_date, end_date, total_days, total_booking_days, account_currency)
|
||||
if book_deferred_entries_based_on == "Months":
|
||||
amount, base_amount = calculate_monthly_amount(
|
||||
doc,
|
||||
item,
|
||||
last_gl_entry,
|
||||
start_date,
|
||||
end_date,
|
||||
total_days,
|
||||
total_booking_days,
|
||||
account_currency,
|
||||
)
|
||||
else:
|
||||
amount, base_amount = calculate_amount(doc, item, last_gl_entry,
|
||||
total_days, total_booking_days, account_currency)
|
||||
amount, base_amount = calculate_amount(
|
||||
doc, item, last_gl_entry, total_days, total_booking_days, account_currency
|
||||
)
|
||||
|
||||
if not amount:
|
||||
return
|
||||
@ -288,92 +382,155 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
|
||||
end_date = get_last_day(add_days(accounts_frozen_upto, 1))
|
||||
|
||||
if via_journal_entry:
|
||||
book_revenue_via_journal_entry(doc, credit_account, debit_account, against, amount,
|
||||
base_amount, end_date, project, account_currency, item.cost_center, item, deferred_process, submit_journal_entry)
|
||||
book_revenue_via_journal_entry(
|
||||
doc,
|
||||
credit_account,
|
||||
debit_account,
|
||||
amount,
|
||||
base_amount,
|
||||
end_date,
|
||||
project,
|
||||
account_currency,
|
||||
item.cost_center,
|
||||
item,
|
||||
deferred_process,
|
||||
submit_journal_entry,
|
||||
)
|
||||
else:
|
||||
make_gl_entries(doc, credit_account, debit_account, against,
|
||||
amount, base_amount, end_date, project, account_currency, item.cost_center, item, deferred_process)
|
||||
make_gl_entries(
|
||||
doc,
|
||||
credit_account,
|
||||
debit_account,
|
||||
against,
|
||||
amount,
|
||||
base_amount,
|
||||
end_date,
|
||||
project,
|
||||
account_currency,
|
||||
item.cost_center,
|
||||
item,
|
||||
deferred_process,
|
||||
)
|
||||
|
||||
# Returned in case of any errors because it tries to submit the same record again and again in case of errors
|
||||
if frappe.flags.deferred_accounting_error:
|
||||
return
|
||||
|
||||
if getdate(end_date) < getdate(posting_date) and not last_gl_entry:
|
||||
_book_deferred_revenue_or_expense(item, via_journal_entry, submit_journal_entry, book_deferred_entries_based_on)
|
||||
_book_deferred_revenue_or_expense(
|
||||
item, via_journal_entry, submit_journal_entry, book_deferred_entries_based_on
|
||||
)
|
||||
|
||||
via_journal_entry = cint(frappe.db.get_singles_value('Accounts Settings', 'book_deferred_entries_via_journal_entry'))
|
||||
submit_journal_entry = cint(frappe.db.get_singles_value('Accounts Settings', 'submit_journal_entries'))
|
||||
book_deferred_entries_based_on = frappe.db.get_singles_value('Accounts Settings', 'book_deferred_entries_based_on')
|
||||
via_journal_entry = cint(
|
||||
frappe.db.get_singles_value("Accounts Settings", "book_deferred_entries_via_journal_entry")
|
||||
)
|
||||
submit_journal_entry = cint(
|
||||
frappe.db.get_singles_value("Accounts Settings", "submit_journal_entries")
|
||||
)
|
||||
book_deferred_entries_based_on = frappe.db.get_singles_value(
|
||||
"Accounts Settings", "book_deferred_entries_based_on"
|
||||
)
|
||||
|
||||
for item in doc.get('items'):
|
||||
for item in doc.get("items"):
|
||||
if item.get(enable_check):
|
||||
_book_deferred_revenue_or_expense(item, via_journal_entry, submit_journal_entry, book_deferred_entries_based_on)
|
||||
_book_deferred_revenue_or_expense(
|
||||
item, via_journal_entry, submit_journal_entry, book_deferred_entries_based_on
|
||||
)
|
||||
|
||||
|
||||
def process_deferred_accounting(posting_date=None):
|
||||
''' Converts deferred income/expense into income/expense
|
||||
Executed via background jobs on every month end '''
|
||||
"""Converts deferred income/expense into income/expense
|
||||
Executed via background jobs on every month end"""
|
||||
|
||||
if not posting_date:
|
||||
posting_date = today()
|
||||
|
||||
if not cint(frappe.db.get_singles_value('Accounts Settings', 'automatically_process_deferred_accounting_entry')):
|
||||
if not cint(
|
||||
frappe.db.get_singles_value(
|
||||
"Accounts Settings", "automatically_process_deferred_accounting_entry"
|
||||
)
|
||||
):
|
||||
return
|
||||
|
||||
start_date = add_months(today(), -1)
|
||||
end_date = add_days(today(), -1)
|
||||
|
||||
companies = frappe.get_all('Company')
|
||||
companies = frappe.get_all("Company")
|
||||
|
||||
for company in companies:
|
||||
for record_type in ('Income', 'Expense'):
|
||||
doc = frappe.get_doc(dict(
|
||||
doctype='Process Deferred Accounting',
|
||||
company=company.name,
|
||||
posting_date=posting_date,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
type=record_type
|
||||
))
|
||||
for record_type in ("Income", "Expense"):
|
||||
doc = frappe.get_doc(
|
||||
dict(
|
||||
doctype="Process Deferred Accounting",
|
||||
company=company.name,
|
||||
posting_date=posting_date,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
type=record_type,
|
||||
)
|
||||
)
|
||||
|
||||
doc.insert()
|
||||
doc.submit()
|
||||
|
||||
def make_gl_entries(doc, credit_account, debit_account, against,
|
||||
amount, base_amount, posting_date, project, account_currency, cost_center, item, deferred_process=None):
|
||||
|
||||
def make_gl_entries(
|
||||
doc,
|
||||
credit_account,
|
||||
debit_account,
|
||||
against,
|
||||
amount,
|
||||
base_amount,
|
||||
posting_date,
|
||||
project,
|
||||
account_currency,
|
||||
cost_center,
|
||||
item,
|
||||
deferred_process=None,
|
||||
):
|
||||
# GL Entry for crediting the amount in the deferred expense
|
||||
from erpnext.accounts.general_ledger import make_gl_entries
|
||||
|
||||
if amount == 0: return
|
||||
if amount == 0:
|
||||
return
|
||||
|
||||
gl_entries = []
|
||||
gl_entries.append(
|
||||
doc.get_gl_dict({
|
||||
"account": credit_account,
|
||||
"against": against,
|
||||
"credit": base_amount,
|
||||
"credit_in_account_currency": amount,
|
||||
"cost_center": cost_center,
|
||||
"voucher_detail_no": item.name,
|
||||
'posting_date': posting_date,
|
||||
'project': project,
|
||||
'against_voucher_type': 'Process Deferred Accounting',
|
||||
'against_voucher': deferred_process
|
||||
}, account_currency, item=item)
|
||||
doc.get_gl_dict(
|
||||
{
|
||||
"account": credit_account,
|
||||
"against": against,
|
||||
"credit": base_amount,
|
||||
"credit_in_account_currency": amount,
|
||||
"cost_center": cost_center,
|
||||
"voucher_detail_no": item.name,
|
||||
"posting_date": posting_date,
|
||||
"project": project,
|
||||
"against_voucher_type": "Process Deferred Accounting",
|
||||
"against_voucher": deferred_process,
|
||||
},
|
||||
account_currency,
|
||||
item=item,
|
||||
)
|
||||
)
|
||||
# GL Entry to debit the amount from the expense
|
||||
gl_entries.append(
|
||||
doc.get_gl_dict({
|
||||
"account": debit_account,
|
||||
"against": against,
|
||||
"debit": base_amount,
|
||||
"debit_in_account_currency": amount,
|
||||
"cost_center": cost_center,
|
||||
"voucher_detail_no": item.name,
|
||||
'posting_date': posting_date,
|
||||
'project': project,
|
||||
'against_voucher_type': 'Process Deferred Accounting',
|
||||
'against_voucher': deferred_process
|
||||
}, account_currency, item=item)
|
||||
doc.get_gl_dict(
|
||||
{
|
||||
"account": debit_account,
|
||||
"against": against,
|
||||
"debit": base_amount,
|
||||
"debit_in_account_currency": amount,
|
||||
"cost_center": cost_center,
|
||||
"voucher_detail_no": item.name,
|
||||
"posting_date": posting_date,
|
||||
"project": project,
|
||||
"against_voucher_type": "Process Deferred Accounting",
|
||||
"against_voucher": deferred_process,
|
||||
},
|
||||
account_currency,
|
||||
item=item,
|
||||
)
|
||||
)
|
||||
|
||||
if gl_entries:
|
||||
@ -382,69 +539,81 @@ def make_gl_entries(doc, credit_account, debit_account, against,
|
||||
frappe.db.commit()
|
||||
except Exception as e:
|
||||
if frappe.flags.in_test:
|
||||
traceback = frappe.get_traceback()
|
||||
frappe.log_error(title=_('Error while processing deferred accounting for Invoice {0}').format(doc.name), message=traceback)
|
||||
doc.log_error(f"Error while processing deferred accounting for Invoice {doc.name}")
|
||||
raise e
|
||||
else:
|
||||
frappe.db.rollback()
|
||||
traceback = frappe.get_traceback()
|
||||
frappe.log_error(title=_('Error while processing deferred accounting for Invoice {0}').format(doc.name), message=traceback)
|
||||
doc.log_error(f"Error while processing deferred accounting for Invoice {doc.name}")
|
||||
frappe.flags.deferred_accounting_error = True
|
||||
|
||||
|
||||
def send_mail(deferred_process):
|
||||
title = _("Error while processing deferred accounting for {0}").format(deferred_process)
|
||||
link = get_link_to_form('Process Deferred Accounting', deferred_process)
|
||||
link = get_link_to_form("Process Deferred Accounting", deferred_process)
|
||||
content = _("Deferred accounting failed for some invoices:") + "\n"
|
||||
content += _("Please check Process Deferred Accounting {0} and submit manually after resolving errors.").format(link)
|
||||
content += _(
|
||||
"Please check Process Deferred Accounting {0} and submit manually after resolving errors."
|
||||
).format(link)
|
||||
sendmail_to_system_managers(title, content)
|
||||
|
||||
def book_revenue_via_journal_entry(doc, credit_account, debit_account, against,
|
||||
amount, base_amount, posting_date, project, account_currency, cost_center, item,
|
||||
deferred_process=None, submit='No'):
|
||||
|
||||
if amount == 0: return
|
||||
def book_revenue_via_journal_entry(
|
||||
doc,
|
||||
credit_account,
|
||||
debit_account,
|
||||
amount,
|
||||
base_amount,
|
||||
posting_date,
|
||||
project,
|
||||
account_currency,
|
||||
cost_center,
|
||||
item,
|
||||
deferred_process=None,
|
||||
submit="No",
|
||||
):
|
||||
|
||||
journal_entry = frappe.new_doc('Journal Entry')
|
||||
if amount == 0:
|
||||
return
|
||||
|
||||
journal_entry = frappe.new_doc("Journal Entry")
|
||||
journal_entry.posting_date = posting_date
|
||||
journal_entry.company = doc.company
|
||||
journal_entry.voucher_type = 'Deferred Revenue' if doc.doctype == 'Sales Invoice' \
|
||||
else 'Deferred Expense'
|
||||
journal_entry.voucher_type = (
|
||||
"Deferred Revenue" if doc.doctype == "Sales Invoice" else "Deferred Expense"
|
||||
)
|
||||
journal_entry.process_deferred_accounting = deferred_process
|
||||
|
||||
debit_entry = {
|
||||
'account': credit_account,
|
||||
'credit': base_amount,
|
||||
'credit_in_account_currency': amount,
|
||||
'account_currency': account_currency,
|
||||
'reference_name': doc.name,
|
||||
'reference_type': doc.doctype,
|
||||
'reference_detail_no': item.name,
|
||||
'cost_center': cost_center,
|
||||
'project': project,
|
||||
"account": credit_account,
|
||||
"credit": base_amount,
|
||||
"credit_in_account_currency": amount,
|
||||
"account_currency": account_currency,
|
||||
"reference_name": doc.name,
|
||||
"reference_type": doc.doctype,
|
||||
"reference_detail_no": item.name,
|
||||
"cost_center": cost_center,
|
||||
"project": project,
|
||||
}
|
||||
|
||||
credit_entry = {
|
||||
'account': debit_account,
|
||||
'debit': base_amount,
|
||||
'debit_in_account_currency': amount,
|
||||
'account_currency': account_currency,
|
||||
'reference_name': doc.name,
|
||||
'reference_type': doc.doctype,
|
||||
'reference_detail_no': item.name,
|
||||
'cost_center': cost_center,
|
||||
'project': project,
|
||||
"account": debit_account,
|
||||
"debit": base_amount,
|
||||
"debit_in_account_currency": amount,
|
||||
"account_currency": account_currency,
|
||||
"reference_name": doc.name,
|
||||
"reference_type": doc.doctype,
|
||||
"reference_detail_no": item.name,
|
||||
"cost_center": cost_center,
|
||||
"project": project,
|
||||
}
|
||||
|
||||
for dimension in get_accounting_dimensions():
|
||||
debit_entry.update({
|
||||
dimension: item.get(dimension)
|
||||
})
|
||||
debit_entry.update({dimension: item.get(dimension)})
|
||||
|
||||
credit_entry.update({
|
||||
dimension: item.get(dimension)
|
||||
})
|
||||
credit_entry.update({dimension: item.get(dimension)})
|
||||
|
||||
journal_entry.append('accounts', debit_entry)
|
||||
journal_entry.append('accounts', credit_entry)
|
||||
journal_entry.append("accounts", debit_entry)
|
||||
journal_entry.append("accounts", credit_entry)
|
||||
|
||||
try:
|
||||
journal_entry.save()
|
||||
@ -455,21 +624,26 @@ def book_revenue_via_journal_entry(doc, credit_account, debit_account, against,
|
||||
frappe.db.commit()
|
||||
except Exception:
|
||||
frappe.db.rollback()
|
||||
traceback = frappe.get_traceback()
|
||||
frappe.log_error(title=_('Error while processing deferred accounting for Invoice {0}').format(doc.name), message=traceback)
|
||||
|
||||
doc.log_error(f"Error while processing deferred accounting for Invoice {doc.name}")
|
||||
frappe.flags.deferred_accounting_error = True
|
||||
|
||||
|
||||
def get_deferred_booking_accounts(doctype, voucher_detail_no, dr_or_cr):
|
||||
|
||||
if doctype == 'Sales Invoice':
|
||||
credit_account, debit_account = frappe.db.get_value('Sales Invoice Item', {'name': voucher_detail_no},
|
||||
['income_account', 'deferred_revenue_account'])
|
||||
if doctype == "Sales Invoice":
|
||||
credit_account, debit_account = frappe.db.get_value(
|
||||
"Sales Invoice Item",
|
||||
{"name": voucher_detail_no},
|
||||
["income_account", "deferred_revenue_account"],
|
||||
)
|
||||
else:
|
||||
credit_account, debit_account = frappe.db.get_value('Purchase Invoice Item', {'name': voucher_detail_no},
|
||||
['deferred_expense_account', 'expense_account'])
|
||||
credit_account, debit_account = frappe.db.get_value(
|
||||
"Purchase Invoice Item",
|
||||
{"name": voucher_detail_no},
|
||||
["deferred_expense_account", "expense_account"],
|
||||
)
|
||||
|
||||
if dr_or_cr == 'Debit':
|
||||
if dr_or_cr == "Debit":
|
||||
return debit_account
|
||||
else:
|
||||
return credit_account
|
||||
|
||||
@ -10,11 +10,17 @@ from frappe.utils.nestedset import NestedSet, get_ancestors_of, get_descendants_
|
||||
import erpnext
|
||||
|
||||
|
||||
class RootNotEditable(frappe.ValidationError): pass
|
||||
class BalanceMismatchError(frappe.ValidationError): pass
|
||||
class RootNotEditable(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class BalanceMismatchError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class Account(NestedSet):
|
||||
nsm_parent_field = 'parent_account'
|
||||
nsm_parent_field = "parent_account"
|
||||
|
||||
def on_update(self):
|
||||
if frappe.local.flags.ignore_update_nsm:
|
||||
return
|
||||
@ -22,17 +28,20 @@ class Account(NestedSet):
|
||||
super(Account, self).on_update()
|
||||
|
||||
def onload(self):
|
||||
frozen_accounts_modifier = frappe.db.get_value("Accounts Settings", "Accounts Settings",
|
||||
"frozen_accounts_modifier")
|
||||
frozen_accounts_modifier = frappe.db.get_value(
|
||||
"Accounts Settings", "Accounts Settings", "frozen_accounts_modifier"
|
||||
)
|
||||
if not frozen_accounts_modifier or frozen_accounts_modifier in frappe.get_roles():
|
||||
self.set_onload("can_freeze_account", True)
|
||||
|
||||
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)
|
||||
|
||||
def validate(self):
|
||||
from erpnext.accounts.utils import validate_field_number
|
||||
|
||||
if frappe.local.flags.allow_unverified_charts:
|
||||
return
|
||||
self.validate_parent()
|
||||
@ -49,22 +58,33 @@ class Account(NestedSet):
|
||||
def validate_parent(self):
|
||||
"""Fetch Parent Details and validate parent account"""
|
||||
if self.parent_account:
|
||||
par = frappe.db.get_value("Account", self.parent_account,
|
||||
["name", "is_group", "company"], as_dict=1)
|
||||
par = frappe.db.get_value(
|
||||
"Account", self.parent_account, ["name", "is_group", "company"], as_dict=1
|
||||
)
|
||||
if not par:
|
||||
throw(_("Account {0}: Parent account {1} does not exist").format(self.name, self.parent_account))
|
||||
throw(
|
||||
_("Account {0}: Parent account {1} does not exist").format(self.name, self.parent_account)
|
||||
)
|
||||
elif par.name == self.name:
|
||||
throw(_("Account {0}: You can not assign itself as parent account").format(self.name))
|
||||
elif not par.is_group:
|
||||
throw(_("Account {0}: Parent account {1} can not be a ledger").format(self.name, self.parent_account))
|
||||
throw(
|
||||
_("Account {0}: Parent account {1} can not be a ledger").format(
|
||||
self.name, self.parent_account
|
||||
)
|
||||
)
|
||||
elif par.company != self.company:
|
||||
throw(_("Account {0}: Parent account {1} does not belong to company: {2}")
|
||||
.format(self.name, self.parent_account, self.company))
|
||||
throw(
|
||||
_("Account {0}: Parent account {1} does not belong to company: {2}").format(
|
||||
self.name, self.parent_account, self.company
|
||||
)
|
||||
)
|
||||
|
||||
def set_root_and_report_type(self):
|
||||
if self.parent_account:
|
||||
par = frappe.db.get_value("Account", self.parent_account,
|
||||
["report_type", "root_type"], as_dict=1)
|
||||
par = frappe.db.get_value(
|
||||
"Account", self.parent_account, ["report_type", "root_type"], as_dict=1
|
||||
)
|
||||
|
||||
if par.report_type:
|
||||
self.report_type = par.report_type
|
||||
@ -75,15 +95,20 @@ class Account(NestedSet):
|
||||
db_value = frappe.db.get_value("Account", self.name, ["report_type", "root_type"], as_dict=1)
|
||||
if db_value:
|
||||
if self.report_type != db_value.report_type:
|
||||
frappe.db.sql("update `tabAccount` set report_type=%s where lft > %s and rgt < %s",
|
||||
(self.report_type, self.lft, self.rgt))
|
||||
frappe.db.sql(
|
||||
"update `tabAccount` set report_type=%s where lft > %s and rgt < %s",
|
||||
(self.report_type, self.lft, self.rgt),
|
||||
)
|
||||
if self.root_type != db_value.root_type:
|
||||
frappe.db.sql("update `tabAccount` set root_type=%s where lft > %s and rgt < %s",
|
||||
(self.root_type, self.lft, self.rgt))
|
||||
frappe.db.sql(
|
||||
"update `tabAccount` set root_type=%s where lft > %s and rgt < %s",
|
||||
(self.root_type, self.lft, self.rgt),
|
||||
)
|
||||
|
||||
if self.root_type and not self.report_type:
|
||||
self.report_type = "Balance Sheet" \
|
||||
if self.root_type in ("Asset", "Liability", "Equity") else "Profit and Loss"
|
||||
self.report_type = (
|
||||
"Balance Sheet" if self.root_type in ("Asset", "Liability", "Equity") else "Profit and Loss"
|
||||
)
|
||||
|
||||
def validate_root_details(self):
|
||||
# does not exists parent
|
||||
@ -96,21 +121,26 @@ class Account(NestedSet):
|
||||
|
||||
def validate_root_company_and_sync_account_to_children(self):
|
||||
# ignore validation while creating new compnay or while syncing to child companies
|
||||
if frappe.local.flags.ignore_root_company_validation or self.flags.ignore_root_company_validation:
|
||||
if (
|
||||
frappe.local.flags.ignore_root_company_validation or self.flags.ignore_root_company_validation
|
||||
):
|
||||
return
|
||||
ancestors = get_root_company(self.company)
|
||||
if ancestors:
|
||||
if frappe.get_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'):
|
||||
if not frappe.db.get_value(
|
||||
"Account", {"account_name": self.account_name, "company": ancestors[0]}, "name"
|
||||
):
|
||||
frappe.throw(_("Please add the account to root level Company - {}").format(ancestors[0]))
|
||||
elif self.parent_account:
|
||||
descendants = get_descendants_of('Company', self.company)
|
||||
if not descendants: return
|
||||
descendants = get_descendants_of("Company", self.company)
|
||||
if not descendants:
|
||||
return
|
||||
parent_acc_name_map = {}
|
||||
parent_acc_name, parent_acc_number = frappe.db.get_value('Account', self.parent_account, \
|
||||
["account_name", "account_number"])
|
||||
parent_acc_name, parent_acc_number = frappe.db.get_value(
|
||||
"Account", self.parent_account, ["account_name", "account_number"]
|
||||
)
|
||||
filters = {
|
||||
"company": ["in", descendants],
|
||||
"account_name": parent_acc_name,
|
||||
@ -118,10 +148,13 @@ class Account(NestedSet):
|
||||
if parent_acc_number:
|
||||
filters["account_number"] = parent_acc_number
|
||||
|
||||
for d in frappe.db.get_values('Account', filters=filters, fieldname=["company", "name"], as_dict=True):
|
||||
for d in frappe.db.get_values(
|
||||
"Account", filters=filters, fieldname=["company", "name"], as_dict=True
|
||||
):
|
||||
parent_acc_name_map[d["company"]] = d["name"]
|
||||
|
||||
if not parent_acc_name_map: return
|
||||
if not parent_acc_name_map:
|
||||
return
|
||||
|
||||
self.create_account_for_child_company(parent_acc_name_map, descendants, parent_acc_name)
|
||||
|
||||
@ -142,26 +175,38 @@ class Account(NestedSet):
|
||||
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"))
|
||||
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"))
|
||||
|
||||
def validate_balance_must_be_debit_or_credit(self):
|
||||
from erpnext.accounts.utils import get_balance_on
|
||||
|
||||
if not self.get("__islocal") and self.balance_must_be:
|
||||
account_balance = get_balance_on(self.name)
|
||||
|
||||
if account_balance > 0 and self.balance_must_be == "Credit":
|
||||
frappe.throw(_("Account balance already in Debit, you are not allowed to set 'Balance Must Be' as 'Credit'"))
|
||||
frappe.throw(
|
||||
_(
|
||||
"Account balance already in Debit, you are not allowed to set 'Balance Must Be' as 'Credit'"
|
||||
)
|
||||
)
|
||||
elif account_balance < 0 and self.balance_must_be == "Debit":
|
||||
frappe.throw(_("Account balance already in Credit, you are not allowed to set 'Balance Must Be' as 'Debit'"))
|
||||
frappe.throw(
|
||||
_(
|
||||
"Account balance already in Credit, you are not allowed to set 'Balance Must Be' as 'Debit'"
|
||||
)
|
||||
)
|
||||
|
||||
def validate_account_currency(self):
|
||||
if not self.account_currency:
|
||||
self.account_currency = frappe.get_cached_value('Company', self.company, "default_currency")
|
||||
self.account_currency = frappe.get_cached_value("Company", self.company, "default_currency")
|
||||
|
||||
elif self.account_currency != frappe.db.get_value("Account", self.name, "account_currency"):
|
||||
gl_currency = frappe.db.get_value("GL Entry", {"account": self.name}, "account_currency")
|
||||
|
||||
if gl_currency and self.account_currency != gl_currency:
|
||||
if frappe.db.get_value("GL Entry", {"account": self.name}):
|
||||
frappe.throw(_("Currency can not be changed after making entries using some other currency"))
|
||||
|
||||
@ -170,45 +215,52 @@ class Account(NestedSet):
|
||||
company_bold = frappe.bold(company)
|
||||
parent_acc_name_bold = frappe.bold(parent_acc_name)
|
||||
if not parent_acc_name_map.get(company):
|
||||
frappe.throw(_("While creating account for Child Company {0}, parent account {1} not found. Please create the parent account in corresponding COA")
|
||||
.format(company_bold, parent_acc_name_bold), title=_("Account Not Found"))
|
||||
frappe.throw(
|
||||
_(
|
||||
"While creating account for Child Company {0}, parent account {1} not found. Please create the parent account in corresponding COA"
|
||||
).format(company_bold, parent_acc_name_bold),
|
||||
title=_("Account Not Found"),
|
||||
)
|
||||
|
||||
# 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")):
|
||||
msg = _("While creating account for Child Company {0}, parent account {1} found as a ledger account.").format(company_bold, parent_acc_name_bold)
|
||||
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"
|
||||
):
|
||||
msg = _(
|
||||
"While creating account for Child Company {0}, parent account {1} found as a ledger account."
|
||||
).format(company_bold, parent_acc_name_bold)
|
||||
msg += "<br><br>"
|
||||
msg += _("Please convert the parent account in corresponding child company to a group account.")
|
||||
msg += _(
|
||||
"Please convert the parent account in corresponding child company to a group account."
|
||||
)
|
||||
frappe.throw(msg, title=_("Invalid Parent Account"))
|
||||
|
||||
filters = {
|
||||
"account_name": self.account_name,
|
||||
"company": company
|
||||
}
|
||||
filters = {"account_name": self.account_name, "company": company}
|
||||
|
||||
if self.account_number:
|
||||
filters["account_number"] = self.account_number
|
||||
|
||||
child_account = frappe.db.get_value("Account", filters, 'name')
|
||||
child_account = frappe.db.get_value("Account", filters, "name")
|
||||
if not child_account:
|
||||
doc = frappe.copy_doc(self)
|
||||
doc.flags.ignore_root_company_validation = True
|
||||
doc.update({
|
||||
"company": company,
|
||||
# parent account's currency should be passed down to child account's curreny
|
||||
# if it is None, it picks it up from default company currency, which might be unintended
|
||||
"account_currency": erpnext.get_company_currency(company),
|
||||
"parent_account": parent_acc_name_map[company]
|
||||
})
|
||||
doc.update(
|
||||
{
|
||||
"company": company,
|
||||
# parent account's currency should be passed down to child account's curreny
|
||||
# if it is None, it picks it up from default company currency, which might be unintended
|
||||
"account_currency": erpnext.get_company_currency(company),
|
||||
"parent_account": parent_acc_name_map[company],
|
||||
}
|
||||
)
|
||||
|
||||
doc.save()
|
||||
frappe.msgprint(_("Account {0} is added in the child company {1}")
|
||||
.format(doc.name, company))
|
||||
frappe.msgprint(_("Account {0} is added in the child company {1}").format(doc.name, company))
|
||||
elif child_account:
|
||||
# update the parent company's value in child companies
|
||||
doc = frappe.get_doc("Account", child_account)
|
||||
parent_value_changed = False
|
||||
for field in ['account_type', 'freeze_account', 'balance_must_be']:
|
||||
for field in ["account_type", "freeze_account", "balance_must_be"]:
|
||||
if doc.get(field) != self.get(field):
|
||||
parent_value_changed = True
|
||||
doc.set(field, self.get(field))
|
||||
@ -243,8 +295,11 @@ class Account(NestedSet):
|
||||
return frappe.db.get_value("GL Entry", {"account": self.name})
|
||||
|
||||
def check_if_child_exists(self):
|
||||
return frappe.db.sql("""select name from `tabAccount` where parent_account = %s
|
||||
and docstatus != 2""", self.name)
|
||||
return frappe.db.sql(
|
||||
"""select name from `tabAccount` where parent_account = %s
|
||||
and docstatus != 2""",
|
||||
self.name,
|
||||
)
|
||||
|
||||
def validate_mandatory(self):
|
||||
if not self.root_type:
|
||||
@ -260,73 +315,99 @@ class Account(NestedSet):
|
||||
|
||||
super(Account, self).on_trash(True)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_parent_account(doctype, txt, searchfield, start, page_len, filters):
|
||||
return frappe.db.sql("""select name from tabAccount
|
||||
return frappe.db.sql(
|
||||
"""select name from tabAccount
|
||||
where is_group = 1 and docstatus != 2 and company = %s
|
||||
and %s like %s order by name limit %s, %s""" %
|
||||
("%s", searchfield, "%s", "%s", "%s"),
|
||||
(filters["company"], "%%%s%%" % txt, start, page_len), as_list=1)
|
||||
and %s like %s order by name limit %s offset %s"""
|
||||
% ("%s", searchfield, "%s", "%s", "%s"),
|
||||
(filters["company"], "%%%s%%" % txt, page_len, start),
|
||||
as_list=1,
|
||||
)
|
||||
|
||||
|
||||
def get_account_currency(account):
|
||||
"""Helper function to get account currency"""
|
||||
if not account:
|
||||
return
|
||||
|
||||
def generator():
|
||||
account_currency, company = frappe.get_cached_value("Account", account, ["account_currency", "company"])
|
||||
account_currency, company = frappe.get_cached_value(
|
||||
"Account", account, ["account_currency", "company"]
|
||||
)
|
||||
if not account_currency:
|
||||
account_currency = frappe.get_cached_value('Company', company, "default_currency")
|
||||
account_currency = frappe.get_cached_value("Company", company, "default_currency")
|
||||
|
||||
return account_currency
|
||||
|
||||
return frappe.local_cache("account_currency", account, generator)
|
||||
|
||||
|
||||
def on_doctype_update():
|
||||
frappe.db.add_index("Account", ["lft", "rgt"])
|
||||
|
||||
|
||||
def get_account_autoname(account_number, account_name, company):
|
||||
# first validate if company exists
|
||||
company = frappe.get_cached_value('Company', company, ["abbr", "name"], as_dict=True)
|
||||
company = frappe.get_cached_value("Company", company, ["abbr", "name"], as_dict=True)
|
||||
if not company:
|
||||
frappe.throw(_('Company {0} does not exist').format(company))
|
||||
frappe.throw(_("Company {0} does not exist").format(company))
|
||||
|
||||
parts = [account_name.strip(), company.abbr]
|
||||
if cstr(account_number).strip():
|
||||
parts.insert(0, cstr(account_number).strip())
|
||||
return ' - '.join(parts)
|
||||
return " - ".join(parts)
|
||||
|
||||
|
||||
def validate_account_number(name, account_number, company):
|
||||
if account_number:
|
||||
account_with_same_number = frappe.db.get_value("Account",
|
||||
{"account_number": account_number, "company": company, "name": ["!=", name]})
|
||||
account_with_same_number = frappe.db.get_value(
|
||||
"Account", {"account_number": account_number, "company": company, "name": ["!=", name]}
|
||||
)
|
||||
if account_with_same_number:
|
||||
frappe.throw(_("Account Number {0} already used in account {1}")
|
||||
.format(account_number, account_with_same_number))
|
||||
frappe.throw(
|
||||
_("Account Number {0} already used in account {1}").format(
|
||||
account_number, account_with_same_number
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@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)
|
||||
if not account: return
|
||||
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 = frappe.db.get_value(
|
||||
"Account", name, ["account_name", "account_number"]
|
||||
)
|
||||
|
||||
# check if account exists in parent company
|
||||
ancestors = get_ancestors_of("Company", account.company)
|
||||
allow_independent_account_creation = frappe.get_value("Company", account.company, "allow_account_creation_against_child_company")
|
||||
allow_independent_account_creation = frappe.get_value(
|
||||
"Company", account.company, "allow_account_creation_against_child_company"
|
||||
)
|
||||
|
||||
if ancestors and not allow_independent_account_creation:
|
||||
for ancestor in ancestors:
|
||||
if frappe.db.get_value("Account", {'account_name': old_acc_name, 'company': ancestor}, 'name'):
|
||||
if frappe.db.get_value("Account", {"account_name": old_acc_name, "company": ancestor}, "name"):
|
||||
# same account in parent company exists
|
||||
allow_child_account_creation = _("Allow Account Creation Against Child Company")
|
||||
|
||||
message = _("Account {0} exists in parent company {1}.").format(frappe.bold(old_acc_name), frappe.bold(ancestor))
|
||||
message = _("Account {0} exists in parent company {1}.").format(
|
||||
frappe.bold(old_acc_name), frappe.bold(ancestor)
|
||||
)
|
||||
message += "<br>"
|
||||
message += _("Renaming it is only allowed via parent company {0}, to avoid mismatch.").format(frappe.bold(ancestor))
|
||||
message += _("Renaming it is only allowed via parent company {0}, to avoid mismatch.").format(
|
||||
frappe.bold(ancestor)
|
||||
)
|
||||
message += "<br><br>"
|
||||
message += _("To overrule this, enable '{0}' in company {1}").format(allow_child_account_creation, frappe.bold(account.company))
|
||||
message += _("To overrule this, enable '{0}' in company {1}").format(
|
||||
allow_child_account_creation, frappe.bold(account.company)
|
||||
)
|
||||
|
||||
frappe.throw(message, title=_("Rename Not Allowed"))
|
||||
|
||||
@ -339,42 +420,53 @@ def update_account_number(name, account_name, account_number=None, from_descenda
|
||||
|
||||
if not from_descendant:
|
||||
# Update and rename in child company accounts as well
|
||||
descendants = get_descendants_of('Company', account.company)
|
||||
descendants = get_descendants_of("Company", account.company)
|
||||
if descendants:
|
||||
sync_update_account_number_in_child(descendants, old_acc_name, account_name, account_number, old_acc_number)
|
||||
sync_update_account_number_in_child(
|
||||
descendants, old_acc_name, account_name, account_number, old_acc_number
|
||||
)
|
||||
|
||||
new_name = get_account_autoname(account_number, account_name, account.company)
|
||||
if name != new_name:
|
||||
frappe.rename_doc("Account", name, new_name, force=1)
|
||||
return new_name
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def merge_account(old, new, is_group, root_type, company):
|
||||
# Validate properties before merging
|
||||
if not frappe.db.exists("Account", new):
|
||||
throw(_("Account {0} does not exist").format(new))
|
||||
|
||||
val = list(frappe.db.get_value("Account", new,
|
||||
["is_group", "root_type", "company"]))
|
||||
val = list(frappe.db.get_value("Account", new, ["is_group", "root_type", "company"]))
|
||||
|
||||
if val != [cint(is_group), root_type, company]:
|
||||
throw(_("""Merging is only possible if following properties are same in both records. 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"))
|
||||
frappe.db.set_value(
|
||||
"Account", new, "parent_account", frappe.db.get_value("Account", old, "parent_account")
|
||||
)
|
||||
|
||||
frappe.rename_doc("Account", old, new, merge=1, force=1)
|
||||
|
||||
return new
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_root_company(company):
|
||||
# return the topmost company in the hierarchy
|
||||
ancestors = get_ancestors_of('Company', company, "lft asc")
|
||||
ancestors = get_ancestors_of("Company", company, "lft asc")
|
||||
return [ancestors[0]] if ancestors else []
|
||||
|
||||
def sync_update_account_number_in_child(descendants, old_acc_name, account_name, account_number=None, old_acc_number=None):
|
||||
|
||||
def sync_update_account_number_in_child(
|
||||
descendants, old_acc_name, account_name, account_number=None, old_acc_number=None
|
||||
):
|
||||
filters = {
|
||||
"company": ["in", descendants],
|
||||
"account_name": old_acc_name,
|
||||
@ -382,5 +474,7 @@ def sync_update_account_number_in_child(descendants, old_acc_name, account_name,
|
||||
if old_acc_number:
|
||||
filters["account_number"] = old_acc_number
|
||||
|
||||
for d in frappe.db.get_values('Account', filters=filters, fieldname=["company", "name"], as_dict=True):
|
||||
update_account_number(d["name"], account_name, account_number, from_descendant=True)
|
||||
for d in frappe.db.get_values(
|
||||
"Account", filters=filters, fieldname=["company", "name"], as_dict=True
|
||||
):
|
||||
update_account_number(d["name"], account_name, account_number, from_descendant=True)
|
||||
|
||||
@ -160,7 +160,7 @@ frappe.treeview_settings["Account"] = {
|
||||
let root_company = treeview.page.fields_dict.root_company.get_value();
|
||||
|
||||
if(root_company) {
|
||||
frappe.throw(__("Please add the account to root level Company - ") + root_company);
|
||||
frappe.throw(__("Please add the account to root level Company - {0}"), [root_company]);
|
||||
} else {
|
||||
treeview.new_node();
|
||||
}
|
||||
|
||||
@ -10,7 +10,9 @@ from frappe.utils.nestedset import rebuild_tree
|
||||
from unidecode import unidecode
|
||||
|
||||
|
||||
def create_charts(company, chart_template=None, existing_company=None, custom_chart=None, from_coa_importer=None):
|
||||
def create_charts(
|
||||
company, chart_template=None, existing_company=None, custom_chart=None, from_coa_importer=None
|
||||
):
|
||||
chart = custom_chart or get_chart(chart_template, existing_company)
|
||||
if chart:
|
||||
accounts = []
|
||||
@ -20,30 +22,41 @@ def create_charts(company, chart_template=None, existing_company=None, custom_ch
|
||||
if root_account:
|
||||
root_type = child.get("root_type")
|
||||
|
||||
if account_name not in ["account_name", "account_number", "account_type",
|
||||
"root_type", "is_group", "tax_rate"]:
|
||||
if account_name not in [
|
||||
"account_name",
|
||||
"account_number",
|
||||
"account_type",
|
||||
"root_type",
|
||||
"is_group",
|
||||
"tax_rate",
|
||||
]:
|
||||
|
||||
account_number = cstr(child.get("account_number")).strip()
|
||||
account_name, account_name_in_db = add_suffix_if_duplicate(account_name,
|
||||
account_number, accounts)
|
||||
account_name, account_name_in_db = add_suffix_if_duplicate(
|
||||
account_name, account_number, accounts
|
||||
)
|
||||
|
||||
is_group = identify_is_group(child)
|
||||
report_type = "Balance Sheet" if root_type in ["Asset", "Liability", "Equity"] \
|
||||
else "Profit and Loss"
|
||||
report_type = (
|
||||
"Balance Sheet" if root_type in ["Asset", "Liability", "Equity"] else "Profit and Loss"
|
||||
)
|
||||
|
||||
account = frappe.get_doc({
|
||||
"doctype": "Account",
|
||||
"account_name": child.get('account_name') if from_coa_importer else account_name,
|
||||
"company": company,
|
||||
"parent_account": parent,
|
||||
"is_group": is_group,
|
||||
"root_type": root_type,
|
||||
"report_type": report_type,
|
||||
"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"),
|
||||
"tax_rate": child.get("tax_rate")
|
||||
})
|
||||
account = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Account",
|
||||
"account_name": child.get("account_name") if from_coa_importer else account_name,
|
||||
"company": company,
|
||||
"parent_account": parent,
|
||||
"is_group": is_group,
|
||||
"root_type": root_type,
|
||||
"report_type": report_type,
|
||||
"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"),
|
||||
"tax_rate": child.get("tax_rate"),
|
||||
}
|
||||
)
|
||||
|
||||
if root_account or frappe.local.flags.allow_unverified_charts:
|
||||
account.flags.ignore_mandatory = True
|
||||
@ -63,10 +76,10 @@ def create_charts(company, chart_template=None, existing_company=None, custom_ch
|
||||
rebuild_tree("Account", "parent_account")
|
||||
frappe.local.flags.ignore_update_nsm = False
|
||||
|
||||
|
||||
def add_suffix_if_duplicate(account_name, account_number, accounts):
|
||||
if account_number:
|
||||
account_name_in_db = unidecode(" - ".join([account_number,
|
||||
account_name.strip().lower()]))
|
||||
account_name_in_db = unidecode(" - ".join([account_number, account_name.strip().lower()]))
|
||||
else:
|
||||
account_name_in_db = unidecode(account_name.strip().lower())
|
||||
|
||||
@ -76,16 +89,21 @@ def add_suffix_if_duplicate(account_name, account_number, accounts):
|
||||
|
||||
return account_name, account_name_in_db
|
||||
|
||||
|
||||
def identify_is_group(child):
|
||||
if child.get("is_group"):
|
||||
is_group = child.get("is_group")
|
||||
elif len(set(child.keys()) - set(["account_name", "account_type", "root_type", "is_group", "tax_rate", "account_number"])):
|
||||
elif len(
|
||||
set(child.keys())
|
||||
- set(["account_name", "account_type", "root_type", "is_group", "tax_rate", "account_number"])
|
||||
):
|
||||
is_group = 1
|
||||
else:
|
||||
is_group = 0
|
||||
|
||||
return is_group
|
||||
|
||||
|
||||
def get_chart(chart_template, existing_company=None):
|
||||
chart = {}
|
||||
if existing_company:
|
||||
@ -95,11 +113,13 @@ def get_chart(chart_template, existing_company=None):
|
||||
from erpnext.accounts.doctype.account.chart_of_accounts.verified import (
|
||||
standard_chart_of_accounts,
|
||||
)
|
||||
|
||||
return standard_chart_of_accounts.get()
|
||||
elif chart_template == "Standard with Numbers":
|
||||
from erpnext.accounts.doctype.account.chart_of_accounts.verified import (
|
||||
standard_chart_of_accounts_with_account_number,
|
||||
)
|
||||
|
||||
return standard_chart_of_accounts_with_account_number.get()
|
||||
else:
|
||||
folders = ("verified",)
|
||||
@ -115,6 +135,7 @@ def get_chart(chart_template, existing_company=None):
|
||||
if chart and json.loads(chart).get("name") == chart_template:
|
||||
return json.loads(chart).get("tree")
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_charts_for_country(country, with_standard=False):
|
||||
charts = []
|
||||
@ -122,9 +143,10 @@ def get_charts_for_country(country, with_standard=False):
|
||||
def _get_chart_name(content):
|
||||
if content:
|
||||
content = json.loads(content)
|
||||
if (content and content.get("disabled", "No") == "No") \
|
||||
or frappe.local.flags.allow_unverified_charts:
|
||||
charts.append(content["name"])
|
||||
if (
|
||||
content and content.get("disabled", "No") == "No"
|
||||
) or frappe.local.flags.allow_unverified_charts:
|
||||
charts.append(content["name"])
|
||||
|
||||
country_code = frappe.db.get_value("Country", country, "code")
|
||||
if country_code:
|
||||
@ -151,11 +173,21 @@ def get_charts_for_country(country, with_standard=False):
|
||||
|
||||
|
||||
def get_account_tree_from_existing_company(existing_company):
|
||||
all_accounts = frappe.get_all('Account',
|
||||
filters={'company': existing_company},
|
||||
fields = ["name", "account_name", "parent_account", "account_type",
|
||||
"is_group", "root_type", "tax_rate", "account_number"],
|
||||
order_by="lft, rgt")
|
||||
all_accounts = frappe.get_all(
|
||||
"Account",
|
||||
filters={"company": existing_company},
|
||||
fields=[
|
||||
"name",
|
||||
"account_name",
|
||||
"parent_account",
|
||||
"account_type",
|
||||
"is_group",
|
||||
"root_type",
|
||||
"tax_rate",
|
||||
"account_number",
|
||||
],
|
||||
order_by="lft, rgt",
|
||||
)
|
||||
|
||||
account_tree = {}
|
||||
|
||||
@ -164,6 +196,7 @@ def get_account_tree_from_existing_company(existing_company):
|
||||
build_account_tree(account_tree, None, all_accounts)
|
||||
return account_tree
|
||||
|
||||
|
||||
def build_account_tree(tree, parent, all_accounts):
|
||||
# find children
|
||||
parent_account = parent.name if parent else ""
|
||||
@ -192,27 +225,29 @@ def build_account_tree(tree, parent, all_accounts):
|
||||
# call recursively to build a subtree for current account
|
||||
build_account_tree(tree[child.account_name], child, all_accounts)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def validate_bank_account(coa, bank_account):
|
||||
accounts = []
|
||||
chart = get_chart(coa)
|
||||
|
||||
if chart:
|
||||
|
||||
def _get_account_names(account_master):
|
||||
for account_name, child in account_master.items():
|
||||
if account_name not in ["account_number", "account_type",
|
||||
"root_type", "is_group", "tax_rate"]:
|
||||
if account_name not in ["account_number", "account_type", "root_type", "is_group", "tax_rate"]:
|
||||
accounts.append(account_name)
|
||||
|
||||
_get_account_names(child)
|
||||
|
||||
_get_account_names(chart)
|
||||
|
||||
return (bank_account in accounts)
|
||||
return bank_account in accounts
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def build_tree_from_json(chart_template, chart_data=None, from_coa_importer=False):
|
||||
''' get chart template from its folder and parse the json to be rendered as tree '''
|
||||
"""get chart template from its folder and parse the json to be rendered as tree"""
|
||||
chart = chart_data or get_chart(chart_template)
|
||||
|
||||
# if no template selected, return as it is
|
||||
@ -220,22 +255,33 @@ def build_tree_from_json(chart_template, chart_data=None, from_coa_importer=Fals
|
||||
return
|
||||
|
||||
accounts = []
|
||||
|
||||
def _import_accounts(children, parent):
|
||||
''' recursively called to form a parent-child based list of dict from chart template '''
|
||||
"""recursively called to form a parent-child based list of dict from chart template"""
|
||||
for account_name, child in children.items():
|
||||
account = {}
|
||||
if account_name in ["account_name", "account_number", "account_type",\
|
||||
"root_type", "is_group", "tax_rate"]: continue
|
||||
if account_name in [
|
||||
"account_name",
|
||||
"account_number",
|
||||
"account_type",
|
||||
"root_type",
|
||||
"is_group",
|
||||
"tax_rate",
|
||||
]:
|
||||
continue
|
||||
|
||||
if from_coa_importer:
|
||||
account_name = child['account_name']
|
||||
account_name = child["account_name"]
|
||||
|
||||
account['parent_account'] = parent
|
||||
account['expandable'] = True if identify_is_group(child) else False
|
||||
account['value'] = (cstr(child.get('account_number')).strip() + ' - ' + account_name) \
|
||||
if child.get('account_number') else account_name
|
||||
account["parent_account"] = parent
|
||||
account["expandable"] = True if identify_is_group(child) else False
|
||||
account["value"] = (
|
||||
(cstr(child.get("account_number")).strip() + " - " + account_name)
|
||||
if child.get("account_number")
|
||||
else account_name
|
||||
)
|
||||
accounts.append(account)
|
||||
_import_accounts(child, account['value'])
|
||||
_import_accounts(child, account["value"])
|
||||
|
||||
_import_accounts(chart, None)
|
||||
return accounts
|
||||
|
||||
@ -20,6 +20,7 @@ charts = {}
|
||||
all_account_types = []
|
||||
all_roots = {}
|
||||
|
||||
|
||||
def go():
|
||||
global accounts, charts
|
||||
default_account_types = get_default_account_types()
|
||||
@ -34,14 +35,16 @@ def go():
|
||||
accounts, charts = {}, {}
|
||||
country_path = os.path.join(path, country_dir)
|
||||
manifest = ast.literal_eval(open(os.path.join(country_path, "__openerp__.py")).read())
|
||||
data_files = manifest.get("data", []) + manifest.get("init_xml", []) + \
|
||||
manifest.get("update_xml", [])
|
||||
data_files = (
|
||||
manifest.get("data", []) + manifest.get("init_xml", []) + manifest.get("update_xml", [])
|
||||
)
|
||||
files_path = [os.path.join(country_path, d) for d in data_files]
|
||||
xml_roots = get_xml_roots(files_path)
|
||||
csv_content = get_csv_contents(files_path)
|
||||
prefix = country_dir if csv_content else None
|
||||
account_types = get_account_types(xml_roots.get("account.account.type", []),
|
||||
csv_content.get("account.account.type", []), prefix)
|
||||
account_types = get_account_types(
|
||||
xml_roots.get("account.account.type", []), csv_content.get("account.account.type", []), prefix
|
||||
)
|
||||
account_types.update(default_account_types)
|
||||
|
||||
if xml_roots:
|
||||
@ -54,12 +57,15 @@ def go():
|
||||
|
||||
create_all_roots_file()
|
||||
|
||||
|
||||
def get_default_account_types():
|
||||
default_types_root = []
|
||||
default_types_root.append(ET.parse(os.path.join(path, "account", "data",
|
||||
"data_account_type.xml")).getroot())
|
||||
default_types_root.append(
|
||||
ET.parse(os.path.join(path, "account", "data", "data_account_type.xml")).getroot()
|
||||
)
|
||||
return get_account_types(default_types_root, None, prefix="account")
|
||||
|
||||
|
||||
def get_xml_roots(files_path):
|
||||
xml_roots = frappe._dict()
|
||||
for filepath in files_path:
|
||||
@ -68,64 +74,69 @@ def get_xml_roots(files_path):
|
||||
tree = ET.parse(filepath)
|
||||
root = tree.getroot()
|
||||
for node in root[0].findall("record"):
|
||||
if node.get("model") in ["account.account.template",
|
||||
"account.chart.template", "account.account.type"]:
|
||||
if node.get("model") in [
|
||||
"account.account.template",
|
||||
"account.chart.template",
|
||||
"account.account.type",
|
||||
]:
|
||||
xml_roots.setdefault(node.get("model"), []).append(root)
|
||||
break
|
||||
return xml_roots
|
||||
|
||||
|
||||
def get_csv_contents(files_path):
|
||||
csv_content = {}
|
||||
for filepath in files_path:
|
||||
fname = os.path.basename(filepath)
|
||||
for file_type in ["account.account.template", "account.account.type",
|
||||
"account.chart.template"]:
|
||||
for file_type in ["account.account.template", "account.account.type", "account.chart.template"]:
|
||||
if fname.startswith(file_type) and fname.endswith(".csv"):
|
||||
with open(filepath, "r") as csvfile:
|
||||
try:
|
||||
csv_content.setdefault(file_type, [])\
|
||||
.append(read_csv_content(csvfile.read()))
|
||||
csv_content.setdefault(file_type, []).append(read_csv_content(csvfile.read()))
|
||||
except Exception as e:
|
||||
continue
|
||||
return csv_content
|
||||
|
||||
|
||||
def get_account_types(root_list, csv_content, prefix=None):
|
||||
types = {}
|
||||
account_type_map = {
|
||||
'cash': 'Cash',
|
||||
'bank': 'Bank',
|
||||
'tr_cash': 'Cash',
|
||||
'tr_bank': 'Bank',
|
||||
'receivable': 'Receivable',
|
||||
'tr_receivable': 'Receivable',
|
||||
'account rec': 'Receivable',
|
||||
'payable': 'Payable',
|
||||
'tr_payable': 'Payable',
|
||||
'equity': 'Equity',
|
||||
'stocks': 'Stock',
|
||||
'stock': 'Stock',
|
||||
'tax': 'Tax',
|
||||
'tr_tax': 'Tax',
|
||||
'tax-out': 'Tax',
|
||||
'tax-in': 'Tax',
|
||||
'charges_personnel': 'Chargeable',
|
||||
'fixed asset': 'Fixed Asset',
|
||||
'cogs': 'Cost of Goods Sold',
|
||||
|
||||
"cash": "Cash",
|
||||
"bank": "Bank",
|
||||
"tr_cash": "Cash",
|
||||
"tr_bank": "Bank",
|
||||
"receivable": "Receivable",
|
||||
"tr_receivable": "Receivable",
|
||||
"account rec": "Receivable",
|
||||
"payable": "Payable",
|
||||
"tr_payable": "Payable",
|
||||
"equity": "Equity",
|
||||
"stocks": "Stock",
|
||||
"stock": "Stock",
|
||||
"tax": "Tax",
|
||||
"tr_tax": "Tax",
|
||||
"tax-out": "Tax",
|
||||
"tax-in": "Tax",
|
||||
"charges_personnel": "Chargeable",
|
||||
"fixed asset": "Fixed Asset",
|
||||
"cogs": "Cost of Goods Sold",
|
||||
}
|
||||
for root in root_list:
|
||||
for node in root[0].findall("record"):
|
||||
if node.get("model")=="account.account.type":
|
||||
if node.get("model") == "account.account.type":
|
||||
data = {}
|
||||
for field in node.findall("field"):
|
||||
if field.get("name")=="code" and field.text.lower() != "none" \
|
||||
and account_type_map.get(field.text):
|
||||
data["account_type"] = account_type_map[field.text]
|
||||
if (
|
||||
field.get("name") == "code"
|
||||
and field.text.lower() != "none"
|
||||
and account_type_map.get(field.text)
|
||||
):
|
||||
data["account_type"] = account_type_map[field.text]
|
||||
|
||||
node_id = prefix + "." + node.get("id") if prefix else node.get("id")
|
||||
types[node_id] = data
|
||||
|
||||
if csv_content and csv_content[0][0]=="id":
|
||||
if csv_content and csv_content[0][0] == "id":
|
||||
for row in csv_content[1:]:
|
||||
row_dict = dict(zip(csv_content[0], row))
|
||||
data = {}
|
||||
@ -136,21 +147,22 @@ def get_account_types(root_list, csv_content, prefix=None):
|
||||
types[node_id] = data
|
||||
return types
|
||||
|
||||
|
||||
def make_maps_for_xml(xml_roots, account_types, country_dir):
|
||||
"""make maps for `charts` and `accounts`"""
|
||||
for model, root_list in xml_roots.items():
|
||||
for root in root_list:
|
||||
for node in root[0].findall("record"):
|
||||
if node.get("model")=="account.account.template":
|
||||
if node.get("model") == "account.account.template":
|
||||
data = {}
|
||||
for field in node.findall("field"):
|
||||
if field.get("name")=="name":
|
||||
if field.get("name") == "name":
|
||||
data["name"] = field.text
|
||||
if field.get("name")=="parent_id":
|
||||
if field.get("name") == "parent_id":
|
||||
parent_id = field.get("ref") or field.get("eval")
|
||||
data["parent_id"] = parent_id
|
||||
|
||||
if field.get("name")=="user_type":
|
||||
if field.get("name") == "user_type":
|
||||
value = field.get("ref")
|
||||
if account_types.get(value, {}).get("account_type"):
|
||||
data["account_type"] = account_types[value]["account_type"]
|
||||
@ -160,16 +172,17 @@ def make_maps_for_xml(xml_roots, account_types, country_dir):
|
||||
data["children"] = []
|
||||
accounts[node.get("id")] = data
|
||||
|
||||
if node.get("model")=="account.chart.template":
|
||||
if node.get("model") == "account.chart.template":
|
||||
data = {}
|
||||
for field in node.findall("field"):
|
||||
if field.get("name")=="name":
|
||||
if field.get("name") == "name":
|
||||
data["name"] = field.text
|
||||
if field.get("name")=="account_root_id":
|
||||
if field.get("name") == "account_root_id":
|
||||
data["account_root_id"] = field.get("ref")
|
||||
data["id"] = country_dir
|
||||
charts.setdefault(node.get("id"), {}).update(data)
|
||||
|
||||
|
||||
def make_maps_for_csv(csv_content, account_types, country_dir):
|
||||
for content in csv_content.get("account.account.template", []):
|
||||
for row in content[1:]:
|
||||
@ -177,7 +190,7 @@ def make_maps_for_csv(csv_content, account_types, country_dir):
|
||||
account = {
|
||||
"name": data.get("name"),
|
||||
"parent_id": data.get("parent_id:id") or data.get("parent_id/id"),
|
||||
"children": []
|
||||
"children": [],
|
||||
}
|
||||
user_type = data.get("user_type/id") or data.get("user_type:id")
|
||||
if account_types.get(user_type, {}).get("account_type"):
|
||||
@ -194,12 +207,14 @@ def make_maps_for_csv(csv_content, account_types, country_dir):
|
||||
for row in content[1:]:
|
||||
if row:
|
||||
data = dict(zip(content[0], row))
|
||||
charts.setdefault(data.get("id"), {}).update({
|
||||
"account_root_id": data.get("account_root_id:id") or \
|
||||
data.get("account_root_id/id"),
|
||||
"name": data.get("name"),
|
||||
"id": country_dir
|
||||
})
|
||||
charts.setdefault(data.get("id"), {}).update(
|
||||
{
|
||||
"account_root_id": data.get("account_root_id:id") or data.get("account_root_id/id"),
|
||||
"name": data.get("name"),
|
||||
"id": country_dir,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def make_account_trees():
|
||||
"""build tree hierarchy"""
|
||||
@ -218,6 +233,7 @@ def make_account_trees():
|
||||
if "children" in accounts[id] and not accounts[id].get("children"):
|
||||
del accounts[id]["children"]
|
||||
|
||||
|
||||
def make_charts():
|
||||
"""write chart files in app/setup/doctype/company/charts"""
|
||||
for chart_id in charts:
|
||||
@ -236,34 +252,38 @@ def make_charts():
|
||||
chart["country_code"] = src["id"][5:]
|
||||
chart["tree"] = accounts[src["account_root_id"]]
|
||||
|
||||
|
||||
for key, val in chart["tree"].items():
|
||||
if key in ["name", "parent_id"]:
|
||||
chart["tree"].pop(key)
|
||||
if type(val) == dict:
|
||||
val["root_type"] = ""
|
||||
if chart:
|
||||
fpath = os.path.join("erpnext", "erpnext", "accounts", "doctype", "account",
|
||||
"chart_of_accounts", filename + ".json")
|
||||
fpath = os.path.join(
|
||||
"erpnext", "erpnext", "accounts", "doctype", "account", "chart_of_accounts", filename + ".json"
|
||||
)
|
||||
|
||||
with open(fpath, "r") as chartfile:
|
||||
old_content = chartfile.read()
|
||||
if not old_content or (json.loads(old_content).get("is_active", "No") == "No" \
|
||||
and json.loads(old_content).get("disabled", "No") == "No"):
|
||||
if not old_content or (
|
||||
json.loads(old_content).get("is_active", "No") == "No"
|
||||
and json.loads(old_content).get("disabled", "No") == "No"
|
||||
):
|
||||
with open(fpath, "w") as chartfile:
|
||||
chartfile.write(json.dumps(chart, indent=4, sort_keys=True))
|
||||
|
||||
all_roots.setdefault(filename, chart["tree"].keys())
|
||||
|
||||
|
||||
def create_all_roots_file():
|
||||
with open('all_roots.txt', 'w') as f:
|
||||
with open("all_roots.txt", "w") as f:
|
||||
for filename, roots in sorted(all_roots.items()):
|
||||
f.write(filename)
|
||||
f.write('\n----------------------\n')
|
||||
f.write("\n----------------------\n")
|
||||
for r in sorted(roots):
|
||||
f.write(r.encode('utf-8'))
|
||||
f.write('\n')
|
||||
f.write('\n\n\n')
|
||||
f.write(r.encode("utf-8"))
|
||||
f.write("\n")
|
||||
f.write("\n\n\n")
|
||||
|
||||
if __name__=="__main__":
|
||||
|
||||
if __name__ == "__main__":
|
||||
go()
|
||||
|
||||
@ -7,182 +7,103 @@ from frappe import _
|
||||
|
||||
def get():
|
||||
return {
|
||||
_("Application of Funds (Assets)"): {
|
||||
_("Current Assets"): {
|
||||
_("Accounts Receivable"): {
|
||||
_("Debtors"): {
|
||||
"account_type": "Receivable"
|
||||
}
|
||||
},
|
||||
_("Bank Accounts"): {
|
||||
"account_type": "Bank",
|
||||
"is_group": 1
|
||||
},
|
||||
_("Cash In Hand"): {
|
||||
_("Cash"): {
|
||||
"account_type": "Cash"
|
||||
},
|
||||
"account_type": "Cash"
|
||||
},
|
||||
_("Loans and Advances (Assets)"): {
|
||||
_("Employee Advances"): {
|
||||
},
|
||||
},
|
||||
_("Securities and Deposits"): {
|
||||
_("Earnest Money"): {}
|
||||
},
|
||||
_("Stock Assets"): {
|
||||
_("Stock In Hand"): {
|
||||
"account_type": "Stock"
|
||||
},
|
||||
"account_type": "Stock",
|
||||
},
|
||||
_("Tax Assets"): {
|
||||
"is_group": 1
|
||||
}
|
||||
},
|
||||
_("Fixed Assets"): {
|
||||
_("Capital Equipments"): {
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
_("Electronic Equipments"): {
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
_("Furnitures and Fixtures"): {
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
_("Office Equipments"): {
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
_("Plants and Machineries"): {
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
_("Buildings"): {
|
||||
"account_type": "Fixed Asset"
|
||||
_("Application of Funds (Assets)"): {
|
||||
_("Current Assets"): {
|
||||
_("Accounts Receivable"): {_("Debtors"): {"account_type": "Receivable"}},
|
||||
_("Bank Accounts"): {"account_type": "Bank", "is_group": 1},
|
||||
_("Cash In Hand"): {_("Cash"): {"account_type": "Cash"}, "account_type": "Cash"},
|
||||
_("Loans and Advances (Assets)"): {
|
||||
_("Employee Advances"): {},
|
||||
},
|
||||
_("Softwares"): {
|
||||
"account_type": "Fixed Asset"
|
||||
_("Securities and Deposits"): {_("Earnest Money"): {}},
|
||||
_("Stock Assets"): {
|
||||
_("Stock In Hand"): {"account_type": "Stock"},
|
||||
"account_type": "Stock",
|
||||
},
|
||||
_("Accumulated Depreciation"): {
|
||||
"account_type": "Accumulated Depreciation"
|
||||
},
|
||||
_("CWIP Account"): {
|
||||
"account_type": "Capital Work in Progress",
|
||||
}
|
||||
},
|
||||
_("Investments"): {
|
||||
"is_group": 1
|
||||
},
|
||||
_("Temporary Accounts"): {
|
||||
_("Temporary Opening"): {
|
||||
"account_type": "Temporary"
|
||||
}
|
||||
},
|
||||
"root_type": "Asset"
|
||||
},
|
||||
_("Expenses"): {
|
||||
_("Direct Expenses"): {
|
||||
_("Stock Expenses"): {
|
||||
_("Cost of Goods Sold"): {
|
||||
"account_type": "Cost of Goods Sold"
|
||||
},
|
||||
_("Expenses Included In Asset Valuation"): {
|
||||
"account_type": "Expenses Included In Asset Valuation"
|
||||
},
|
||||
_("Expenses Included In Valuation"): {
|
||||
"account_type": "Expenses Included In Valuation"
|
||||
},
|
||||
_("Stock Adjustment"): {
|
||||
"account_type": "Stock Adjustment"
|
||||
}
|
||||
},
|
||||
},
|
||||
_("Indirect Expenses"): {
|
||||
_("Administrative Expenses"): {},
|
||||
_("Commission on Sales"): {},
|
||||
_("Depreciation"): {
|
||||
"account_type": "Depreciation"
|
||||
},
|
||||
_("Entertainment Expenses"): {},
|
||||
_("Freight and Forwarding Charges"): {
|
||||
"account_type": "Chargeable"
|
||||
},
|
||||
_("Legal Expenses"): {},
|
||||
_("Marketing Expenses"): {
|
||||
"account_type": "Chargeable"
|
||||
},
|
||||
_("Miscellaneous Expenses"): {
|
||||
"account_type": "Chargeable"
|
||||
},
|
||||
_("Office Maintenance Expenses"): {},
|
||||
_("Office Rent"): {},
|
||||
_("Postal Expenses"): {},
|
||||
_("Print and Stationery"): {},
|
||||
_("Round Off"): {
|
||||
"account_type": "Round Off"
|
||||
},
|
||||
_("Salary"): {},
|
||||
_("Sales Expenses"): {},
|
||||
_("Telephone Expenses"): {},
|
||||
_("Travel Expenses"): {},
|
||||
_("Utility Expenses"): {},
|
||||
_("Tax Assets"): {"is_group": 1},
|
||||
},
|
||||
_("Fixed Assets"): {
|
||||
_("Capital Equipments"): {"account_type": "Fixed Asset"},
|
||||
_("Electronic Equipments"): {"account_type": "Fixed Asset"},
|
||||
_("Furnitures and Fixtures"): {"account_type": "Fixed Asset"},
|
||||
_("Office Equipments"): {"account_type": "Fixed Asset"},
|
||||
_("Plants and Machineries"): {"account_type": "Fixed Asset"},
|
||||
_("Buildings"): {"account_type": "Fixed Asset"},
|
||||
_("Softwares"): {"account_type": "Fixed Asset"},
|
||||
_("Accumulated Depreciation"): {"account_type": "Accumulated Depreciation"},
|
||||
_("CWIP Account"): {
|
||||
"account_type": "Capital Work in Progress",
|
||||
},
|
||||
},
|
||||
_("Investments"): {"is_group": 1},
|
||||
_("Temporary Accounts"): {_("Temporary Opening"): {"account_type": "Temporary"}},
|
||||
"root_type": "Asset",
|
||||
},
|
||||
_("Expenses"): {
|
||||
_("Direct Expenses"): {
|
||||
_("Stock Expenses"): {
|
||||
_("Cost of Goods Sold"): {"account_type": "Cost of Goods Sold"},
|
||||
_("Expenses Included In Asset Valuation"): {
|
||||
"account_type": "Expenses Included In Asset Valuation"
|
||||
},
|
||||
_("Expenses Included In Valuation"): {"account_type": "Expenses Included In Valuation"},
|
||||
_("Stock Adjustment"): {"account_type": "Stock Adjustment"},
|
||||
},
|
||||
},
|
||||
_("Indirect Expenses"): {
|
||||
_("Administrative Expenses"): {},
|
||||
_("Commission on Sales"): {},
|
||||
_("Depreciation"): {"account_type": "Depreciation"},
|
||||
_("Entertainment Expenses"): {},
|
||||
_("Freight and Forwarding Charges"): {"account_type": "Chargeable"},
|
||||
_("Legal Expenses"): {},
|
||||
_("Marketing Expenses"): {"account_type": "Chargeable"},
|
||||
_("Miscellaneous Expenses"): {"account_type": "Chargeable"},
|
||||
_("Office Maintenance Expenses"): {},
|
||||
_("Office Rent"): {},
|
||||
_("Postal Expenses"): {},
|
||||
_("Print and Stationery"): {},
|
||||
_("Round Off"): {"account_type": "Round Off"},
|
||||
_("Salary"): {},
|
||||
_("Sales Expenses"): {},
|
||||
_("Telephone Expenses"): {},
|
||||
_("Travel Expenses"): {},
|
||||
_("Utility Expenses"): {},
|
||||
_("Write Off"): {},
|
||||
_("Exchange Gain/Loss"): {},
|
||||
_("Gain/Loss on Asset Disposal"): {}
|
||||
},
|
||||
"root_type": "Expense"
|
||||
},
|
||||
_("Income"): {
|
||||
_("Direct Income"): {
|
||||
_("Sales"): {},
|
||||
_("Service"): {}
|
||||
},
|
||||
_("Indirect Income"): {
|
||||
"is_group": 1
|
||||
},
|
||||
"root_type": "Income"
|
||||
},
|
||||
_("Source of Funds (Liabilities)"): {
|
||||
_("Current Liabilities"): {
|
||||
_("Accounts Payable"): {
|
||||
_("Creditors"): {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
_("Payroll Payable"): {},
|
||||
},
|
||||
_("Stock Liabilities"): {
|
||||
_("Stock Received But Not Billed"): {
|
||||
"account_type": "Stock Received But Not Billed"
|
||||
},
|
||||
_("Asset Received But Not Billed"): {
|
||||
"account_type": "Asset Received But Not Billed"
|
||||
}
|
||||
},
|
||||
_("Duties and Taxes"): {
|
||||
"account_type": "Tax",
|
||||
"is_group": 1
|
||||
_("Gain/Loss on Asset Disposal"): {},
|
||||
},
|
||||
"root_type": "Expense",
|
||||
},
|
||||
_("Income"): {
|
||||
_("Direct Income"): {_("Sales"): {}, _("Service"): {}},
|
||||
_("Indirect Income"): {"is_group": 1},
|
||||
"root_type": "Income",
|
||||
},
|
||||
_("Source of Funds (Liabilities)"): {
|
||||
_("Current Liabilities"): {
|
||||
_("Accounts Payable"): {
|
||||
_("Creditors"): {"account_type": "Payable"},
|
||||
_("Payroll Payable"): {},
|
||||
},
|
||||
_("Stock Liabilities"): {
|
||||
_("Stock Received But Not Billed"): {"account_type": "Stock Received But Not Billed"},
|
||||
_("Asset Received But Not Billed"): {"account_type": "Asset Received But Not Billed"},
|
||||
},
|
||||
_("Duties and Taxes"): {"account_type": "Tax", "is_group": 1},
|
||||
_("Loans (Liabilities)"): {
|
||||
_("Secured Loans"): {},
|
||||
_("Unsecured Loans"): {},
|
||||
_("Bank Overdraft Account"): {},
|
||||
},
|
||||
},
|
||||
"root_type": "Liability"
|
||||
},
|
||||
},
|
||||
"root_type": "Liability",
|
||||
},
|
||||
_("Equity"): {
|
||||
_("Capital Stock"): {
|
||||
"account_type": "Equity"
|
||||
},
|
||||
_("Dividends Paid"): {
|
||||
"account_type": "Equity"
|
||||
},
|
||||
_("Opening Balance Equity"): {
|
||||
"account_type": "Equity"
|
||||
},
|
||||
_("Retained Earnings"): {
|
||||
"account_type": "Equity"
|
||||
},
|
||||
"root_type": "Equity"
|
||||
}
|
||||
_("Capital Stock"): {"account_type": "Equity"},
|
||||
_("Dividends Paid"): {"account_type": "Equity"},
|
||||
_("Opening Balance Equity"): {"account_type": "Equity"},
|
||||
_("Retained Earnings"): {"account_type": "Equity"},
|
||||
"root_type": "Equity",
|
||||
},
|
||||
}
|
||||
|
||||
@ -6,288 +6,153 @@ from frappe import _
|
||||
|
||||
|
||||
def get():
|
||||
return {
|
||||
_("Application of Funds (Assets)"): {
|
||||
_("Current Assets"): {
|
||||
_("Accounts Receivable"): {
|
||||
_("Debtors"): {
|
||||
"account_type": "Receivable",
|
||||
"account_number": "1310"
|
||||
},
|
||||
"account_number": "1300"
|
||||
},
|
||||
_("Bank Accounts"): {
|
||||
"account_type": "Bank",
|
||||
"is_group": 1,
|
||||
"account_number": "1200"
|
||||
},
|
||||
_("Cash In Hand"): {
|
||||
_("Cash"): {
|
||||
"account_type": "Cash",
|
||||
"account_number": "1110"
|
||||
},
|
||||
"account_type": "Cash",
|
||||
"account_number": "1100"
|
||||
},
|
||||
_("Loans and Advances (Assets)"): {
|
||||
_("Employee Advances"): {
|
||||
"account_number": "1610"
|
||||
},
|
||||
"account_number": "1600"
|
||||
},
|
||||
_("Securities and Deposits"): {
|
||||
_("Earnest Money"): {
|
||||
"account_number": "1651"
|
||||
},
|
||||
"account_number": "1650"
|
||||
},
|
||||
_("Stock Assets"): {
|
||||
_("Stock In Hand"): {
|
||||
"account_type": "Stock",
|
||||
"account_number": "1410"
|
||||
},
|
||||
"account_type": "Stock",
|
||||
"account_number": "1400"
|
||||
},
|
||||
_("Tax Assets"): {
|
||||
"is_group": 1,
|
||||
"account_number": "1500"
|
||||
},
|
||||
"account_number": "1100-1600"
|
||||
},
|
||||
_("Fixed Assets"): {
|
||||
_("Capital Equipments"): {
|
||||
"account_type": "Fixed Asset",
|
||||
"account_number": "1710"
|
||||
},
|
||||
_("Electronic Equipments"): {
|
||||
"account_type": "Fixed Asset",
|
||||
"account_number": "1720"
|
||||
},
|
||||
_("Furnitures and Fixtures"): {
|
||||
"account_type": "Fixed Asset",
|
||||
"account_number": "1730"
|
||||
},
|
||||
_("Office Equipments"): {
|
||||
"account_type": "Fixed Asset",
|
||||
"account_number": "1740"
|
||||
},
|
||||
_("Plants and Machineries"): {
|
||||
"account_type": "Fixed Asset",
|
||||
"account_number": "1750"
|
||||
},
|
||||
_("Buildings"): {
|
||||
"account_type": "Fixed Asset",
|
||||
"account_number": "1760"
|
||||
},
|
||||
_("Softwares"): {
|
||||
"account_type": "Fixed Asset",
|
||||
"account_number": "1770"
|
||||
},
|
||||
_("Accumulated Depreciation"): {
|
||||
"account_type": "Accumulated Depreciation",
|
||||
"account_number": "1780"
|
||||
},
|
||||
_("CWIP Account"): {
|
||||
"account_type": "Capital Work in Progress",
|
||||
"account_number": "1790"
|
||||
},
|
||||
"account_number": "1700"
|
||||
},
|
||||
_("Investments"): {
|
||||
"is_group": 1,
|
||||
"account_number": "1800"
|
||||
},
|
||||
_("Temporary Accounts"): {
|
||||
_("Temporary Opening"): {
|
||||
"account_type": "Temporary",
|
||||
"account_number": "1910"
|
||||
},
|
||||
"account_number": "1900"
|
||||
},
|
||||
"root_type": "Asset",
|
||||
"account_number": "1000"
|
||||
},
|
||||
_("Expenses"): {
|
||||
_("Direct Expenses"): {
|
||||
_("Stock Expenses"): {
|
||||
_("Cost of Goods Sold"): {
|
||||
"account_type": "Cost of Goods Sold",
|
||||
"account_number": "5111"
|
||||
},
|
||||
_("Expenses Included In Asset Valuation"): {
|
||||
"account_type": "Expenses Included In Asset Valuation",
|
||||
"account_number": "5112"
|
||||
},
|
||||
_("Expenses Included In Valuation"): {
|
||||
"account_type": "Expenses Included In Valuation",
|
||||
"account_number": "5118"
|
||||
},
|
||||
_("Stock Adjustment"): {
|
||||
"account_type": "Stock Adjustment",
|
||||
"account_number": "5119"
|
||||
},
|
||||
"account_number": "5110"
|
||||
},
|
||||
"account_number": "5100"
|
||||
},
|
||||
_("Indirect Expenses"): {
|
||||
_("Administrative Expenses"): {
|
||||
"account_number": "5201"
|
||||
},
|
||||
_("Commission on Sales"): {
|
||||
"account_number": "5202"
|
||||
},
|
||||
_("Depreciation"): {
|
||||
"account_type": "Depreciation",
|
||||
"account_number": "5203"
|
||||
},
|
||||
_("Entertainment Expenses"): {
|
||||
"account_number": "5204"
|
||||
},
|
||||
_("Freight and Forwarding Charges"): {
|
||||
"account_type": "Chargeable",
|
||||
"account_number": "5205"
|
||||
},
|
||||
_("Legal Expenses"): {
|
||||
"account_number": "5206"
|
||||
},
|
||||
_("Marketing Expenses"): {
|
||||
"account_type": "Chargeable",
|
||||
"account_number": "5207"
|
||||
},
|
||||
_("Office Maintenance Expenses"): {
|
||||
"account_number": "5208"
|
||||
},
|
||||
_("Office Rent"): {
|
||||
"account_number": "5209"
|
||||
},
|
||||
_("Postal Expenses"): {
|
||||
"account_number": "5210"
|
||||
},
|
||||
_("Print and Stationery"): {
|
||||
"account_number": "5211"
|
||||
},
|
||||
_("Round Off"): {
|
||||
"account_type": "Round Off",
|
||||
"account_number": "5212"
|
||||
},
|
||||
_("Salary"): {
|
||||
"account_number": "5213"
|
||||
},
|
||||
_("Sales Expenses"): {
|
||||
"account_number": "5214"
|
||||
},
|
||||
_("Telephone Expenses"): {
|
||||
"account_number": "5215"
|
||||
},
|
||||
_("Travel Expenses"): {
|
||||
"account_number": "5216"
|
||||
},
|
||||
_("Utility Expenses"): {
|
||||
"account_number": "5217"
|
||||
},
|
||||
_("Write Off"): {
|
||||
"account_number": "5218"
|
||||
},
|
||||
_("Exchange Gain/Loss"): {
|
||||
"account_number": "5219"
|
||||
},
|
||||
_("Gain/Loss on Asset Disposal"): {
|
||||
"account_number": "5220"
|
||||
},
|
||||
_("Miscellaneous Expenses"): {
|
||||
"account_type": "Chargeable",
|
||||
"account_number": "5221"
|
||||
},
|
||||
"account_number": "5200"
|
||||
},
|
||||
"root_type": "Expense",
|
||||
"account_number": "5000"
|
||||
},
|
||||
_("Income"): {
|
||||
_("Direct Income"): {
|
||||
_("Sales"): {
|
||||
"account_number": "4110"
|
||||
},
|
||||
_("Service"): {
|
||||
"account_number": "4120"
|
||||
},
|
||||
"account_number": "4100"
|
||||
},
|
||||
_("Indirect Income"): {
|
||||
"is_group": 1,
|
||||
"account_number": "4200"
|
||||
},
|
||||
"root_type": "Income",
|
||||
"account_number": "4000"
|
||||
},
|
||||
_("Source of Funds (Liabilities)"): {
|
||||
_("Current Liabilities"): {
|
||||
_("Accounts Payable"): {
|
||||
_("Creditors"): {
|
||||
"account_type": "Payable",
|
||||
"account_number": "2110"
|
||||
},
|
||||
_("Payroll Payable"): {
|
||||
"account_number": "2120"
|
||||
},
|
||||
"account_number": "2100"
|
||||
},
|
||||
_("Stock Liabilities"): {
|
||||
_("Stock Received But Not Billed"): {
|
||||
"account_type": "Stock Received But Not Billed",
|
||||
"account_number": "2210"
|
||||
},
|
||||
_("Asset Received But Not Billed"): {
|
||||
"account_type": "Asset Received But Not Billed",
|
||||
"account_number": "2211"
|
||||
},
|
||||
"account_number": "2200"
|
||||
},
|
||||
_("Duties and Taxes"): {
|
||||
_("TDS Payable"): {
|
||||
"account_number": "2310"
|
||||
},
|
||||
"account_type": "Tax",
|
||||
"is_group": 1,
|
||||
"account_number": "2300"
|
||||
},
|
||||
_("Loans (Liabilities)"): {
|
||||
_("Secured Loans"): {
|
||||
"account_number": "2410"
|
||||
},
|
||||
_("Unsecured Loans"): {
|
||||
"account_number": "2420"
|
||||
},
|
||||
_("Bank Overdraft Account"): {
|
||||
"account_number": "2430"
|
||||
},
|
||||
"account_number": "2400"
|
||||
},
|
||||
"account_number": "2100-2400"
|
||||
},
|
||||
"root_type": "Liability",
|
||||
"account_number": "2000"
|
||||
},
|
||||
_("Equity"): {
|
||||
_("Capital Stock"): {
|
||||
"account_type": "Equity",
|
||||
"account_number": "3100"
|
||||
},
|
||||
_("Dividends Paid"): {
|
||||
"account_type": "Equity",
|
||||
"account_number": "3200"
|
||||
},
|
||||
_("Opening Balance Equity"): {
|
||||
"account_type": "Equity",
|
||||
"account_number": "3300"
|
||||
},
|
||||
_("Retained Earnings"): {
|
||||
"account_type": "Equity",
|
||||
"account_number": "3400"
|
||||
},
|
||||
"root_type": "Equity",
|
||||
"account_number": "3000"
|
||||
}
|
||||
}
|
||||
return {
|
||||
_("Application of Funds (Assets)"): {
|
||||
_("Current Assets"): {
|
||||
_("Accounts Receivable"): {
|
||||
_("Debtors"): {"account_type": "Receivable", "account_number": "1310"},
|
||||
"account_number": "1300",
|
||||
},
|
||||
_("Bank Accounts"): {"account_type": "Bank", "is_group": 1, "account_number": "1200"},
|
||||
_("Cash In Hand"): {
|
||||
_("Cash"): {"account_type": "Cash", "account_number": "1110"},
|
||||
"account_type": "Cash",
|
||||
"account_number": "1100",
|
||||
},
|
||||
_("Loans and Advances (Assets)"): {
|
||||
_("Employee Advances"): {"account_number": "1610"},
|
||||
"account_number": "1600",
|
||||
},
|
||||
_("Securities and Deposits"): {
|
||||
_("Earnest Money"): {"account_number": "1651"},
|
||||
"account_number": "1650",
|
||||
},
|
||||
_("Stock Assets"): {
|
||||
_("Stock In Hand"): {"account_type": "Stock", "account_number": "1410"},
|
||||
"account_type": "Stock",
|
||||
"account_number": "1400",
|
||||
},
|
||||
_("Tax Assets"): {"is_group": 1, "account_number": "1500"},
|
||||
"account_number": "1100-1600",
|
||||
},
|
||||
_("Fixed Assets"): {
|
||||
_("Capital Equipments"): {"account_type": "Fixed Asset", "account_number": "1710"},
|
||||
_("Electronic Equipments"): {"account_type": "Fixed Asset", "account_number": "1720"},
|
||||
_("Furnitures and Fixtures"): {"account_type": "Fixed Asset", "account_number": "1730"},
|
||||
_("Office Equipments"): {"account_type": "Fixed Asset", "account_number": "1740"},
|
||||
_("Plants and Machineries"): {"account_type": "Fixed Asset", "account_number": "1750"},
|
||||
_("Buildings"): {"account_type": "Fixed Asset", "account_number": "1760"},
|
||||
_("Softwares"): {"account_type": "Fixed Asset", "account_number": "1770"},
|
||||
_("Accumulated Depreciation"): {
|
||||
"account_type": "Accumulated Depreciation",
|
||||
"account_number": "1780",
|
||||
},
|
||||
_("CWIP Account"): {"account_type": "Capital Work in Progress", "account_number": "1790"},
|
||||
"account_number": "1700",
|
||||
},
|
||||
_("Investments"): {"is_group": 1, "account_number": "1800"},
|
||||
_("Temporary Accounts"): {
|
||||
_("Temporary Opening"): {"account_type": "Temporary", "account_number": "1910"},
|
||||
"account_number": "1900",
|
||||
},
|
||||
"root_type": "Asset",
|
||||
"account_number": "1000",
|
||||
},
|
||||
_("Expenses"): {
|
||||
_("Direct Expenses"): {
|
||||
_("Stock Expenses"): {
|
||||
_("Cost of Goods Sold"): {"account_type": "Cost of Goods Sold", "account_number": "5111"},
|
||||
_("Expenses Included In Asset Valuation"): {
|
||||
"account_type": "Expenses Included In Asset Valuation",
|
||||
"account_number": "5112",
|
||||
},
|
||||
_("Expenses Included In Valuation"): {
|
||||
"account_type": "Expenses Included In Valuation",
|
||||
"account_number": "5118",
|
||||
},
|
||||
_("Stock Adjustment"): {"account_type": "Stock Adjustment", "account_number": "5119"},
|
||||
"account_number": "5110",
|
||||
},
|
||||
"account_number": "5100",
|
||||
},
|
||||
_("Indirect Expenses"): {
|
||||
_("Administrative Expenses"): {"account_number": "5201"},
|
||||
_("Commission on Sales"): {"account_number": "5202"},
|
||||
_("Depreciation"): {"account_type": "Depreciation", "account_number": "5203"},
|
||||
_("Entertainment Expenses"): {"account_number": "5204"},
|
||||
_("Freight and Forwarding Charges"): {"account_type": "Chargeable", "account_number": "5205"},
|
||||
_("Legal Expenses"): {"account_number": "5206"},
|
||||
_("Marketing Expenses"): {"account_type": "Chargeable", "account_number": "5207"},
|
||||
_("Office Maintenance Expenses"): {"account_number": "5208"},
|
||||
_("Office Rent"): {"account_number": "5209"},
|
||||
_("Postal Expenses"): {"account_number": "5210"},
|
||||
_("Print and Stationery"): {"account_number": "5211"},
|
||||
_("Round Off"): {"account_type": "Round Off", "account_number": "5212"},
|
||||
_("Salary"): {"account_number": "5213"},
|
||||
_("Sales Expenses"): {"account_number": "5214"},
|
||||
_("Telephone Expenses"): {"account_number": "5215"},
|
||||
_("Travel Expenses"): {"account_number": "5216"},
|
||||
_("Utility Expenses"): {"account_number": "5217"},
|
||||
_("Write Off"): {"account_number": "5218"},
|
||||
_("Exchange Gain/Loss"): {"account_number": "5219"},
|
||||
_("Gain/Loss on Asset Disposal"): {"account_number": "5220"},
|
||||
_("Miscellaneous Expenses"): {"account_type": "Chargeable", "account_number": "5221"},
|
||||
"account_number": "5200",
|
||||
},
|
||||
"root_type": "Expense",
|
||||
"account_number": "5000",
|
||||
},
|
||||
_("Income"): {
|
||||
_("Direct Income"): {
|
||||
_("Sales"): {"account_number": "4110"},
|
||||
_("Service"): {"account_number": "4120"},
|
||||
"account_number": "4100",
|
||||
},
|
||||
_("Indirect Income"): {"is_group": 1, "account_number": "4200"},
|
||||
"root_type": "Income",
|
||||
"account_number": "4000",
|
||||
},
|
||||
_("Source of Funds (Liabilities)"): {
|
||||
_("Current Liabilities"): {
|
||||
_("Accounts Payable"): {
|
||||
_("Creditors"): {"account_type": "Payable", "account_number": "2110"},
|
||||
_("Payroll Payable"): {"account_number": "2120"},
|
||||
"account_number": "2100",
|
||||
},
|
||||
_("Stock Liabilities"): {
|
||||
_("Stock Received But Not Billed"): {
|
||||
"account_type": "Stock Received But Not Billed",
|
||||
"account_number": "2210",
|
||||
},
|
||||
_("Asset Received But Not Billed"): {
|
||||
"account_type": "Asset Received But Not Billed",
|
||||
"account_number": "2211",
|
||||
},
|
||||
"account_number": "2200",
|
||||
},
|
||||
_("Duties and Taxes"): {
|
||||
_("TDS Payable"): {"account_number": "2310"},
|
||||
"account_type": "Tax",
|
||||
"is_group": 1,
|
||||
"account_number": "2300",
|
||||
},
|
||||
_("Loans (Liabilities)"): {
|
||||
_("Secured Loans"): {"account_number": "2410"},
|
||||
_("Unsecured Loans"): {"account_number": "2420"},
|
||||
_("Bank Overdraft Account"): {"account_number": "2430"},
|
||||
"account_number": "2400",
|
||||
},
|
||||
"account_number": "2100-2400",
|
||||
},
|
||||
"root_type": "Liability",
|
||||
"account_number": "2000",
|
||||
},
|
||||
_("Equity"): {
|
||||
_("Capital Stock"): {"account_type": "Equity", "account_number": "3100"},
|
||||
_("Dividends Paid"): {"account_type": "Equity", "account_number": "3200"},
|
||||
_("Opening Balance Equity"): {"account_type": "Equity", "account_number": "3300"},
|
||||
_("Retained Earnings"): {"account_type": "Equity", "account_number": "3400"},
|
||||
"root_type": "Equity",
|
||||
"account_number": "3000",
|
||||
},
|
||||
}
|
||||
|
||||
@ -20,8 +20,9 @@ class TestAccount(unittest.TestCase):
|
||||
acc.company = "_Test Company"
|
||||
acc.insert()
|
||||
|
||||
account_number, account_name = frappe.db.get_value("Account", "1210 - Debtors - _TC",
|
||||
["account_number", "account_name"])
|
||||
account_number, account_name = frappe.db.get_value(
|
||||
"Account", "1210 - Debtors - _TC", ["account_number", "account_name"]
|
||||
)
|
||||
self.assertEqual(account_number, "1210")
|
||||
self.assertEqual(account_name, "Debtors")
|
||||
|
||||
@ -30,8 +31,12 @@ class TestAccount(unittest.TestCase):
|
||||
|
||||
update_account_number("1210 - Debtors - _TC", new_account_name, new_account_number)
|
||||
|
||||
new_acc = frappe.db.get_value("Account", "1211-11-4 - 6 - - Debtors 1 - Test - - _TC",
|
||||
["account_name", "account_number"], as_dict=1)
|
||||
new_acc = frappe.db.get_value(
|
||||
"Account",
|
||||
"1211-11-4 - 6 - - Debtors 1 - Test - - _TC",
|
||||
["account_name", "account_number"],
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
self.assertEqual(new_acc.account_name, "Debtors 1 - Test -")
|
||||
self.assertEqual(new_acc.account_number, "1211-11-4 - 6 -")
|
||||
@ -79,7 +84,9 @@ class TestAccount(unittest.TestCase):
|
||||
|
||||
self.assertEqual(parent, "Securities and Deposits - _TC")
|
||||
|
||||
merge_account("Securities and Deposits - _TC", "Cash In Hand - _TC", doc.is_group, doc.root_type, doc.company)
|
||||
merge_account(
|
||||
"Securities and Deposits - _TC", "Cash In Hand - _TC", doc.is_group, doc.root_type, doc.company
|
||||
)
|
||||
parent = frappe.db.get_value("Account", "Earnest Money - _TC", "parent_account")
|
||||
|
||||
# Parent account of the child account changes after merging
|
||||
@ -91,14 +98,28 @@ class TestAccount(unittest.TestCase):
|
||||
doc = frappe.get_doc("Account", "Current Assets - _TC")
|
||||
|
||||
# Raise error as is_group property doesn't match
|
||||
self.assertRaises(frappe.ValidationError, merge_account, "Current Assets - _TC",\
|
||||
"Accumulated Depreciation - _TC", doc.is_group, doc.root_type, doc.company)
|
||||
self.assertRaises(
|
||||
frappe.ValidationError,
|
||||
merge_account,
|
||||
"Current Assets - _TC",
|
||||
"Accumulated Depreciation - _TC",
|
||||
doc.is_group,
|
||||
doc.root_type,
|
||||
doc.company,
|
||||
)
|
||||
|
||||
doc = frappe.get_doc("Account", "Capital Stock - _TC")
|
||||
|
||||
# Raise error as root_type property doesn't match
|
||||
self.assertRaises(frappe.ValidationError, merge_account, "Capital Stock - _TC",\
|
||||
"Softwares - _TC", doc.is_group, doc.root_type, doc.company)
|
||||
self.assertRaises(
|
||||
frappe.ValidationError,
|
||||
merge_account,
|
||||
"Capital Stock - _TC",
|
||||
"Softwares - _TC",
|
||||
doc.is_group,
|
||||
doc.root_type,
|
||||
doc.company,
|
||||
)
|
||||
|
||||
def test_account_sync(self):
|
||||
frappe.local.flags.pop("ignore_root_company_validation", None)
|
||||
@ -109,8 +130,12 @@ class TestAccount(unittest.TestCase):
|
||||
acc.company = "_Test Company 3"
|
||||
acc.insert()
|
||||
|
||||
acc_tc_4 = frappe.db.get_value('Account', {'account_name': "Test Sync Account", "company": "_Test Company 4"})
|
||||
acc_tc_5 = frappe.db.get_value('Account', {'account_name': "Test Sync Account", "company": "_Test Company 5"})
|
||||
acc_tc_4 = frappe.db.get_value(
|
||||
"Account", {"account_name": "Test Sync Account", "company": "_Test Company 4"}
|
||||
)
|
||||
acc_tc_5 = frappe.db.get_value(
|
||||
"Account", {"account_name": "Test Sync Account", "company": "_Test Company 5"}
|
||||
)
|
||||
self.assertEqual(acc_tc_4, "Test Sync Account - _TC4")
|
||||
self.assertEqual(acc_tc_5, "Test Sync Account - _TC5")
|
||||
|
||||
@ -138,8 +163,26 @@ class TestAccount(unittest.TestCase):
|
||||
update_account_number(acc.name, "Test Rename Sync Account", "1234")
|
||||
|
||||
# Check if renamed in children
|
||||
self.assertTrue(frappe.db.exists("Account", {'account_name': "Test Rename Sync Account", "company": "_Test Company 4", "account_number": "1234"}))
|
||||
self.assertTrue(frappe.db.exists("Account", {'account_name': "Test Rename Sync Account", "company": "_Test Company 5", "account_number": "1234"}))
|
||||
self.assertTrue(
|
||||
frappe.db.exists(
|
||||
"Account",
|
||||
{
|
||||
"account_name": "Test Rename Sync Account",
|
||||
"company": "_Test Company 4",
|
||||
"account_number": "1234",
|
||||
},
|
||||
)
|
||||
)
|
||||
self.assertTrue(
|
||||
frappe.db.exists(
|
||||
"Account",
|
||||
{
|
||||
"account_name": "Test Rename Sync Account",
|
||||
"company": "_Test Company 5",
|
||||
"account_number": "1234",
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
frappe.delete_doc("Account", "1234 - Test Rename Sync Account - _TC3")
|
||||
frappe.delete_doc("Account", "1234 - Test Rename Sync Account - _TC4")
|
||||
@ -155,25 +198,71 @@ class TestAccount(unittest.TestCase):
|
||||
acc.company = "_Test Company 3"
|
||||
acc.insert()
|
||||
|
||||
self.assertTrue(frappe.db.exists("Account", {'account_name': "Test Group Account", "company": "_Test Company 4"}))
|
||||
self.assertTrue(frappe.db.exists("Account", {'account_name': "Test Group Account", "company": "_Test Company 5"}))
|
||||
self.assertTrue(
|
||||
frappe.db.exists(
|
||||
"Account", {"account_name": "Test Group Account", "company": "_Test Company 4"}
|
||||
)
|
||||
)
|
||||
self.assertTrue(
|
||||
frappe.db.exists(
|
||||
"Account", {"account_name": "Test Group Account", "company": "_Test Company 5"}
|
||||
)
|
||||
)
|
||||
|
||||
# Try renaming child company account
|
||||
acc_tc_5 = frappe.db.get_value('Account', {'account_name': "Test Group Account", "company": "_Test Company 5"})
|
||||
self.assertRaises(frappe.ValidationError, update_account_number, acc_tc_5, "Test Modified Account")
|
||||
acc_tc_5 = frappe.db.get_value(
|
||||
"Account", {"account_name": "Test Group Account", "company": "_Test Company 5"}
|
||||
)
|
||||
self.assertRaises(
|
||||
frappe.ValidationError, update_account_number, acc_tc_5, "Test Modified Account"
|
||||
)
|
||||
|
||||
# Rename child company account with allow_account_creation_against_child_company enabled
|
||||
frappe.db.set_value("Company", "_Test Company 5", "allow_account_creation_against_child_company", 1)
|
||||
frappe.db.set_value(
|
||||
"Company", "_Test Company 5", "allow_account_creation_against_child_company", 1
|
||||
)
|
||||
|
||||
update_account_number(acc_tc_5, "Test Modified Account")
|
||||
self.assertTrue(frappe.db.exists("Account", {'name': "Test Modified Account - _TC5", "company": "_Test Company 5"}))
|
||||
self.assertTrue(
|
||||
frappe.db.exists(
|
||||
"Account", {"name": "Test Modified Account - _TC5", "company": "_Test Company 5"}
|
||||
)
|
||||
)
|
||||
|
||||
frappe.db.set_value("Company", "_Test Company 5", "allow_account_creation_against_child_company", 0)
|
||||
frappe.db.set_value(
|
||||
"Company", "_Test Company 5", "allow_account_creation_against_child_company", 0
|
||||
)
|
||||
|
||||
to_delete = ["Test Group Account - _TC3", "Test Group Account - _TC4", "Test Modified Account - _TC5"]
|
||||
to_delete = [
|
||||
"Test Group Account - _TC3",
|
||||
"Test Group Account - _TC4",
|
||||
"Test Modified Account - _TC5",
|
||||
]
|
||||
for doc in to_delete:
|
||||
frappe.delete_doc("Account", doc)
|
||||
|
||||
def test_validate_account_currency(self):
|
||||
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
|
||||
|
||||
if not frappe.db.get_value("Account", "Test Currency Account - _TC"):
|
||||
acc = frappe.new_doc("Account")
|
||||
acc.account_name = "Test Currency Account"
|
||||
acc.parent_account = "Tax Assets - _TC"
|
||||
acc.company = "_Test Company"
|
||||
acc.insert()
|
||||
else:
|
||||
acc = frappe.get_doc("Account", "Test Currency Account - _TC")
|
||||
|
||||
self.assertEqual(acc.account_currency, "INR")
|
||||
|
||||
# Make a JV against this account
|
||||
make_journal_entry(
|
||||
"Test Currency Account - _TC", "Miscellaneous Expenses - _TC", 100, submit=True
|
||||
)
|
||||
|
||||
acc.account_currency = "USD"
|
||||
self.assertRaises(frappe.ValidationError, acc.save)
|
||||
|
||||
|
||||
def _make_test_records(verbose=None):
|
||||
from frappe.test_runner import make_test_objects
|
||||
@ -184,20 +273,16 @@ def _make_test_records(verbose=None):
|
||||
["_Test Bank USD", "Bank Accounts", 0, "Bank", "USD"],
|
||||
["_Test Bank EUR", "Bank Accounts", 0, "Bank", "EUR"],
|
||||
["_Test Cash", "Cash In Hand", 0, "Cash", None],
|
||||
|
||||
["_Test Account Stock Expenses", "Direct Expenses", 1, None, None],
|
||||
["_Test Account Shipping Charges", "_Test Account Stock Expenses", 0, "Chargeable", None],
|
||||
["_Test Account Customs Duty", "_Test Account Stock Expenses", 0, "Tax", None],
|
||||
["_Test Account Insurance Charges", "_Test Account Stock Expenses", 0, "Chargeable", None],
|
||||
["_Test Account Stock Adjustment", "_Test Account Stock Expenses", 0, "Stock Adjustment", None],
|
||||
["_Test Employee Advance", "Current Liabilities", 0, None, None],
|
||||
|
||||
["_Test Account Tax Assets", "Current Assets", 1, None, None],
|
||||
["_Test Account VAT", "_Test Account Tax Assets", 0, "Tax", None],
|
||||
["_Test Account Service Tax", "_Test Account Tax Assets", 0, "Tax", None],
|
||||
|
||||
["_Test Account Reserves and Surplus", "Current Liabilities", 0, None, None],
|
||||
|
||||
["_Test Account Cost for Goods Sold", "Expenses", 0, None, None],
|
||||
["_Test Account Excise Duty", "_Test Account Tax Assets", 0, "Tax", None],
|
||||
["_Test Account Education Cess", "_Test Account Tax Assets", 0, "Tax", None],
|
||||
@ -206,38 +291,45 @@ def _make_test_records(verbose=None):
|
||||
["_Test Account Discount", "Direct Expenses", 0, None, None],
|
||||
["_Test Write Off", "Indirect Expenses", 0, None, None],
|
||||
["_Test Exchange Gain/Loss", "Indirect Expenses", 0, None, None],
|
||||
|
||||
["_Test Account Sales", "Direct Income", 0, None, None],
|
||||
|
||||
# related to Account Inventory Integration
|
||||
["_Test Account Stock In Hand", "Current Assets", 0, None, None],
|
||||
|
||||
# fixed asset depreciation
|
||||
["_Test Fixed Asset", "Current Assets", 0, "Fixed Asset", None],
|
||||
["_Test Accumulated Depreciations", "Current Assets", 0, "Accumulated Depreciation", None],
|
||||
["_Test Depreciations", "Expenses", 0, None, None],
|
||||
["_Test Gain/Loss on Asset Disposal", "Expenses", 0, None, None],
|
||||
|
||||
# Receivable / Payable Account
|
||||
["_Test Receivable", "Current Assets", 0, "Receivable", None],
|
||||
["_Test Payable", "Current Liabilities", 0, "Payable", None],
|
||||
["_Test Receivable USD", "Current Assets", 0, "Receivable", "USD"],
|
||||
["_Test Payable USD", "Current Liabilities", 0, "Payable", "USD"]
|
||||
["_Test Payable USD", "Current Liabilities", 0, "Payable", "USD"],
|
||||
]
|
||||
|
||||
for company, abbr in [["_Test Company", "_TC"], ["_Test Company 1", "_TC1"], ["_Test Company with perpetual inventory", "TCP1"]]:
|
||||
test_objects = make_test_objects("Account", [{
|
||||
"doctype": "Account",
|
||||
"account_name": account_name,
|
||||
"parent_account": parent_account + " - " + abbr,
|
||||
"company": company,
|
||||
"is_group": is_group,
|
||||
"account_type": account_type,
|
||||
"account_currency": currency
|
||||
} for account_name, parent_account, is_group, account_type, currency in accounts])
|
||||
for company, abbr in [
|
||||
["_Test Company", "_TC"],
|
||||
["_Test Company 1", "_TC1"],
|
||||
["_Test Company with perpetual inventory", "TCP1"],
|
||||
]:
|
||||
test_objects = make_test_objects(
|
||||
"Account",
|
||||
[
|
||||
{
|
||||
"doctype": "Account",
|
||||
"account_name": account_name,
|
||||
"parent_account": parent_account + " - " + abbr,
|
||||
"company": company,
|
||||
"is_group": is_group,
|
||||
"account_type": account_type,
|
||||
"account_currency": currency,
|
||||
}
|
||||
for account_name, parent_account, is_group, account_type, currency in accounts
|
||||
],
|
||||
)
|
||||
|
||||
return test_objects
|
||||
|
||||
|
||||
def get_inventory_account(company, warehouse=None):
|
||||
account = None
|
||||
if warehouse:
|
||||
@ -247,19 +339,24 @@ def get_inventory_account(company, warehouse=None):
|
||||
|
||||
return account
|
||||
|
||||
|
||||
def create_account(**kwargs):
|
||||
account = frappe.db.get_value("Account", filters={"account_name": kwargs.get("account_name"), "company": kwargs.get("company")})
|
||||
account = frappe.db.get_value(
|
||||
"Account", filters={"account_name": kwargs.get("account_name"), "company": kwargs.get("company")}
|
||||
)
|
||||
if account:
|
||||
return account
|
||||
else:
|
||||
account = frappe.get_doc(dict(
|
||||
doctype = "Account",
|
||||
account_name = kwargs.get('account_name'),
|
||||
account_type = kwargs.get('account_type'),
|
||||
parent_account = kwargs.get('parent_account'),
|
||||
company = kwargs.get('company'),
|
||||
account_currency = kwargs.get('account_currency')
|
||||
))
|
||||
account = frappe.get_doc(
|
||||
dict(
|
||||
doctype="Account",
|
||||
account_name=kwargs.get("account_name"),
|
||||
account_type=kwargs.get("account_type"),
|
||||
parent_account=kwargs.get("parent_account"),
|
||||
company=kwargs.get("company"),
|
||||
account_currency=kwargs.get("account_currency"),
|
||||
)
|
||||
)
|
||||
|
||||
account.save()
|
||||
return account.name
|
||||
|
||||
@ -17,13 +17,21 @@ class AccountingDimension(Document):
|
||||
self.set_fieldname_and_label()
|
||||
|
||||
def validate(self):
|
||||
if self.document_type in core_doctypes_list + ('Accounting Dimension', 'Project',
|
||||
'Cost Center', 'Accounting Dimension Detail', 'Company', 'Account') :
|
||||
if self.document_type in core_doctypes_list + (
|
||||
"Accounting Dimension",
|
||||
"Project",
|
||||
"Cost Center",
|
||||
"Accounting Dimension Detail",
|
||||
"Company",
|
||||
"Account",
|
||||
):
|
||||
|
||||
msg = _("Not allowed to create accounting dimension for {0}").format(self.document_type)
|
||||
frappe.throw(msg)
|
||||
|
||||
exists = frappe.db.get_value("Accounting Dimension", {'document_type': self.document_type}, ['name'])
|
||||
exists = frappe.db.get_value(
|
||||
"Accounting Dimension", {"document_type": self.document_type}, ["name"]
|
||||
)
|
||||
|
||||
if exists and self.is_new():
|
||||
frappe.throw(_("Document Type already used as a dimension"))
|
||||
@ -42,13 +50,13 @@ class AccountingDimension(Document):
|
||||
if frappe.flags.in_test:
|
||||
make_dimension_in_accounting_doctypes(doc=self)
|
||||
else:
|
||||
frappe.enqueue(make_dimension_in_accounting_doctypes, doc=self, queue='long')
|
||||
frappe.enqueue(make_dimension_in_accounting_doctypes, doc=self, queue="long")
|
||||
|
||||
def on_trash(self):
|
||||
if frappe.flags.in_test:
|
||||
delete_accounting_dimension(doc=self)
|
||||
else:
|
||||
frappe.enqueue(delete_accounting_dimension, doc=self, queue='long')
|
||||
frappe.enqueue(delete_accounting_dimension, doc=self, queue="long")
|
||||
|
||||
def set_fieldname_and_label(self):
|
||||
if not self.label:
|
||||
@ -60,6 +68,7 @@ class AccountingDimension(Document):
|
||||
def on_update(self):
|
||||
frappe.flags.accounting_dimensions = None
|
||||
|
||||
|
||||
def make_dimension_in_accounting_doctypes(doc, doclist=None):
|
||||
if not doclist:
|
||||
doclist = get_doctypes_with_dimensions()
|
||||
@ -70,9 +79,9 @@ def make_dimension_in_accounting_doctypes(doc, doclist=None):
|
||||
for doctype in doclist:
|
||||
|
||||
if (doc_count + 1) % 2 == 0:
|
||||
insert_after_field = 'dimension_col_break'
|
||||
insert_after_field = "dimension_col_break"
|
||||
else:
|
||||
insert_after_field = 'accounting_dimensions_section'
|
||||
insert_after_field = "accounting_dimensions_section"
|
||||
|
||||
df = {
|
||||
"fieldname": doc.fieldname,
|
||||
@ -80,30 +89,33 @@ def make_dimension_in_accounting_doctypes(doc, doclist=None):
|
||||
"fieldtype": "Link",
|
||||
"options": doc.document_type,
|
||||
"insert_after": insert_after_field,
|
||||
"owner": "Administrator"
|
||||
"owner": "Administrator",
|
||||
}
|
||||
|
||||
meta = frappe.get_meta(doctype, cached=False)
|
||||
fieldnames = [d.fieldname for d in meta.get("fields")]
|
||||
|
||||
if df['fieldname'] not in fieldnames:
|
||||
if df["fieldname"] not in fieldnames:
|
||||
if doctype == "Budget":
|
||||
add_dimension_to_budget_doctype(df.copy(), doc)
|
||||
else:
|
||||
create_custom_field(doctype, df)
|
||||
create_custom_field(doctype, df, ignore_validate=True)
|
||||
|
||||
count += 1
|
||||
|
||||
frappe.publish_progress(count*100/len(doclist), title = _("Creating Dimensions..."))
|
||||
frappe.publish_progress(count * 100 / len(doclist), title=_("Creating Dimensions..."))
|
||||
frappe.clear_cache(doctype=doctype)
|
||||
|
||||
def add_dimension_to_budget_doctype(df, doc):
|
||||
df.update({
|
||||
"insert_after": "cost_center",
|
||||
"depends_on": "eval:doc.budget_against == '{0}'".format(doc.document_type)
|
||||
})
|
||||
|
||||
create_custom_field("Budget", df)
|
||||
def add_dimension_to_budget_doctype(df, doc):
|
||||
df.update(
|
||||
{
|
||||
"insert_after": "cost_center",
|
||||
"depends_on": "eval:doc.budget_against == '{0}'".format(doc.document_type),
|
||||
}
|
||||
)
|
||||
|
||||
create_custom_field("Budget", df, ignore_validate=True)
|
||||
|
||||
property_setter = frappe.db.exists("Property Setter", "Budget-budget_against-options")
|
||||
|
||||
@ -112,36 +124,44 @@ def add_dimension_to_budget_doctype(df, doc):
|
||||
property_setter_doc.value = property_setter_doc.value + "\n" + doc.document_type
|
||||
property_setter_doc.save()
|
||||
|
||||
frappe.clear_cache(doctype='Budget')
|
||||
frappe.clear_cache(doctype="Budget")
|
||||
else:
|
||||
frappe.get_doc({
|
||||
"doctype": "Property Setter",
|
||||
"doctype_or_field": "DocField",
|
||||
"doc_type": "Budget",
|
||||
"field_name": "budget_against",
|
||||
"property": "options",
|
||||
"property_type": "Text",
|
||||
"value": "\nCost Center\nProject\n" + doc.document_type
|
||||
}).insert(ignore_permissions=True)
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Property Setter",
|
||||
"doctype_or_field": "DocField",
|
||||
"doc_type": "Budget",
|
||||
"field_name": "budget_against",
|
||||
"property": "options",
|
||||
"property_type": "Text",
|
||||
"value": "\nCost Center\nProject\n" + doc.document_type,
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
|
||||
def delete_accounting_dimension(doc):
|
||||
doclist = get_doctypes_with_dimensions()
|
||||
|
||||
frappe.db.sql("""
|
||||
frappe.db.sql(
|
||||
"""
|
||||
DELETE FROM `tabCustom Field`
|
||||
WHERE fieldname = %s
|
||||
AND dt IN (%s)""" % #nosec
|
||||
('%s', ', '.join(['%s']* len(doclist))), tuple([doc.fieldname] + doclist))
|
||||
AND dt IN (%s)"""
|
||||
% ("%s", ", ".join(["%s"] * len(doclist))), # nosec
|
||||
tuple([doc.fieldname] + doclist),
|
||||
)
|
||||
|
||||
frappe.db.sql("""
|
||||
frappe.db.sql(
|
||||
"""
|
||||
DELETE FROM `tabProperty Setter`
|
||||
WHERE field_name = %s
|
||||
AND doc_type IN (%s)""" % #nosec
|
||||
('%s', ', '.join(['%s']* len(doclist))), tuple([doc.fieldname] + doclist))
|
||||
AND doc_type IN (%s)"""
|
||||
% ("%s", ", ".join(["%s"] * len(doclist))), # nosec
|
||||
tuple([doc.fieldname] + doclist),
|
||||
)
|
||||
|
||||
budget_against_property = frappe.get_doc("Property Setter", "Budget-budget_against-options")
|
||||
value_list = budget_against_property.value.split('\n')[3:]
|
||||
value_list = budget_against_property.value.split("\n")[3:]
|
||||
|
||||
if doc.document_type in value_list:
|
||||
value_list.remove(doc.document_type)
|
||||
@ -152,6 +172,7 @@ def delete_accounting_dimension(doc):
|
||||
for doctype in doclist:
|
||||
frappe.clear_cache(doctype=doctype)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def disable_dimension(doc):
|
||||
if frappe.flags.in_test:
|
||||
@ -159,10 +180,11 @@ def disable_dimension(doc):
|
||||
else:
|
||||
frappe.enqueue(toggle_disabling, doc=doc)
|
||||
|
||||
|
||||
def toggle_disabling(doc):
|
||||
doc = json.loads(doc)
|
||||
|
||||
if doc.get('disabled'):
|
||||
if doc.get("disabled"):
|
||||
df = {"read_only": 1}
|
||||
else:
|
||||
df = {"read_only": 0}
|
||||
@ -170,7 +192,7 @@ def toggle_disabling(doc):
|
||||
doclist = get_doctypes_with_dimensions()
|
||||
|
||||
for doctype in doclist:
|
||||
field = frappe.db.get_value("Custom Field", {"dt": doctype, "fieldname": doc.get('fieldname')})
|
||||
field = frappe.db.get_value("Custom Field", {"dt": doctype, "fieldname": doc.get("fieldname")})
|
||||
if field:
|
||||
custom_field = frappe.get_doc("Custom Field", field)
|
||||
custom_field.update(df)
|
||||
@ -178,61 +200,82 @@ def toggle_disabling(doc):
|
||||
|
||||
frappe.clear_cache(doctype=doctype)
|
||||
|
||||
|
||||
def get_doctypes_with_dimensions():
|
||||
return frappe.get_hooks("accounting_dimension_doctypes")
|
||||
|
||||
def get_accounting_dimensions(as_list=True):
|
||||
|
||||
def get_accounting_dimensions(as_list=True, filters=None):
|
||||
|
||||
if not filters:
|
||||
filters = {"disabled": 0}
|
||||
|
||||
if frappe.flags.accounting_dimensions is None:
|
||||
frappe.flags.accounting_dimensions = frappe.get_all("Accounting Dimension",
|
||||
fields=["label", "fieldname", "disabled", "document_type"])
|
||||
frappe.flags.accounting_dimensions = frappe.get_all(
|
||||
"Accounting Dimension",
|
||||
fields=["label", "fieldname", "disabled", "document_type"],
|
||||
filters=filters,
|
||||
)
|
||||
|
||||
if as_list:
|
||||
return [d.fieldname for d in frappe.flags.accounting_dimensions]
|
||||
else:
|
||||
return frappe.flags.accounting_dimensions
|
||||
|
||||
|
||||
def get_checks_for_pl_and_bs_accounts():
|
||||
dimensions = frappe.db.sql("""SELECT p.label, p.disabled, p.fieldname, c.default_dimension, c.company, c.mandatory_for_pl, c.mandatory_for_bs
|
||||
dimensions = frappe.db.sql(
|
||||
"""SELECT p.label, p.disabled, p.fieldname, c.default_dimension, c.company, c.mandatory_for_pl, c.mandatory_for_bs
|
||||
FROM `tabAccounting Dimension`p ,`tabAccounting Dimension Detail` c
|
||||
WHERE p.name = c.parent""", as_dict=1)
|
||||
WHERE p.name = c.parent""",
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
return dimensions
|
||||
|
||||
def get_dimension_with_children(doctype, dimension):
|
||||
|
||||
if isinstance(dimension, list):
|
||||
dimension = dimension[0]
|
||||
def get_dimension_with_children(doctype, dimensions):
|
||||
|
||||
if isinstance(dimensions, str):
|
||||
dimensions = [dimensions]
|
||||
|
||||
all_dimensions = []
|
||||
lft, rgt = frappe.db.get_value(doctype, dimension, ["lft", "rgt"])
|
||||
children = frappe.get_all(doctype, filters={"lft": [">=", lft], "rgt": ["<=", rgt]}, order_by="lft")
|
||||
all_dimensions += [c.name for c in children]
|
||||
|
||||
for dimension in dimensions:
|
||||
lft, rgt = frappe.db.get_value(doctype, dimension, ["lft", "rgt"])
|
||||
children = frappe.get_all(
|
||||
doctype, filters={"lft": [">=", lft], "rgt": ["<=", rgt]}, order_by="lft"
|
||||
)
|
||||
all_dimensions += [c.name for c in children]
|
||||
|
||||
return all_dimensions
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_dimensions(with_cost_center_and_project=False):
|
||||
dimension_filters = frappe.db.sql("""
|
||||
dimension_filters = frappe.db.sql(
|
||||
"""
|
||||
SELECT label, fieldname, document_type
|
||||
FROM `tabAccounting Dimension`
|
||||
WHERE disabled = 0
|
||||
""", as_dict=1)
|
||||
""",
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
default_dimensions = frappe.db.sql("""SELECT p.fieldname, c.company, c.default_dimension
|
||||
default_dimensions = frappe.db.sql(
|
||||
"""SELECT p.fieldname, c.company, c.default_dimension
|
||||
FROM `tabAccounting Dimension Detail` c, `tabAccounting Dimension` p
|
||||
WHERE c.parent = p.name""", as_dict=1)
|
||||
WHERE c.parent = p.name""",
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
if with_cost_center_and_project:
|
||||
dimension_filters.extend([
|
||||
{
|
||||
'fieldname': 'cost_center',
|
||||
'document_type': 'Cost Center'
|
||||
},
|
||||
{
|
||||
'fieldname': 'project',
|
||||
'document_type': 'Project'
|
||||
}
|
||||
])
|
||||
dimension_filters.extend(
|
||||
[
|
||||
{"fieldname": "cost_center", "document_type": "Cost Center"},
|
||||
{"fieldname": "project", "document_type": "Project"},
|
||||
]
|
||||
)
|
||||
|
||||
default_dimensions_map = {}
|
||||
for dimension in default_dimensions:
|
||||
|
||||
@ -8,7 +8,8 @@ import frappe
|
||||
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
|
||||
test_dependencies = ['Cost Center', 'Location', 'Warehouse', 'Department']
|
||||
test_dependencies = ["Cost Center", "Location", "Warehouse", "Department"]
|
||||
|
||||
|
||||
class TestAccountingDimension(unittest.TestCase):
|
||||
def setUp(self):
|
||||
@ -18,24 +19,27 @@ class TestAccountingDimension(unittest.TestCase):
|
||||
si = create_sales_invoice(do_not_save=1)
|
||||
|
||||
si.location = "Block 1"
|
||||
si.append("items", {
|
||||
"item_code": "_Test Item",
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"qty": 1,
|
||||
"rate": 100,
|
||||
"income_account": "Sales - _TC",
|
||||
"expense_account": "Cost of Goods Sold - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"department": "_Test Department - _TC",
|
||||
"location": "Block 1"
|
||||
})
|
||||
si.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": "_Test Item",
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"qty": 1,
|
||||
"rate": 100,
|
||||
"income_account": "Sales - _TC",
|
||||
"expense_account": "Cost of Goods Sold - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"department": "_Test Department - _TC",
|
||||
"location": "Block 1",
|
||||
},
|
||||
)
|
||||
|
||||
si.save()
|
||||
si.submit()
|
||||
|
||||
gle = frappe.get_doc("GL Entry", {"voucher_no": si.name, "account": "Sales - _TC"})
|
||||
|
||||
self.assertEqual(gle.get('department'), "_Test Department - _TC")
|
||||
self.assertEqual(gle.get("department"), "_Test Department - _TC")
|
||||
|
||||
def test_dimension_against_journal_entry(self):
|
||||
je = make_journal_entry("Sales - _TC", "Sales Expenses - _TC", 500, save=False)
|
||||
@ -50,21 +54,24 @@ class TestAccountingDimension(unittest.TestCase):
|
||||
|
||||
gle = frappe.get_doc("GL Entry", {"voucher_no": je.name, "account": "Sales - _TC"})
|
||||
gle1 = frappe.get_doc("GL Entry", {"voucher_no": je.name, "account": "Sales Expenses - _TC"})
|
||||
self.assertEqual(gle.get('department'), "_Test Department - _TC")
|
||||
self.assertEqual(gle1.get('department'), "_Test Department - _TC")
|
||||
self.assertEqual(gle.get("department"), "_Test Department - _TC")
|
||||
self.assertEqual(gle1.get("department"), "_Test Department - _TC")
|
||||
|
||||
def test_mandatory(self):
|
||||
si = create_sales_invoice(do_not_save=1)
|
||||
si.append("items", {
|
||||
"item_code": "_Test Item",
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"qty": 1,
|
||||
"rate": 100,
|
||||
"income_account": "Sales - _TC",
|
||||
"expense_account": "Cost of Goods Sold - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"location": ""
|
||||
})
|
||||
si.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": "_Test Item",
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"qty": 1,
|
||||
"rate": 100,
|
||||
"income_account": "Sales - _TC",
|
||||
"expense_account": "Cost of Goods Sold - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"location": "",
|
||||
},
|
||||
)
|
||||
|
||||
si.save()
|
||||
self.assertRaises(frappe.ValidationError, si.submit)
|
||||
@ -72,31 +79,39 @@ class TestAccountingDimension(unittest.TestCase):
|
||||
def tearDown(self):
|
||||
disable_dimension()
|
||||
|
||||
|
||||
def create_dimension():
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
if not frappe.db.exists("Accounting Dimension", {"document_type": "Department"}):
|
||||
frappe.get_doc({
|
||||
"doctype": "Accounting Dimension",
|
||||
"document_type": "Department",
|
||||
}).insert()
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Accounting Dimension",
|
||||
"document_type": "Department",
|
||||
}
|
||||
).insert()
|
||||
else:
|
||||
dimension = frappe.get_doc("Accounting Dimension", "Department")
|
||||
dimension.disabled = 0
|
||||
dimension.save()
|
||||
|
||||
if not frappe.db.exists("Accounting Dimension", {"document_type": "Location"}):
|
||||
dimension1 = frappe.get_doc({
|
||||
"doctype": "Accounting Dimension",
|
||||
"document_type": "Location",
|
||||
})
|
||||
dimension1 = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Accounting Dimension",
|
||||
"document_type": "Location",
|
||||
}
|
||||
)
|
||||
|
||||
dimension1.append("dimension_defaults", {
|
||||
"company": "_Test Company",
|
||||
"reference_document": "Location",
|
||||
"default_dimension": "Block 1",
|
||||
"mandatory_for_bs": 1
|
||||
})
|
||||
dimension1.append(
|
||||
"dimension_defaults",
|
||||
{
|
||||
"company": "_Test Company",
|
||||
"reference_document": "Location",
|
||||
"default_dimension": "Block 1",
|
||||
"mandatory_for_bs": 1,
|
||||
},
|
||||
)
|
||||
|
||||
dimension1.insert()
|
||||
dimension1.save()
|
||||
@ -105,6 +120,7 @@ def create_dimension():
|
||||
dimension1.disabled = 0
|
||||
dimension1.save()
|
||||
|
||||
|
||||
def disable_dimension():
|
||||
dimension1 = frappe.get_doc("Accounting Dimension", "Department")
|
||||
dimension1.disabled = 1
|
||||
|
||||
@ -19,17 +19,27 @@ class AccountingDimensionFilter(Document):
|
||||
WHERE d.name = a.parent
|
||||
and d.name != %s
|
||||
and d.accounting_dimension = %s
|
||||
""", (self.name, self.accounting_dimension), as_dict=1)
|
||||
""",
|
||||
(self.name, self.accounting_dimension),
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
account_list = [d.account for d in accounts]
|
||||
|
||||
for account in self.get('accounts'):
|
||||
for account in self.get("accounts"):
|
||||
if account.applicable_on_account in account_list:
|
||||
frappe.throw(_("Row {0}: {1} account already applied for Accounting Dimension {2}").format(
|
||||
account.idx, frappe.bold(account.applicable_on_account), frappe.bold(self.accounting_dimension)))
|
||||
frappe.throw(
|
||||
_("Row {0}: {1} account already applied for Accounting Dimension {2}").format(
|
||||
account.idx,
|
||||
frappe.bold(account.applicable_on_account),
|
||||
frappe.bold(self.accounting_dimension),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def get_dimension_filter_map():
|
||||
filters = frappe.db.sql("""
|
||||
filters = frappe.db.sql(
|
||||
"""
|
||||
SELECT
|
||||
a.applicable_on_account, d.dimension_value, p.accounting_dimension,
|
||||
p.allow_or_restrict, a.is_mandatory
|
||||
@ -40,22 +50,30 @@ def get_dimension_filter_map():
|
||||
p.name = a.parent
|
||||
AND p.disabled = 0
|
||||
AND p.name = d.parent
|
||||
""", as_dict=1)
|
||||
""",
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
dimension_filter_map = {}
|
||||
|
||||
for f in filters:
|
||||
f.fieldname = scrub(f.accounting_dimension)
|
||||
|
||||
build_map(dimension_filter_map, f.fieldname, f.applicable_on_account, f.dimension_value,
|
||||
f.allow_or_restrict, f.is_mandatory)
|
||||
build_map(
|
||||
dimension_filter_map,
|
||||
f.fieldname,
|
||||
f.applicable_on_account,
|
||||
f.dimension_value,
|
||||
f.allow_or_restrict,
|
||||
f.is_mandatory,
|
||||
)
|
||||
|
||||
return dimension_filter_map
|
||||
|
||||
|
||||
def build_map(map_object, dimension, account, filter_value, allow_or_restrict, is_mandatory):
|
||||
map_object.setdefault((dimension, account), {
|
||||
'allowed_dimensions': [],
|
||||
'is_mandatory': is_mandatory,
|
||||
'allow_or_restrict': allow_or_restrict
|
||||
})
|
||||
map_object[(dimension, account)]['allowed_dimensions'].append(filter_value)
|
||||
map_object.setdefault(
|
||||
(dimension, account),
|
||||
{"allowed_dimensions": [], "is_mandatory": is_mandatory, "allow_or_restrict": allow_or_restrict},
|
||||
)
|
||||
map_object[(dimension, account)]["allowed_dimensions"].append(filter_value)
|
||||
|
||||
@ -12,7 +12,8 @@ from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension imp
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.exceptions import InvalidAccountDimensionError, MandatoryAccountDimensionError
|
||||
|
||||
test_dependencies = ['Location', 'Cost Center', 'Department']
|
||||
test_dependencies = ["Location", "Cost Center", "Department"]
|
||||
|
||||
|
||||
class TestAccountingDimensionFilter(unittest.TestCase):
|
||||
def setUp(self):
|
||||
@ -22,9 +23,9 @@ class TestAccountingDimensionFilter(unittest.TestCase):
|
||||
|
||||
def test_allowed_dimension_validation(self):
|
||||
si = create_sales_invoice(do_not_save=1)
|
||||
si.items[0].cost_center = 'Main - _TC'
|
||||
si.department = 'Accounts - _TC'
|
||||
si.location = 'Block 1'
|
||||
si.items[0].cost_center = "Main - _TC"
|
||||
si.department = "Accounts - _TC"
|
||||
si.location = "Block 1"
|
||||
si.save()
|
||||
|
||||
self.assertRaises(InvalidAccountDimensionError, si.submit)
|
||||
@ -32,12 +33,12 @@ class TestAccountingDimensionFilter(unittest.TestCase):
|
||||
|
||||
def test_mandatory_dimension_validation(self):
|
||||
si = create_sales_invoice(do_not_save=1)
|
||||
si.department = ''
|
||||
si.location = 'Block 1'
|
||||
si.department = ""
|
||||
si.location = "Block 1"
|
||||
|
||||
# Test with no department for Sales Account
|
||||
si.items[0].department = ''
|
||||
si.items[0].cost_center = '_Test Cost Center 2 - _TC'
|
||||
si.items[0].department = ""
|
||||
si.items[0].cost_center = "_Test Cost Center 2 - _TC"
|
||||
si.save()
|
||||
|
||||
self.assertRaises(MandatoryAccountDimensionError, si.submit)
|
||||
@ -52,53 +53,54 @@ class TestAccountingDimensionFilter(unittest.TestCase):
|
||||
if si.docstatus == 1:
|
||||
si.cancel()
|
||||
|
||||
|
||||
def create_accounting_dimension_filter():
|
||||
if not frappe.db.get_value('Accounting Dimension Filter',
|
||||
{'accounting_dimension': 'Cost Center'}):
|
||||
frappe.get_doc({
|
||||
'doctype': 'Accounting Dimension Filter',
|
||||
'accounting_dimension': 'Cost Center',
|
||||
'allow_or_restrict': 'Allow',
|
||||
'company': '_Test Company',
|
||||
'accounts': [{
|
||||
'applicable_on_account': 'Sales - _TC',
|
||||
}],
|
||||
'dimensions': [{
|
||||
'accounting_dimension': 'Cost Center',
|
||||
'dimension_value': '_Test Cost Center 2 - _TC'
|
||||
}]
|
||||
}).insert()
|
||||
if not frappe.db.get_value(
|
||||
"Accounting Dimension Filter", {"accounting_dimension": "Cost Center"}
|
||||
):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Accounting Dimension Filter",
|
||||
"accounting_dimension": "Cost Center",
|
||||
"allow_or_restrict": "Allow",
|
||||
"company": "_Test Company",
|
||||
"accounts": [
|
||||
{
|
||||
"applicable_on_account": "Sales - _TC",
|
||||
}
|
||||
],
|
||||
"dimensions": [
|
||||
{"accounting_dimension": "Cost Center", "dimension_value": "_Test Cost Center 2 - _TC"}
|
||||
],
|
||||
}
|
||||
).insert()
|
||||
else:
|
||||
doc = frappe.get_doc('Accounting Dimension Filter', {'accounting_dimension': 'Cost Center'})
|
||||
doc = frappe.get_doc("Accounting Dimension Filter", {"accounting_dimension": "Cost Center"})
|
||||
doc.disabled = 0
|
||||
doc.save()
|
||||
|
||||
if not frappe.db.get_value('Accounting Dimension Filter',
|
||||
{'accounting_dimension': 'Department'}):
|
||||
frappe.get_doc({
|
||||
'doctype': 'Accounting Dimension Filter',
|
||||
'accounting_dimension': 'Department',
|
||||
'allow_or_restrict': 'Allow',
|
||||
'company': '_Test Company',
|
||||
'accounts': [{
|
||||
'applicable_on_account': 'Sales - _TC',
|
||||
'is_mandatory': 1
|
||||
}],
|
||||
'dimensions': [{
|
||||
'accounting_dimension': 'Department',
|
||||
'dimension_value': 'Accounts - _TC'
|
||||
}]
|
||||
}).insert()
|
||||
if not frappe.db.get_value("Accounting Dimension Filter", {"accounting_dimension": "Department"}):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Accounting Dimension Filter",
|
||||
"accounting_dimension": "Department",
|
||||
"allow_or_restrict": "Allow",
|
||||
"company": "_Test Company",
|
||||
"accounts": [{"applicable_on_account": "Sales - _TC", "is_mandatory": 1}],
|
||||
"dimensions": [{"accounting_dimension": "Department", "dimension_value": "Accounts - _TC"}],
|
||||
}
|
||||
).insert()
|
||||
else:
|
||||
doc = frappe.get_doc('Accounting Dimension Filter', {'accounting_dimension': 'Department'})
|
||||
doc = frappe.get_doc("Accounting Dimension Filter", {"accounting_dimension": "Department"})
|
||||
doc.disabled = 0
|
||||
doc.save()
|
||||
|
||||
|
||||
def disable_dimension_filter():
|
||||
doc = frappe.get_doc('Accounting Dimension Filter', {'accounting_dimension': 'Cost Center'})
|
||||
doc = frappe.get_doc("Accounting Dimension Filter", {"accounting_dimension": "Cost Center"})
|
||||
doc.disabled = 1
|
||||
doc.save()
|
||||
|
||||
doc = frappe.get_doc('Accounting Dimension Filter', {'accounting_dimension': 'Department'})
|
||||
doc = frappe.get_doc("Accounting Dimension Filter", {"accounting_dimension": "Department"})
|
||||
doc.disabled = 1
|
||||
doc.save()
|
||||
|
||||
@ -7,7 +7,9 @@ from frappe import _
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class OverlapError(frappe.ValidationError): pass
|
||||
class OverlapError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class AccountingPeriod(Document):
|
||||
def validate(self):
|
||||
@ -17,11 +19,12 @@ class AccountingPeriod(Document):
|
||||
self.bootstrap_doctypes_for_closing()
|
||||
|
||||
def autoname(self):
|
||||
company_abbr = frappe.get_cached_value('Company', self.company, "abbr")
|
||||
company_abbr = frappe.get_cached_value("Company", self.company, "abbr")
|
||||
self.name = " - ".join([self.period_name, company_abbr])
|
||||
|
||||
def validate_overlap(self):
|
||||
existing_accounting_period = frappe.db.sql("""select name from `tabAccounting Period`
|
||||
existing_accounting_period = frappe.db.sql(
|
||||
"""select name from `tabAccounting Period`
|
||||
where (
|
||||
(%(start_date)s between start_date and end_date)
|
||||
or (%(end_date)s between start_date and end_date)
|
||||
@ -32,18 +35,29 @@ class AccountingPeriod(Document):
|
||||
"start_date": self.start_date,
|
||||
"end_date": self.end_date,
|
||||
"name": self.name,
|
||||
"company": self.company
|
||||
}, as_dict=True)
|
||||
"company": self.company,
|
||||
},
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
if len(existing_accounting_period) > 0:
|
||||
frappe.throw(_("Accounting Period overlaps with {0}")
|
||||
.format(existing_accounting_period[0].get("name")), OverlapError)
|
||||
frappe.throw(
|
||||
_("Accounting Period overlaps with {0}").format(existing_accounting_period[0].get("name")),
|
||||
OverlapError,
|
||||
)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_doctypes_for_closing(self):
|
||||
docs_for_closing = []
|
||||
doctypes = ["Sales Invoice", "Purchase Invoice", "Journal Entry", "Payroll Entry", \
|
||||
"Bank Clearance", "Asset", "Stock Entry"]
|
||||
doctypes = [
|
||||
"Sales Invoice",
|
||||
"Purchase Invoice",
|
||||
"Journal Entry",
|
||||
"Payroll Entry",
|
||||
"Bank Clearance",
|
||||
"Asset",
|
||||
"Stock Entry",
|
||||
]
|
||||
closed_doctypes = [{"document_type": doctype, "closed": 1} for doctype in doctypes]
|
||||
for closed_doctype in closed_doctypes:
|
||||
docs_for_closing.append(closed_doctype)
|
||||
@ -53,7 +67,7 @@ class AccountingPeriod(Document):
|
||||
def bootstrap_doctypes_for_closing(self):
|
||||
if len(self.closed_documents) == 0:
|
||||
for doctype_for_closing in self.get_doctypes_for_closing():
|
||||
self.append('closed_documents', {
|
||||
"document_type": doctype_for_closing.document_type,
|
||||
"closed": doctype_for_closing.closed
|
||||
})
|
||||
self.append(
|
||||
"closed_documents",
|
||||
{"document_type": doctype_for_closing.document_type, "closed": doctype_for_closing.closed},
|
||||
)
|
||||
|
||||
@ -10,29 +10,38 @@ from erpnext.accounts.doctype.accounting_period.accounting_period import Overlap
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.general_ledger import ClosedAccountingPeriod
|
||||
|
||||
test_dependencies = ['Item']
|
||||
test_dependencies = ["Item"]
|
||||
|
||||
|
||||
class TestAccountingPeriod(unittest.TestCase):
|
||||
def test_overlap(self):
|
||||
ap1 = create_accounting_period(start_date = "2018-04-01",
|
||||
end_date = "2018-06-30", company = "Wind Power LLC")
|
||||
ap1 = create_accounting_period(
|
||||
start_date="2018-04-01", end_date="2018-06-30", company="Wind Power LLC"
|
||||
)
|
||||
ap1.save()
|
||||
|
||||
ap2 = create_accounting_period(start_date = "2018-06-30",
|
||||
end_date = "2018-07-10", company = "Wind Power LLC", period_name = "Test Accounting Period 1")
|
||||
ap2 = create_accounting_period(
|
||||
start_date="2018-06-30",
|
||||
end_date="2018-07-10",
|
||||
company="Wind Power LLC",
|
||||
period_name="Test Accounting Period 1",
|
||||
)
|
||||
self.assertRaises(OverlapError, ap2.save)
|
||||
|
||||
def test_accounting_period(self):
|
||||
ap1 = create_accounting_period(period_name = "Test Accounting Period 2")
|
||||
ap1 = create_accounting_period(period_name="Test Accounting Period 2")
|
||||
ap1.save()
|
||||
|
||||
doc = create_sales_invoice(do_not_submit=1, cost_center="_Test Company - _TC", warehouse="Stores - _TC")
|
||||
doc = create_sales_invoice(
|
||||
do_not_submit=1, cost_center="_Test Company - _TC", warehouse="Stores - _TC"
|
||||
)
|
||||
self.assertRaises(ClosedAccountingPeriod, doc.submit)
|
||||
|
||||
def tearDown(self):
|
||||
for d in frappe.get_all("Accounting Period"):
|
||||
frappe.delete_doc("Accounting Period", d.name)
|
||||
|
||||
|
||||
def create_accounting_period(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
@ -41,8 +50,6 @@ def create_accounting_period(**args):
|
||||
accounting_period.end_date = args.end_date or add_months(nowdate(), 1)
|
||||
accounting_period.company = args.company or "_Test Company"
|
||||
accounting_period.period_name = args.period_name or "_Test_Period_Name_1"
|
||||
accounting_period.append("closed_documents", {
|
||||
"document_type": 'Sales Invoice', "closed": 1
|
||||
})
|
||||
accounting_period.append("closed_documents", {"document_type": "Sales Invoice", "closed": 1})
|
||||
|
||||
return accounting_period
|
||||
|
||||
@ -18,7 +18,6 @@
|
||||
"automatically_fetch_payment_terms",
|
||||
"column_break_17",
|
||||
"enable_common_party_accounting",
|
||||
"enable_discount_accounting",
|
||||
"report_setting_section",
|
||||
"use_custom_cash_flow",
|
||||
"deferred_accounting_settings_section",
|
||||
@ -272,13 +271,6 @@
|
||||
"fieldtype": "Check",
|
||||
"label": "Create Ledger Entries for Change Amount"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "If enabled, additional ledger entries will be made for discounts in a separate Discount Account",
|
||||
"fieldname": "enable_discount_accounting",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable Discount Accounting"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Learn about <a href=\"https://docs.erpnext.com/docs/v13/user/manual/en/accounts/articles/common_party_accounting#:~:text=Common%20Party%20Accounting%20in%20ERPNext,Invoice%20against%20a%20primary%20Supplier.\">Common Party</a>",
|
||||
@ -354,7 +346,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2022-02-04 12:32:36.805652",
|
||||
"modified": "2022-04-08 14:45:06.796418",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
|
||||
@ -18,48 +18,38 @@ class AccountsSettings(Document):
|
||||
frappe.clear_cache()
|
||||
|
||||
def validate(self):
|
||||
frappe.db.set_default("add_taxes_from_item_tax_template",
|
||||
self.get("add_taxes_from_item_tax_template", 0))
|
||||
frappe.db.set_default(
|
||||
"add_taxes_from_item_tax_template", self.get("add_taxes_from_item_tax_template", 0)
|
||||
)
|
||||
|
||||
frappe.db.set_default("enable_common_party_accounting",
|
||||
self.get("enable_common_party_accounting", 0))
|
||||
frappe.db.set_default(
|
||||
"enable_common_party_accounting", self.get("enable_common_party_accounting", 0)
|
||||
)
|
||||
|
||||
self.validate_stale_days()
|
||||
self.enable_payment_schedule_in_print()
|
||||
self.toggle_discount_accounting_fields()
|
||||
self.validate_pending_reposts()
|
||||
|
||||
def validate_stale_days(self):
|
||||
if not self.allow_stale and cint(self.stale_days) <= 0:
|
||||
frappe.msgprint(
|
||||
_("Stale Days should start from 1."), title='Error', indicator='red',
|
||||
raise_exception=1)
|
||||
_("Stale Days should start from 1."), title="Error", indicator="red", raise_exception=1
|
||||
)
|
||||
|
||||
def enable_payment_schedule_in_print(self):
|
||||
show_in_print = cint(self.show_payment_schedule_in_print)
|
||||
for doctype in ("Sales Order", "Sales Invoice", "Purchase Order", "Purchase Invoice"):
|
||||
make_property_setter(doctype, "due_date", "print_hide", show_in_print, "Check", validate_fields_for_doctype=False)
|
||||
make_property_setter(doctype, "payment_schedule", "print_hide", 0 if show_in_print else 1, "Check", validate_fields_for_doctype=False)
|
||||
|
||||
def toggle_discount_accounting_fields(self):
|
||||
enable_discount_accounting = cint(self.enable_discount_accounting)
|
||||
|
||||
for doctype in ["Sales Invoice Item", "Purchase Invoice Item"]:
|
||||
make_property_setter(doctype, "discount_account", "hidden", not(enable_discount_accounting), "Check", validate_fields_for_doctype=False)
|
||||
if enable_discount_accounting:
|
||||
make_property_setter(doctype, "discount_account", "mandatory_depends_on", "eval: doc.discount_amount", "Code", validate_fields_for_doctype=False)
|
||||
else:
|
||||
make_property_setter(doctype, "discount_account", "mandatory_depends_on", "", "Code", validate_fields_for_doctype=False)
|
||||
|
||||
for doctype in ["Sales Invoice", "Purchase Invoice"]:
|
||||
make_property_setter(doctype, "additional_discount_account", "hidden", not(enable_discount_accounting), "Check", validate_fields_for_doctype=False)
|
||||
if enable_discount_accounting:
|
||||
make_property_setter(doctype, "additional_discount_account", "mandatory_depends_on", "eval: doc.discount_amount", "Code", validate_fields_for_doctype=False)
|
||||
else:
|
||||
make_property_setter(doctype, "additional_discount_account", "mandatory_depends_on", "", "Code", validate_fields_for_doctype=False)
|
||||
|
||||
make_property_setter("Item", "default_discount_account", "hidden", not(enable_discount_accounting), "Check", validate_fields_for_doctype=False)
|
||||
|
||||
make_property_setter(
|
||||
doctype, "due_date", "print_hide", show_in_print, "Check", validate_fields_for_doctype=False
|
||||
)
|
||||
make_property_setter(
|
||||
doctype,
|
||||
"payment_schedule",
|
||||
"print_hide",
|
||||
0 if show_in_print else 1,
|
||||
"Check",
|
||||
validate_fields_for_doctype=False,
|
||||
)
|
||||
|
||||
def validate_pending_reposts(self):
|
||||
if self.acc_frozen_upto:
|
||||
|
||||
@ -7,12 +7,12 @@ class TestAccountsSettings(unittest.TestCase):
|
||||
def tearDown(self):
|
||||
# Just in case `save` method succeeds, we need to take things back to default so that other tests
|
||||
# don't break
|
||||
cur_settings = frappe.get_doc('Accounts Settings', 'Accounts Settings')
|
||||
cur_settings = frappe.get_doc("Accounts Settings", "Accounts Settings")
|
||||
cur_settings.allow_stale = 1
|
||||
cur_settings.save()
|
||||
|
||||
def test_stale_days(self):
|
||||
cur_settings = frappe.get_doc('Accounts Settings', 'Accounts Settings')
|
||||
cur_settings = frappe.get_doc("Accounts Settings", "Accounts Settings")
|
||||
cur_settings.allow_stale = 0
|
||||
cur_settings.stale_days = 0
|
||||
|
||||
|
||||
@ -15,4 +15,4 @@ class Bank(Document):
|
||||
load_address_and_contact(self)
|
||||
|
||||
def on_trash(self):
|
||||
delete_contact_and_address('Bank', self.name)
|
||||
delete_contact_and_address("Bank", self.name)
|
||||
|
||||
@ -3,11 +3,6 @@ from frappe import _
|
||||
|
||||
def get_data():
|
||||
return {
|
||||
'fieldname': 'bank',
|
||||
'transactions': [
|
||||
{
|
||||
'label': _('Bank Details'),
|
||||
'items': ['Bank Account', 'Bank Guarantee']
|
||||
}
|
||||
]
|
||||
"fieldname": "bank",
|
||||
"transactions": [{"label": _("Bank Details"), "items": ["Bank Account", "Bank Guarantee"]}],
|
||||
}
|
||||
|
||||
@ -27,7 +27,6 @@
|
||||
"bank_account_no",
|
||||
"address_and_contact",
|
||||
"address_html",
|
||||
"website",
|
||||
"column_break_13",
|
||||
"contact_html",
|
||||
"integration_details_section",
|
||||
@ -156,11 +155,6 @@
|
||||
"fieldtype": "HTML",
|
||||
"label": "Address HTML"
|
||||
},
|
||||
{
|
||||
"fieldname": "website",
|
||||
"fieldtype": "Data",
|
||||
"label": "Website"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_13",
|
||||
"fieldtype": "Column Break"
|
||||
@ -208,7 +202,7 @@
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2020-10-23 16:48:06.303658",
|
||||
"modified": "2022-05-04 15:49:42.620630",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Account",
|
||||
@ -243,5 +237,6 @@
|
||||
"search_fields": "bank,account",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@ -20,7 +20,7 @@ class BankAccount(Document):
|
||||
self.name = self.account_name + " - " + self.bank
|
||||
|
||||
def on_trash(self):
|
||||
delete_contact_and_address('BankAccount', self.name)
|
||||
delete_contact_and_address("BankAccount", self.name)
|
||||
|
||||
def validate(self):
|
||||
self.validate_company()
|
||||
@ -31,9 +31,9 @@ class BankAccount(Document):
|
||||
frappe.throw(_("Company is manadatory for company account"))
|
||||
|
||||
def validate_iban(self):
|
||||
'''
|
||||
"""
|
||||
Algorithm: https://en.wikipedia.org/wiki/International_Bank_Account_Number#Validating_the_IBAN
|
||||
'''
|
||||
"""
|
||||
# IBAN field is optional
|
||||
if not self.iban:
|
||||
return
|
||||
@ -43,7 +43,7 @@ class BankAccount(Document):
|
||||
return str(9 + ord(c) - 64)
|
||||
|
||||
# remove whitespaces, upper case to get the right number from ord()
|
||||
iban = ''.join(self.iban.split(' ')).upper()
|
||||
iban = "".join(self.iban.split(" ")).upper()
|
||||
|
||||
# Move country code and checksum from the start to the end
|
||||
flipped = iban[4:] + iban[:4]
|
||||
@ -52,12 +52,12 @@ class BankAccount(Document):
|
||||
encoded = [encode_char(c) if ord(c) >= 65 and ord(c) <= 90 else c for c in flipped]
|
||||
|
||||
try:
|
||||
to_check = int(''.join(encoded))
|
||||
to_check = int("".join(encoded))
|
||||
except ValueError:
|
||||
frappe.throw(_('IBAN is not valid'))
|
||||
frappe.throw(_("IBAN is not valid"))
|
||||
|
||||
if to_check % 97 != 1:
|
||||
frappe.throw(_('IBAN is not valid'))
|
||||
frappe.throw(_("IBAN is not valid"))
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@ -69,12 +69,14 @@ def make_bank_account(doctype, docname):
|
||||
|
||||
return doc
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_party_bank_account(party_type, party):
|
||||
return frappe.db.get_value(party_type,
|
||||
party, 'default_bank_account')
|
||||
return frappe.db.get_value(party_type, party, "default_bank_account")
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_bank_account_details(bank_account):
|
||||
return frappe.db.get_value("Bank Account",
|
||||
bank_account, ['account', 'bank', 'bank_account_no'], as_dict=1)
|
||||
return frappe.db.get_value(
|
||||
"Bank Account", bank_account, ["account", "bank", "bank_account_no"], as_dict=1
|
||||
)
|
||||
|
||||
@ -3,25 +3,18 @@ from frappe import _
|
||||
|
||||
def get_data():
|
||||
return {
|
||||
'fieldname': 'bank_account',
|
||||
'non_standard_fieldnames': {
|
||||
'Customer': 'default_bank_account',
|
||||
'Supplier': 'default_bank_account',
|
||||
"fieldname": "bank_account",
|
||||
"non_standard_fieldnames": {
|
||||
"Customer": "default_bank_account",
|
||||
"Supplier": "default_bank_account",
|
||||
},
|
||||
'transactions': [
|
||||
"transactions": [
|
||||
{
|
||||
'label': _('Payments'),
|
||||
'items': ['Payment Entry', 'Payment Request', 'Payment Order', 'Payroll Entry']
|
||||
"label": _("Payments"),
|
||||
"items": ["Payment Entry", "Payment Request", "Payment Order", "Payroll Entry"],
|
||||
},
|
||||
{
|
||||
'label': _('Party'),
|
||||
'items': ['Customer', 'Supplier']
|
||||
},
|
||||
{
|
||||
'items': ['Bank Guarantee']
|
||||
},
|
||||
{
|
||||
'items': ['Journal Entry']
|
||||
}
|
||||
]
|
||||
{"label": _("Party"), "items": ["Customer", "Supplier"]},
|
||||
{"items": ["Bank Guarantee"]},
|
||||
{"items": ["Journal Entry"]},
|
||||
],
|
||||
}
|
||||
|
||||
@ -8,28 +8,28 @@ from frappe import ValidationError
|
||||
|
||||
# test_records = frappe.get_test_records('Bank Account')
|
||||
|
||||
class TestBankAccount(unittest.TestCase):
|
||||
|
||||
class TestBankAccount(unittest.TestCase):
|
||||
def test_validate_iban(self):
|
||||
valid_ibans = [
|
||||
'GB82 WEST 1234 5698 7654 32',
|
||||
'DE91 1000 0000 0123 4567 89',
|
||||
'FR76 3000 6000 0112 3456 7890 189'
|
||||
"GB82 WEST 1234 5698 7654 32",
|
||||
"DE91 1000 0000 0123 4567 89",
|
||||
"FR76 3000 6000 0112 3456 7890 189",
|
||||
]
|
||||
|
||||
invalid_ibans = [
|
||||
# wrong checksum (3rd place)
|
||||
'GB72 WEST 1234 5698 7654 32',
|
||||
'DE81 1000 0000 0123 4567 89',
|
||||
'FR66 3000 6000 0112 3456 7890 189'
|
||||
"GB72 WEST 1234 5698 7654 32",
|
||||
"DE81 1000 0000 0123 4567 89",
|
||||
"FR66 3000 6000 0112 3456 7890 189",
|
||||
]
|
||||
|
||||
bank_account = frappe.get_doc({'doctype':'Bank Account'})
|
||||
bank_account = frappe.get_doc({"doctype": "Bank Account"})
|
||||
|
||||
try:
|
||||
bank_account.validate_iban()
|
||||
except AttributeError:
|
||||
msg = 'BankAccount.validate_iban() failed for empty IBAN'
|
||||
msg = "BankAccount.validate_iban() failed for empty IBAN"
|
||||
self.fail(msg=msg)
|
||||
|
||||
for iban in valid_ibans:
|
||||
@ -37,11 +37,11 @@ class TestBankAccount(unittest.TestCase):
|
||||
try:
|
||||
bank_account.validate_iban()
|
||||
except ValidationError:
|
||||
msg = 'BankAccount.validate_iban() failed for valid IBAN {}'.format(iban)
|
||||
msg = "BankAccount.validate_iban() failed for valid IBAN {}".format(iban)
|
||||
self.fail(msg=msg)
|
||||
|
||||
for not_iban in invalid_ibans:
|
||||
bank_account.iban = not_iban
|
||||
msg = 'BankAccount.validate_iban() accepted invalid IBAN {}'.format(not_iban)
|
||||
msg = "BankAccount.validate_iban() accepted invalid IBAN {}".format(not_iban)
|
||||
with self.assertRaises(ValidationError, msg=msg):
|
||||
bank_account.validate_iban()
|
||||
|
||||
@ -5,11 +5,13 @@
|
||||
import frappe
|
||||
from frappe import _, msgprint
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import flt, fmt_money, getdate, nowdate
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.utils import flt, fmt_money, getdate
|
||||
|
||||
import erpnext
|
||||
|
||||
form_grid_templates = {"journal_entries": "templates/form_grid/bank_reconciliation_grid.html"}
|
||||
|
||||
form_grid_templates = {
|
||||
"journal_entries": "templates/form_grid/bank_reconciliation_grid.html"
|
||||
}
|
||||
|
||||
class BankClearance(Document):
|
||||
@frappe.whitelist()
|
||||
@ -24,7 +26,8 @@ class BankClearance(Document):
|
||||
if not self.include_reconciled_entries:
|
||||
condition = "and (clearance_date IS NULL or clearance_date='0000-00-00')"
|
||||
|
||||
journal_entries = frappe.db.sql("""
|
||||
journal_entries = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
"Journal Entry" as payment_document, t1.name as payment_entry,
|
||||
t1.cheque_no as cheque_number, t1.cheque_date,
|
||||
@ -38,12 +41,18 @@ class BankClearance(Document):
|
||||
and ifnull(t1.is_opening, 'No') = 'No' {condition}
|
||||
group by t2.account, t1.name
|
||||
order by t1.posting_date ASC, t1.name DESC
|
||||
""".format(condition=condition), {"account": self.account, "from": self.from_date, "to": self.to_date}, as_dict=1)
|
||||
""".format(
|
||||
condition=condition
|
||||
),
|
||||
{"account": self.account, "from": self.from_date, "to": self.to_date},
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
if self.bank_account:
|
||||
condition += 'and bank_account = %(bank_account)s'
|
||||
condition += "and bank_account = %(bank_account)s"
|
||||
|
||||
payment_entries = frappe.db.sql("""
|
||||
payment_entries = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
"Payment Entry" as payment_document, name as payment_entry,
|
||||
reference_no as cheque_number, reference_date as cheque_date,
|
||||
@ -58,12 +67,69 @@ class BankClearance(Document):
|
||||
{condition}
|
||||
order by
|
||||
posting_date ASC, name DESC
|
||||
""".format(condition=condition), {"account": self.account, "from":self.from_date,
|
||||
"to": self.to_date, "bank_account": self.bank_account}, as_dict=1)
|
||||
""".format(
|
||||
condition=condition
|
||||
),
|
||||
{
|
||||
"account": self.account,
|
||||
"from": self.from_date,
|
||||
"to": self.to_date,
|
||||
"bank_account": self.bank_account,
|
||||
},
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
loan_disbursement = frappe.qb.DocType("Loan Disbursement")
|
||||
|
||||
loan_disbursements = (
|
||||
frappe.qb.from_(loan_disbursement)
|
||||
.select(
|
||||
ConstantColumn("Loan Disbursement").as_("payment_document"),
|
||||
loan_disbursement.name.as_("payment_entry"),
|
||||
loan_disbursement.disbursed_amount.as_("credit"),
|
||||
ConstantColumn(0).as_("debit"),
|
||||
loan_disbursement.reference_number.as_("cheque_number"),
|
||||
loan_disbursement.reference_date.as_("cheque_date"),
|
||||
loan_disbursement.disbursement_date.as_("posting_date"),
|
||||
loan_disbursement.applicant.as_("against_account"),
|
||||
)
|
||||
.where(loan_disbursement.docstatus == 1)
|
||||
.where(loan_disbursement.disbursement_date >= self.from_date)
|
||||
.where(loan_disbursement.disbursement_date <= self.to_date)
|
||||
.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)
|
||||
).run(as_dict=1)
|
||||
|
||||
loan_repayment = frappe.qb.DocType("Loan Repayment")
|
||||
|
||||
loan_repayments = (
|
||||
frappe.qb.from_(loan_repayment)
|
||||
.select(
|
||||
ConstantColumn("Loan Repayment").as_("payment_document"),
|
||||
loan_repayment.name.as_("payment_entry"),
|
||||
loan_repayment.amount_paid.as_("debit"),
|
||||
ConstantColumn(0).as_("credit"),
|
||||
loan_repayment.reference_number.as_("cheque_number"),
|
||||
loan_repayment.reference_date.as_("cheque_date"),
|
||||
loan_repayment.applicant.as_("against_account"),
|
||||
loan_repayment.posting_date,
|
||||
)
|
||||
.where(loan_repayment.docstatus == 1)
|
||||
.where(loan_repayment.clearance_date.isnull())
|
||||
.where(loan_repayment.repay_from_salary == 0)
|
||||
.where(loan_repayment.posting_date >= self.from_date)
|
||||
.where(loan_repayment.posting_date <= self.to_date)
|
||||
.where(loan_repayment.payment_account.isin([self.bank_account, self.account]))
|
||||
.orderby(loan_repayment.posting_date)
|
||||
.orderby(loan_repayment.name, frappe.qb.desc)
|
||||
).run(as_dict=1)
|
||||
|
||||
pos_sales_invoices, pos_purchase_invoices = [], []
|
||||
if self.include_pos_transactions:
|
||||
pos_sales_invoices = frappe.db.sql("""
|
||||
pos_sales_invoices = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
"Sales Invoice Payment" as payment_document, sip.name as payment_entry, sip.amount as debit,
|
||||
si.posting_date, si.customer as against_account, sip.clearance_date,
|
||||
@ -74,9 +140,13 @@ class BankClearance(Document):
|
||||
and account.name = sip.account and si.posting_date >= %(from)s and si.posting_date <= %(to)s
|
||||
order by
|
||||
si.posting_date ASC, si.name DESC
|
||||
""", {"account":self.account, "from":self.from_date, "to":self.to_date}, as_dict=1)
|
||||
""",
|
||||
{"account": self.account, "from": self.from_date, "to": self.to_date},
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
pos_purchase_invoices = frappe.db.sql("""
|
||||
pos_purchase_invoices = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
"Purchase Invoice" as payment_document, pi.name as payment_entry, pi.paid_amount as credit,
|
||||
pi.posting_date, pi.supplier as against_account, pi.clearance_date,
|
||||
@ -87,21 +157,36 @@ class BankClearance(Document):
|
||||
and pi.posting_date >= %(from)s and pi.posting_date <= %(to)s
|
||||
order by
|
||||
pi.posting_date ASC, pi.name DESC
|
||||
""", {"account": self.account, "from": self.from_date, "to": self.to_date}, as_dict=1)
|
||||
""",
|
||||
{"account": self.account, "from": self.from_date, "to": self.to_date},
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
entries = sorted(list(payment_entries) + list(journal_entries + list(pos_sales_invoices) + list(pos_purchase_invoices)),
|
||||
key=lambda k: k['posting_date'] or getdate(nowdate()))
|
||||
entries = sorted(
|
||||
list(payment_entries)
|
||||
+ list(journal_entries)
|
||||
+ list(pos_sales_invoices)
|
||||
+ list(pos_purchase_invoices)
|
||||
+ list(loan_disbursements)
|
||||
+ list(loan_repayments),
|
||||
key=lambda k: getdate(k["posting_date"]),
|
||||
)
|
||||
|
||||
self.set('payment_entries', [])
|
||||
self.set("payment_entries", [])
|
||||
self.total_amount = 0.0
|
||||
default_currency = erpnext.get_default_currency()
|
||||
|
||||
for d in entries:
|
||||
row = self.append('payment_entries', {})
|
||||
row = self.append("payment_entries", {})
|
||||
|
||||
amount = flt(d.get('debit', 0)) - flt(d.get('credit', 0))
|
||||
amount = flt(d.get("debit", 0)) - flt(d.get("credit", 0))
|
||||
|
||||
if not d.get("account_currency"):
|
||||
d.account_currency = default_currency
|
||||
|
||||
formatted_amount = fmt_money(abs(amount), 2, d.account_currency)
|
||||
d.amount = formatted_amount + " " + (_("Dr") if amount > 0 else _("Cr"))
|
||||
d.posting_date = getdate(d.posting_date)
|
||||
|
||||
d.pop("credit")
|
||||
d.pop("debit")
|
||||
@ -112,21 +197,24 @@ class BankClearance(Document):
|
||||
@frappe.whitelist()
|
||||
def update_clearance_date(self):
|
||||
clearance_date_updated = False
|
||||
for d in self.get('payment_entries'):
|
||||
for d in self.get("payment_entries"):
|
||||
if d.clearance_date:
|
||||
if not d.payment_document:
|
||||
frappe.throw(_("Row #{0}: Payment document is required to complete the transaction"))
|
||||
|
||||
if d.cheque_date and getdate(d.clearance_date) < getdate(d.cheque_date):
|
||||
frappe.throw(_("Row #{0}: Clearance date {1} cannot be before Cheque Date {2}")
|
||||
.format(d.idx, d.clearance_date, d.cheque_date))
|
||||
frappe.throw(
|
||||
_("Row #{0}: Clearance date {1} cannot be before Cheque Date {2}").format(
|
||||
d.idx, d.clearance_date, d.cheque_date
|
||||
)
|
||||
)
|
||||
|
||||
if d.clearance_date or self.include_reconciled_entries:
|
||||
if not d.clearance_date:
|
||||
d.clearance_date = None
|
||||
|
||||
payment_entry = frappe.get_doc(d.payment_document, d.payment_entry)
|
||||
payment_entry.db_set('clearance_date', d.clearance_date)
|
||||
payment_entry.db_set("clearance_date", d.clearance_date)
|
||||
|
||||
clearance_date_updated = True
|
||||
|
||||
|
||||
@ -1,9 +1,96 @@
|
||||
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.utils import add_months, getdate
|
||||
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
from erpnext.loan_management.doctype.loan.test_loan import (
|
||||
create_loan,
|
||||
create_loan_accounts,
|
||||
create_loan_type,
|
||||
create_repayment_entry,
|
||||
make_loan_disbursement_entry,
|
||||
)
|
||||
|
||||
|
||||
class TestBankClearance(unittest.TestCase):
|
||||
pass
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
make_bank_account()
|
||||
create_loan_accounts()
|
||||
create_loan_masters()
|
||||
add_transactions()
|
||||
|
||||
# Basic test case to test if bank clearance tool doesn't break
|
||||
# Detailed test can be added later
|
||||
def test_bank_clearance(self):
|
||||
bank_clearance = frappe.get_doc("Bank Clearance")
|
||||
bank_clearance.account = "_Test Bank Clearance - _TC"
|
||||
bank_clearance.from_date = add_months(getdate(), -1)
|
||||
bank_clearance.to_date = getdate()
|
||||
bank_clearance.get_payment_entries()
|
||||
self.assertEqual(len(bank_clearance.payment_entries), 3)
|
||||
|
||||
|
||||
def make_bank_account():
|
||||
if not frappe.db.get_value("Account", "_Test Bank Clearance - _TC"):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Account",
|
||||
"account_type": "Bank",
|
||||
"account_name": "_Test Bank Clearance",
|
||||
"company": "_Test Company",
|
||||
"parent_account": "Bank Accounts - _TC",
|
||||
}
|
||||
).insert()
|
||||
|
||||
|
||||
def create_loan_masters():
|
||||
create_loan_type(
|
||||
"Clearance Loan",
|
||||
2000000,
|
||||
13.5,
|
||||
25,
|
||||
0,
|
||||
5,
|
||||
"Cash",
|
||||
"_Test Bank Clearance - _TC",
|
||||
"_Test Bank Clearance - _TC",
|
||||
"Loan Account - _TC",
|
||||
"Interest Income Account - _TC",
|
||||
"Penalty Income Account - _TC",
|
||||
)
|
||||
|
||||
|
||||
def add_transactions():
|
||||
make_payment_entry()
|
||||
make_loan()
|
||||
|
||||
|
||||
def make_loan():
|
||||
loan = create_loan(
|
||||
"_Test Customer",
|
||||
"Clearance Loan",
|
||||
280000,
|
||||
"Repay Over Number of Periods",
|
||||
20,
|
||||
applicant_type="Customer",
|
||||
)
|
||||
loan.submit()
|
||||
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=getdate())
|
||||
repayment_entry = create_repayment_entry(loan.name, "_Test Customer", getdate(), loan.loan_amount)
|
||||
repayment_entry.save()
|
||||
repayment_entry.submit()
|
||||
|
||||
|
||||
def make_payment_entry():
|
||||
pi = make_purchase_invoice(supplier="_Test Supplier", qty=1, rate=690)
|
||||
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank Clearance - _TC")
|
||||
pe.reference_no = "Conrad Oct 18"
|
||||
pe.reference_date = "2018-10-24"
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
|
||||
@ -23,10 +23,16 @@ class BankGuarantee(Document):
|
||||
if not self.bank:
|
||||
frappe.throw(_("Enter the name of the bank or lending institution before submittting."))
|
||||
|
||||
|
||||
@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]
|
||||
return frappe.db.sql(
|
||||
""" select {columns} from `tab{doctype}` where name=%s""".format(
|
||||
columns=", ".join(column_list), doctype=doctype
|
||||
),
|
||||
docname,
|
||||
as_dict=1,
|
||||
)[0]
|
||||
|
||||
@ -22,48 +22,63 @@ from erpnext.accounts.utils import get_balance_on
|
||||
class BankReconciliationTool(Document):
|
||||
pass
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_bank_transactions(bank_account, from_date = None, to_date = None):
|
||||
def get_bank_transactions(bank_account, from_date=None, to_date=None):
|
||||
# returns bank transactions for a bank account
|
||||
filters = []
|
||||
filters.append(['bank_account', '=', bank_account])
|
||||
filters.append(['docstatus', '=', 1])
|
||||
filters.append(['unallocated_amount', '>', 0])
|
||||
filters.append(["bank_account", "=", bank_account])
|
||||
filters.append(["docstatus", "=", 1])
|
||||
filters.append(["unallocated_amount", ">", 0])
|
||||
if to_date:
|
||||
filters.append(['date', '<=', to_date])
|
||||
filters.append(["date", "<=", to_date])
|
||||
if from_date:
|
||||
filters.append(['date', '>=', from_date])
|
||||
filters.append(["date", ">=", from_date])
|
||||
transactions = frappe.get_all(
|
||||
'Bank Transaction',
|
||||
fields = ['date', 'deposit', 'withdrawal', 'currency',
|
||||
'description', 'name', 'bank_account', 'company',
|
||||
'unallocated_amount', 'reference_number', 'party_type', 'party'],
|
||||
filters = filters
|
||||
"Bank Transaction",
|
||||
fields=[
|
||||
"date",
|
||||
"deposit",
|
||||
"withdrawal",
|
||||
"currency",
|
||||
"description",
|
||||
"name",
|
||||
"bank_account",
|
||||
"company",
|
||||
"unallocated_amount",
|
||||
"reference_number",
|
||||
"party_type",
|
||||
"party",
|
||||
],
|
||||
filters=filters,
|
||||
)
|
||||
return transactions
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_account_balance(bank_account, till_date):
|
||||
# returns account balance till the specified date
|
||||
account = frappe.db.get_value('Bank Account', bank_account, 'account')
|
||||
filters = frappe._dict({
|
||||
"account": account,
|
||||
"report_date": till_date,
|
||||
"include_pos_transactions": 1
|
||||
})
|
||||
account = frappe.db.get_value("Bank Account", bank_account, "account")
|
||||
filters = frappe._dict(
|
||||
{"account": account, "report_date": till_date, "include_pos_transactions": 1}
|
||||
)
|
||||
data = get_entries(filters)
|
||||
|
||||
balance_as_per_system = get_balance_on(filters["account"], filters["report_date"])
|
||||
|
||||
total_debit, total_credit = 0,0
|
||||
total_debit, total_credit = 0, 0
|
||||
for d in data:
|
||||
total_debit += flt(d.debit)
|
||||
total_credit += flt(d.credit)
|
||||
|
||||
amounts_not_reflected_in_system = get_amounts_not_reflected_in_system(filters)
|
||||
|
||||
bank_bal = flt(balance_as_per_system) - flt(total_debit) + flt(total_credit) \
|
||||
bank_bal = (
|
||||
flt(balance_as_per_system)
|
||||
- flt(total_debit)
|
||||
+ flt(total_credit)
|
||||
+ amounts_not_reflected_in_system
|
||||
)
|
||||
|
||||
return bank_bal
|
||||
|
||||
@ -76,71 +91,94 @@ def update_bank_transaction(bank_transaction_name, reference_number, party_type=
|
||||
bank_transaction.party_type = party_type
|
||||
bank_transaction.party = party
|
||||
bank_transaction.save()
|
||||
return frappe.db.get_all('Bank Transaction',
|
||||
filters={
|
||||
'name': bank_transaction_name
|
||||
},
|
||||
fields=['date', 'deposit', 'withdrawal', 'currency',
|
||||
'description', 'name', 'bank_account', 'company',
|
||||
'unallocated_amount', 'reference_number',
|
||||
'party_type', 'party'],
|
||||
return frappe.db.get_all(
|
||||
"Bank Transaction",
|
||||
filters={"name": bank_transaction_name},
|
||||
fields=[
|
||||
"date",
|
||||
"deposit",
|
||||
"withdrawal",
|
||||
"currency",
|
||||
"description",
|
||||
"name",
|
||||
"bank_account",
|
||||
"company",
|
||||
"unallocated_amount",
|
||||
"reference_number",
|
||||
"party_type",
|
||||
"party",
|
||||
],
|
||||
)[0]
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_journal_entry_bts( bank_transaction_name, reference_number=None, reference_date=None, posting_date=None, entry_type=None,
|
||||
second_account=None, mode_of_payment=None, party_type=None, party=None, allow_edit=None):
|
||||
def create_journal_entry_bts(
|
||||
bank_transaction_name,
|
||||
reference_number=None,
|
||||
reference_date=None,
|
||||
posting_date=None,
|
||||
entry_type=None,
|
||||
second_account=None,
|
||||
mode_of_payment=None,
|
||||
party_type=None,
|
||||
party=None,
|
||||
allow_edit=None,
|
||||
):
|
||||
# Create a new journal entry based on the bank transaction
|
||||
bank_transaction = frappe.db.get_values(
|
||||
"Bank Transaction", bank_transaction_name,
|
||||
fieldname=["name", "deposit", "withdrawal", "bank_account"] ,
|
||||
as_dict=True
|
||||
"Bank Transaction",
|
||||
bank_transaction_name,
|
||||
fieldname=["name", "deposit", "withdrawal", "bank_account"],
|
||||
as_dict=True,
|
||||
)[0]
|
||||
company_account = frappe.get_value("Bank Account", bank_transaction.bank_account, "account")
|
||||
account_type = frappe.db.get_value("Account", second_account, "account_type")
|
||||
if account_type in ["Receivable", "Payable"]:
|
||||
if not (party_type and party):
|
||||
frappe.throw(_("Party Type and Party is required for Receivable / Payable account {0}").format( second_account))
|
||||
frappe.throw(
|
||||
_("Party Type and Party is required for Receivable / Payable account {0}").format(
|
||||
second_account
|
||||
)
|
||||
)
|
||||
accounts = []
|
||||
# Multi Currency?
|
||||
accounts.append({
|
||||
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,
|
||||
"party_type":party_type,
|
||||
"party":party,
|
||||
})
|
||||
"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,
|
||||
"party_type": party_type,
|
||||
"party": party,
|
||||
}
|
||||
)
|
||||
|
||||
accounts.append({
|
||||
accounts.append(
|
||||
{
|
||||
"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,
|
||||
})
|
||||
if bank_transaction.withdrawal > 0
|
||||
else 0,
|
||||
"debit_in_account_currency": bank_transaction.deposit if bank_transaction.deposit > 0 else 0,
|
||||
}
|
||||
)
|
||||
|
||||
company = frappe.get_value("Account", company_account, "company")
|
||||
|
||||
journal_entry_dict = {
|
||||
"voucher_type" : entry_type,
|
||||
"company" : company,
|
||||
"posting_date" : posting_date,
|
||||
"cheque_date" : reference_date,
|
||||
"cheque_no" : reference_number,
|
||||
"mode_of_payment" : mode_of_payment
|
||||
"voucher_type": entry_type,
|
||||
"company": company,
|
||||
"posting_date": posting_date,
|
||||
"cheque_date": reference_date,
|
||||
"cheque_no": reference_number,
|
||||
"mode_of_payment": mode_of_payment,
|
||||
}
|
||||
journal_entry = frappe.new_doc('Journal Entry')
|
||||
journal_entry = frappe.new_doc("Journal Entry")
|
||||
journal_entry.update(journal_entry_dict)
|
||||
journal_entry.set("accounts", accounts)
|
||||
|
||||
|
||||
if allow_edit:
|
||||
return journal_entry
|
||||
|
||||
@ -152,21 +190,32 @@ def create_journal_entry_bts( bank_transaction_name, reference_number=None, refe
|
||||
else:
|
||||
paid_amount = bank_transaction.withdrawal
|
||||
|
||||
vouchers = json.dumps([{
|
||||
"payment_doctype":"Journal Entry",
|
||||
"payment_name":journal_entry.name,
|
||||
"amount":paid_amount}])
|
||||
vouchers = json.dumps(
|
||||
[{"payment_doctype": "Journal Entry", "payment_name": journal_entry.name, "amount": paid_amount}]
|
||||
)
|
||||
|
||||
return reconcile_vouchers(bank_transaction.name, vouchers)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_payment_entry_bts( bank_transaction_name, reference_number=None, reference_date=None, party_type=None, party=None, posting_date=None,
|
||||
mode_of_payment=None, project=None, cost_center=None, allow_edit=None):
|
||||
def create_payment_entry_bts(
|
||||
bank_transaction_name,
|
||||
reference_number=None,
|
||||
reference_date=None,
|
||||
party_type=None,
|
||||
party=None,
|
||||
posting_date=None,
|
||||
mode_of_payment=None,
|
||||
project=None,
|
||||
cost_center=None,
|
||||
allow_edit=None,
|
||||
):
|
||||
# Create a new payment entry based on the bank transaction
|
||||
bank_transaction = frappe.db.get_values(
|
||||
"Bank Transaction", bank_transaction_name,
|
||||
fieldname=["name", "unallocated_amount", "deposit", "bank_account"] ,
|
||||
as_dict=True
|
||||
"Bank Transaction",
|
||||
bank_transaction_name,
|
||||
fieldname=["name", "unallocated_amount", "deposit", "bank_account"],
|
||||
as_dict=True,
|
||||
)[0]
|
||||
paid_amount = bank_transaction.unallocated_amount
|
||||
payment_type = "Receive" if bank_transaction.deposit > 0 else "Pay"
|
||||
@ -174,27 +223,26 @@ def create_payment_entry_bts( bank_transaction_name, reference_number=None, refe
|
||||
company_account = frappe.get_value("Bank Account", bank_transaction.bank_account, "account")
|
||||
company = frappe.get_value("Account", company_account, "company")
|
||||
payment_entry_dict = {
|
||||
"company" : company,
|
||||
"payment_type" : payment_type,
|
||||
"reference_no" : reference_number,
|
||||
"reference_date" : reference_date,
|
||||
"party_type" : party_type,
|
||||
"party" : party,
|
||||
"posting_date" : posting_date,
|
||||
"company": company,
|
||||
"payment_type": payment_type,
|
||||
"reference_no": reference_number,
|
||||
"reference_date": reference_date,
|
||||
"party_type": party_type,
|
||||
"party": party,
|
||||
"posting_date": posting_date,
|
||||
"paid_amount": paid_amount,
|
||||
"received_amount": paid_amount
|
||||
"received_amount": paid_amount,
|
||||
}
|
||||
payment_entry = frappe.new_doc("Payment Entry")
|
||||
|
||||
|
||||
payment_entry.update(payment_entry_dict)
|
||||
|
||||
if mode_of_payment:
|
||||
payment_entry.mode_of_payment = mode_of_payment
|
||||
payment_entry.mode_of_payment = mode_of_payment
|
||||
if project:
|
||||
payment_entry.project = project
|
||||
payment_entry.project = project
|
||||
if cost_center:
|
||||
payment_entry.cost_center = cost_center
|
||||
payment_entry.cost_center = cost_center
|
||||
if payment_type == "Receive":
|
||||
payment_entry.paid_to = company_account
|
||||
else:
|
||||
@ -208,84 +256,111 @@ def create_payment_entry_bts( bank_transaction_name, reference_number=None, refe
|
||||
payment_entry.insert()
|
||||
|
||||
payment_entry.submit()
|
||||
vouchers = json.dumps([{
|
||||
"payment_doctype":"Payment Entry",
|
||||
"payment_name":payment_entry.name,
|
||||
"amount":paid_amount}])
|
||||
vouchers = json.dumps(
|
||||
[{"payment_doctype": "Payment Entry", "payment_name": payment_entry.name, "amount": paid_amount}]
|
||||
)
|
||||
return reconcile_vouchers(bank_transaction.name, vouchers)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
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')
|
||||
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)
|
||||
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"))
|
||||
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)
|
||||
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.append(
|
||||
"payment_entries",
|
||||
{
|
||||
"payment_document": voucher["payment_entry"].doctype,
|
||||
"payment_entry": voucher["payment_entry"].name,
|
||||
"allocated_amount": allocated_amount,
|
||||
},
|
||||
)
|
||||
|
||||
transaction.save()
|
||||
transaction.update_allocations()
|
||||
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):
|
||||
# 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]
|
||||
"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
|
||||
|
||||
|
||||
def check_matching(bank_account, company, transaction, document_types):
|
||||
# combine all types of vouchers
|
||||
subquery = get_queries(bank_account, company, transaction, document_types)
|
||||
filters = {
|
||||
"amount": transaction.unallocated_amount,
|
||||
"payment_type" : "Receive" if transaction.deposit > 0 else "Pay",
|
||||
"reference_no": transaction.reference_number,
|
||||
"party_type": transaction.party_type,
|
||||
"party": transaction.party,
|
||||
"bank_account": bank_account
|
||||
}
|
||||
"amount": transaction.unallocated_amount,
|
||||
"payment_type": "Receive" if transaction.deposit > 0 else "Pay",
|
||||
"reference_no": transaction.reference_number,
|
||||
"party_type": transaction.party_type,
|
||||
"party": transaction.party,
|
||||
"bank_account": bank_account,
|
||||
}
|
||||
|
||||
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))
|
||||
|
||||
for query in subquery:
|
||||
matching_vouchers.extend(
|
||||
frappe.db.sql(query, filters,)
|
||||
frappe.db.sql(
|
||||
query,
|
||||
filters,
|
||||
)
|
||||
)
|
||||
|
||||
return sorted(matching_vouchers, key = lambda x: x[0], reverse=True) if matching_vouchers else []
|
||||
return sorted(matching_vouchers, key=lambda x: x[0], reverse=True) if matching_vouchers else []
|
||||
|
||||
|
||||
def get_queries(bank_account, company, transaction, document_types):
|
||||
# get queries to get matching vouchers
|
||||
@ -302,7 +377,7 @@ def get_queries(bank_account, company, transaction, document_types):
|
||||
queries.extend([je_amount_matching])
|
||||
|
||||
if transaction.deposit > 0 and "sales_invoice" in document_types:
|
||||
si_amount_matching = get_si_matching_query(amount_condition)
|
||||
si_amount_matching = get_si_matching_query(amount_condition)
|
||||
queries.extend([si_amount_matching])
|
||||
|
||||
if transaction.withdrawal > 0:
|
||||
@ -316,6 +391,7 @@ def get_queries(bank_account, company, transaction, document_types):
|
||||
|
||||
return queries
|
||||
|
||||
|
||||
def get_loan_vouchers(bank_account, transaction, document_types, filters):
|
||||
vouchers = []
|
||||
amount_condition = True if "exact_match" in document_types else False
|
||||
@ -328,109 +404,91 @@ def get_loan_vouchers(bank_account, transaction, document_types, filters):
|
||||
|
||||
return vouchers
|
||||
|
||||
|
||||
def get_ld_matching_query(bank_account, amount_condition, 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("party_type") and \
|
||||
loan_disbursement.applicant == filters.get("party")
|
||||
matching_party = loan_disbursement.applicant_type == filters.get(
|
||||
"party_type"
|
||||
) and loan_disbursement.applicant == filters.get("party")
|
||||
|
||||
rank = (
|
||||
frappe.qb.terms.Case()
|
||||
.when(matching_reference, 1)
|
||||
.else_(0)
|
||||
rank = frappe.qb.terms.Case().when(matching_reference, 1).else_(0)
|
||||
|
||||
rank1 = frappe.qb.terms.Case().when(matching_party, 1).else_(0)
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(loan_disbursement)
|
||||
.select(
|
||||
rank + rank1 + 1,
|
||||
ConstantColumn("Loan Disbursement").as_("doctype"),
|
||||
loan_disbursement.name,
|
||||
loan_disbursement.disbursed_amount,
|
||||
loan_disbursement.reference_number,
|
||||
loan_disbursement.reference_date,
|
||||
loan_disbursement.applicant_type,
|
||||
loan_disbursement.disbursement_date,
|
||||
)
|
||||
|
||||
rank1 = (
|
||||
frappe.qb.terms.Case()
|
||||
.when(matching_party, 1)
|
||||
.else_(0)
|
||||
)
|
||||
|
||||
query = frappe.qb.from_(loan_disbursement).select(
|
||||
rank + rank1 + 1,
|
||||
ConstantColumn("Loan Disbursement").as_("doctype"),
|
||||
loan_disbursement.name,
|
||||
loan_disbursement.disbursed_amount,
|
||||
loan_disbursement.reference_number,
|
||||
loan_disbursement.reference_date,
|
||||
loan_disbursement.applicant_type,
|
||||
loan_disbursement.disbursement_date
|
||||
).where(
|
||||
loan_disbursement.docstatus == 1
|
||||
).where(
|
||||
loan_disbursement.clearance_date.isnull()
|
||||
).where(
|
||||
loan_disbursement.disbursement_account == bank_account
|
||||
.where(loan_disbursement.docstatus == 1)
|
||||
.where(loan_disbursement.clearance_date.isnull())
|
||||
.where(loan_disbursement.disbursement_account == bank_account)
|
||||
)
|
||||
|
||||
if amount_condition:
|
||||
query.where(
|
||||
loan_disbursement.disbursed_amount == filters.get('amount')
|
||||
)
|
||||
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 <= filters.get("amount"))
|
||||
|
||||
vouchers = query.run(as_list=True)
|
||||
|
||||
return vouchers
|
||||
|
||||
|
||||
def get_lr_matching_query(bank_account, amount_condition, 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("party_type") and \
|
||||
loan_repayment.applicant == filters.get("party")
|
||||
matching_party = loan_repayment.applicant_type == filters.get(
|
||||
"party_type"
|
||||
) and loan_repayment.applicant == filters.get("party")
|
||||
|
||||
rank = (
|
||||
frappe.qb.terms.Case()
|
||||
.when(matching_reference, 1)
|
||||
.else_(0)
|
||||
rank = frappe.qb.terms.Case().when(matching_reference, 1).else_(0)
|
||||
|
||||
rank1 = frappe.qb.terms.Case().when(matching_party, 1).else_(0)
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(loan_repayment)
|
||||
.select(
|
||||
rank + rank1 + 1,
|
||||
ConstantColumn("Loan Repayment").as_("doctype"),
|
||||
loan_repayment.name,
|
||||
loan_repayment.amount_paid,
|
||||
loan_repayment.reference_number,
|
||||
loan_repayment.reference_date,
|
||||
loan_repayment.applicant_type,
|
||||
loan_repayment.posting_date,
|
||||
)
|
||||
|
||||
rank1 = (
|
||||
frappe.qb.terms.Case()
|
||||
.when(matching_party, 1)
|
||||
.else_(0)
|
||||
)
|
||||
|
||||
query = frappe.qb.from_(loan_repayment).select(
|
||||
rank + rank1 + 1,
|
||||
ConstantColumn("Loan Repayment").as_("doctype"),
|
||||
loan_repayment.name,
|
||||
loan_repayment.amount_paid,
|
||||
loan_repayment.reference_number,
|
||||
loan_repayment.reference_date,
|
||||
loan_repayment.applicant_type,
|
||||
loan_repayment.posting_date
|
||||
).where(
|
||||
loan_repayment.docstatus == 1
|
||||
).where(
|
||||
loan_repayment.clearance_date.isnull()
|
||||
).where(
|
||||
loan_repayment.payment_account == bank_account
|
||||
.where(loan_repayment.docstatus == 1)
|
||||
.where(loan_repayment.repay_from_salary == 0)
|
||||
.where(loan_repayment.clearance_date.isnull())
|
||||
.where(loan_repayment.payment_account == bank_account)
|
||||
)
|
||||
|
||||
if amount_condition:
|
||||
query.where(
|
||||
loan_repayment.amount_paid == filters.get('amount')
|
||||
)
|
||||
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 <= filters.get("amount"))
|
||||
|
||||
vouchers = query.run()
|
||||
|
||||
return vouchers
|
||||
|
||||
|
||||
def get_pe_matching_query(amount_condition, account_from_to, transaction):
|
||||
# get matching payment entries query
|
||||
if transaction.deposit > 0:
|
||||
currency_field = "paid_to_account_currency as currency"
|
||||
else:
|
||||
currency_field = "paid_from_account_currency as currency"
|
||||
return f"""
|
||||
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
|
||||
@ -519,6 +577,7 @@ def get_si_matching_query(amount_condition):
|
||||
AND si.docstatus = 1
|
||||
"""
|
||||
|
||||
|
||||
def get_pi_matching_query(amount_condition):
|
||||
# get matching purchase invoice query
|
||||
return f"""
|
||||
@ -544,11 +603,16 @@ def get_pi_matching_query(amount_condition):
|
||||
AND cash_bank_account = %(bank_account)s
|
||||
"""
|
||||
|
||||
|
||||
def get_ec_matching_query(bank_account, company, amount_condition):
|
||||
# get matching Expense Claim query
|
||||
mode_of_payments = [x["parent"] for x in frappe.db.get_all("Mode of Payment Account",
|
||||
filters={"default_account": bank_account}, fields=["parent"])]
|
||||
mode_of_payments = '(\'' + '\', \''.join(mode_of_payments) + '\' )'
|
||||
mode_of_payments = [
|
||||
x["parent"]
|
||||
for x in frappe.db.get_all(
|
||||
"Mode of Payment Account", filters={"default_account": bank_account}, fields=["parent"]
|
||||
)
|
||||
]
|
||||
mode_of_payments = "('" + "', '".join(mode_of_payments) + "' )"
|
||||
company_currency = get_company_currency(company)
|
||||
return f"""
|
||||
SELECT
|
||||
|
||||
@ -200,7 +200,7 @@ frappe.ui.form.on("Bank Statement Import", {
|
||||
})
|
||||
.then((result) => {
|
||||
if (result.length > 0) {
|
||||
frm.add_custom_button("Report Error", () => {
|
||||
frm.add_custom_button(__("Report Error"), () => {
|
||||
let fake_xhr = {
|
||||
responseText: JSON.stringify({
|
||||
exc: result[0].error,
|
||||
|
||||
@ -18,6 +18,7 @@ from openpyxl.utils import get_column_letter
|
||||
|
||||
INVALID_VALUES = ("", None)
|
||||
|
||||
|
||||
class BankStatementImport(DataImport):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(BankStatementImport, self).__init__(*args, **kwargs)
|
||||
@ -49,16 +50,14 @@ class BankStatementImport(DataImport):
|
||||
self.import_file, self.google_sheets_url
|
||||
)
|
||||
|
||||
if 'Bank Account' not in json.dumps(preview['columns']):
|
||||
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.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")
|
||||
)
|
||||
frappe.throw(_("Scheduler is inactive. Cannot import data."), title=_("Scheduler Inactive"))
|
||||
|
||||
enqueued_jobs = [d.get("job_name") for d in get_info()]
|
||||
|
||||
@ -81,21 +80,25 @@ class BankStatementImport(DataImport):
|
||||
|
||||
return False
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_preview_from_template(data_import, import_file=None, google_sheets_url=None):
|
||||
return frappe.get_doc("Bank Statement Import", data_import).get_preview_from_template(
|
||||
import_file, google_sheets_url
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def form_start_import(data_import):
|
||||
return frappe.get_doc("Bank Statement Import", data_import).start_import()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def download_errored_template(data_import_name):
|
||||
data_import = frappe.get_doc("Bank Statement Import", data_import_name)
|
||||
data_import.export_errored_rows()
|
||||
|
||||
|
||||
def parse_data_from_template(raw_data):
|
||||
data = []
|
||||
|
||||
@ -108,7 +111,10 @@ def parse_data_from_template(raw_data):
|
||||
|
||||
return data
|
||||
|
||||
def start_import(data_import, bank_account, import_file_path, google_sheets_url, bank, template_options):
|
||||
|
||||
def start_import(
|
||||
data_import, bank_account, import_file_path, google_sheets_url, bank, template_options
|
||||
):
|
||||
"""This method runs in background job"""
|
||||
|
||||
update_mapping_db(bank, template_options)
|
||||
@ -116,7 +122,7 @@ def start_import(data_import, bank_account, import_file_path, google_sheets_url,
|
||||
data_import = frappe.get_doc("Bank Statement Import", data_import)
|
||||
file = import_file_path if import_file_path else google_sheets_url
|
||||
|
||||
import_file = ImportFile("Bank Transaction", file = file, import_type="Insert New Records")
|
||||
import_file = ImportFile("Bank Transaction", file=file, import_type="Insert New Records")
|
||||
|
||||
data = parse_data_from_template(import_file.raw_data)
|
||||
|
||||
@ -130,22 +136,24 @@ def start_import(data_import, bank_account, import_file_path, google_sheets_url,
|
||||
except Exception:
|
||||
frappe.db.rollback()
|
||||
data_import.db_set("status", "Error")
|
||||
frappe.log_error(title=data_import.name)
|
||||
data_import.log_error("Bank Statement Import failed")
|
||||
finally:
|
||||
frappe.flags.in_import = False
|
||||
|
||||
frappe.publish_realtime("data_import_refresh", {"data_import": data_import.name})
|
||||
|
||||
|
||||
def update_mapping_db(bank, template_options):
|
||||
bank = frappe.get_doc("Bank", bank)
|
||||
for d in bank.bank_transaction_mapping:
|
||||
d.delete()
|
||||
|
||||
for d in json.loads(template_options)["column_to_field_map"].items():
|
||||
bank.append("bank_transaction_mapping", {"bank_transaction_field": d[1] ,"file_field": d[0]} )
|
||||
bank.append("bank_transaction_mapping", {"bank_transaction_field": d[1], "file_field": d[0]})
|
||||
|
||||
bank.save()
|
||||
|
||||
|
||||
def add_bank_account(data, bank_account):
|
||||
bank_account_loc = None
|
||||
if "Bank Account" not in data[0]:
|
||||
@ -161,6 +169,7 @@ def add_bank_account(data, bank_account):
|
||||
else:
|
||||
row.append(bank_account)
|
||||
|
||||
|
||||
def write_files(import_file, data):
|
||||
full_file_path = import_file.file_doc.get_full_path()
|
||||
parts = import_file.file_doc.get_extension()
|
||||
@ -168,11 +177,12 @@ def write_files(import_file, data):
|
||||
extension = extension.lstrip(".")
|
||||
|
||||
if extension == "csv":
|
||||
with open(full_file_path, 'w', newline='') as file:
|
||||
with open(full_file_path, "w", newline="") as file:
|
||||
writer = csv.writer(file)
|
||||
writer.writerows(data)
|
||||
elif extension == "xlsx" or "xls":
|
||||
write_xlsx(data, "trans", file_path = full_file_path)
|
||||
write_xlsx(data, "trans", file_path=full_file_path)
|
||||
|
||||
|
||||
def write_xlsx(data, sheet_name, wb=None, column_widths=None, file_path=None):
|
||||
# from xlsx utils with changes
|
||||
@ -187,19 +197,19 @@ def write_xlsx(data, sheet_name, wb=None, column_widths=None, file_path=None):
|
||||
ws.column_dimensions[get_column_letter(i + 1)].width = column_width
|
||||
|
||||
row1 = ws.row_dimensions[1]
|
||||
row1.font = Font(name='Calibri', bold=True)
|
||||
row1.font = Font(name="Calibri", bold=True)
|
||||
|
||||
for row in data:
|
||||
clean_row = []
|
||||
for item in row:
|
||||
if isinstance(item, str) and (sheet_name not in ['Data Import Template', 'Data Export']):
|
||||
if isinstance(item, str) and (sheet_name not in ["Data Import Template", "Data Export"]):
|
||||
value = handle_html(item)
|
||||
else:
|
||||
value = item
|
||||
|
||||
if isinstance(item, str) and next(ILLEGAL_CHARACTERS_RE.finditer(value), None):
|
||||
# Remove illegal characters from the string
|
||||
value = re.sub(ILLEGAL_CHARACTERS_RE, '', value)
|
||||
value = re.sub(ILLEGAL_CHARACTERS_RE, "", value)
|
||||
|
||||
clean_row.append(value)
|
||||
|
||||
@ -208,19 +218,20 @@ def write_xlsx(data, sheet_name, wb=None, column_widths=None, file_path=None):
|
||||
wb.save(file_path)
|
||||
return True
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def upload_bank_statement(**args):
|
||||
args = frappe._dict(args)
|
||||
bsi = frappe.new_doc("Bank Statement Import")
|
||||
|
||||
if args.company:
|
||||
bsi.update({
|
||||
"company": args.company,
|
||||
})
|
||||
bsi.update(
|
||||
{
|
||||
"company": args.company,
|
||||
}
|
||||
)
|
||||
|
||||
if args.bank_account:
|
||||
bsi.update({
|
||||
"bank_account": args.bank_account
|
||||
})
|
||||
bsi.update({"bank_account": args.bank_account})
|
||||
|
||||
return bsi
|
||||
|
||||
@ -134,7 +134,8 @@
|
||||
{
|
||||
"fieldname": "allocated_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Allocated Amount"
|
||||
"label": "Allocated Amount",
|
||||
"options": "currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "amended_from",
|
||||
@ -152,7 +153,8 @@
|
||||
{
|
||||
"fieldname": "unallocated_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Unallocated Amount"
|
||||
"label": "Unallocated Amount",
|
||||
"options": "currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "party_section",
|
||||
@ -192,10 +194,11 @@
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-04-14 17:31:58.963529",
|
||||
"modified": "2022-03-21 19:05:04.208222",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Transaction",
|
||||
"naming_rule": "By \"Naming Series\" field",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@ -242,6 +245,7 @@
|
||||
],
|
||||
"sort_field": "date",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "bank_account",
|
||||
"track_changes": 1
|
||||
}
|
||||
@ -29,17 +29,26 @@ class BankTransaction(StatusUpdater):
|
||||
|
||||
def update_allocations(self):
|
||||
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 = reduce(
|
||||
lambda x, y: flt(x) + flt(y), [x.allocated_amount for x in self.payment_entries]
|
||||
)
|
||||
else:
|
||||
allocated_amount = 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))
|
||||
frappe.db.set_value(
|
||||
self.doctype,
|
||||
self.name,
|
||||
"unallocated_amount",
|
||||
abs(flt(self.withdrawal) - flt(self.deposit)) - flt(allocated_amount),
|
||||
)
|
||||
|
||||
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)))
|
||||
frappe.db.set_value(
|
||||
self.doctype, self.name, "unallocated_amount", abs(flt(self.withdrawal) - flt(self.deposit))
|
||||
)
|
||||
|
||||
amount = self.deposit or self.withdrawal
|
||||
if amount == self.allocated_amount:
|
||||
@ -49,8 +58,14 @@ class BankTransaction(StatusUpdater):
|
||||
|
||||
def clear_linked_payment_entries(self, for_cancel=False):
|
||||
for payment_entry in self.payment_entries:
|
||||
if payment_entry.payment_document in ["Payment Entry", "Journal Entry", "Purchase Invoice", "Expense Claim", "Loan Repayment",
|
||||
"Loan Disbursement"]:
|
||||
if payment_entry.payment_document in [
|
||||
"Payment Entry",
|
||||
"Journal Entry",
|
||||
"Purchase Invoice",
|
||||
"Expense Claim",
|
||||
"Loan Repayment",
|
||||
"Loan Disbursement",
|
||||
]:
|
||||
self.clear_simple_entry(payment_entry, for_cancel=for_cancel)
|
||||
|
||||
elif payment_entry.payment_document == "Sales Invoice":
|
||||
@ -58,38 +73,41 @@ class BankTransaction(StatusUpdater):
|
||||
|
||||
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 (
|
||||
frappe.db.get_value("Payment Entry", payment_entry.payment_entry, "payment_type")
|
||||
== "Internal Transfer"
|
||||
):
|
||||
if len(get_reconciled_bank_transactions(payment_entry)) < 2:
|
||||
return
|
||||
|
||||
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)
|
||||
payment_entry.payment_document, payment_entry.payment_entry, "clearance_date", clearance_date
|
||||
)
|
||||
|
||||
def clear_sales_invoice(self, payment_entry, for_cancel=False):
|
||||
clearance_date = self.date if not for_cancel else None
|
||||
frappe.db.set_value(
|
||||
"Sales Invoice Payment",
|
||||
dict(
|
||||
parenttype=payment_entry.payment_document,
|
||||
parent=payment_entry.payment_entry
|
||||
),
|
||||
"clearance_date", clearance_date)
|
||||
dict(parenttype=payment_entry.payment_document, parent=payment_entry.payment_entry),
|
||||
"clearance_date",
|
||||
clearance_date,
|
||||
)
|
||||
|
||||
|
||||
def get_reconciled_bank_transactions(payment_entry):
|
||||
reconciled_bank_transactions = frappe.get_all(
|
||||
'Bank Transaction Payments',
|
||||
filters = {
|
||||
'payment_entry': payment_entry.payment_entry
|
||||
},
|
||||
fields = ['parent']
|
||||
"Bank Transaction Payments",
|
||||
filters={"payment_entry": payment_entry.payment_entry},
|
||||
fields=["parent"],
|
||||
)
|
||||
|
||||
return reconciled_bank_transactions
|
||||
|
||||
|
||||
def get_total_allocated_amount(payment_entry):
|
||||
return frappe.db.sql("""
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
SELECT
|
||||
SUM(btp.allocated_amount) as allocated_amount,
|
||||
bt.name
|
||||
@ -102,43 +120,73 @@ def get_total_allocated_amount(payment_entry):
|
||||
AND
|
||||
btp.payment_entry = %s
|
||||
AND
|
||||
bt.docstatus = 1""", (payment_entry.payment_document, payment_entry.payment_entry), as_dict=True)
|
||||
bt.docstatus = 1""",
|
||||
(payment_entry.payment_document, payment_entry.payment_entry),
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
|
||||
def get_paid_amount(payment_entry, currency, bank_account):
|
||||
if payment_entry.payment_document in ["Payment Entry", "Sales Invoice", "Purchase Invoice"]:
|
||||
|
||||
paid_amount_field = "paid_amount"
|
||||
if payment_entry.payment_document == 'Payment Entry':
|
||||
if payment_entry.payment_document == "Payment Entry":
|
||||
doc = frappe.get_doc("Payment Entry", payment_entry.payment_entry)
|
||||
paid_amount_field = ("base_paid_amount"
|
||||
if doc.paid_to_account_currency == currency else "paid_amount")
|
||||
|
||||
return frappe.db.get_value(payment_entry.payment_document,
|
||||
payment_entry.payment_entry, paid_amount_field)
|
||||
if doc.payment_type == "Receive":
|
||||
paid_amount_field = (
|
||||
"received_amount" if doc.paid_to_account_currency == currency else "base_received_amount"
|
||||
)
|
||||
elif doc.payment_type == "Pay":
|
||||
paid_amount_field = (
|
||||
"paid_amount" if doc.paid_to_account_currency == currency else "base_paid_amount"
|
||||
)
|
||||
|
||||
return frappe.db.get_value(
|
||||
payment_entry.payment_document, payment_entry.payment_entry, paid_amount_field
|
||||
)
|
||||
|
||||
elif payment_entry.payment_document == "Journal Entry":
|
||||
return frappe.db.get_value('Journal Entry Account', {'parent': payment_entry.payment_entry, 'account': bank_account},
|
||||
"sum(credit_in_account_currency)")
|
||||
return frappe.db.get_value(
|
||||
"Journal Entry Account",
|
||||
{"parent": payment_entry.payment_entry, "account": bank_account},
|
||||
"sum(credit_in_account_currency)",
|
||||
)
|
||||
|
||||
elif payment_entry.payment_document == "Expense Claim":
|
||||
return frappe.db.get_value(payment_entry.payment_document, payment_entry.payment_entry, "total_amount_reimbursed")
|
||||
return frappe.db.get_value(
|
||||
payment_entry.payment_document, payment_entry.payment_entry, "total_amount_reimbursed"
|
||||
)
|
||||
|
||||
elif payment_entry.payment_document == "Loan Disbursement":
|
||||
return frappe.db.get_value(payment_entry.payment_document, payment_entry.payment_entry, "disbursed_amount")
|
||||
return frappe.db.get_value(
|
||||
payment_entry.payment_document, payment_entry.payment_entry, "disbursed_amount"
|
||||
)
|
||||
|
||||
elif payment_entry.payment_document == "Loan Repayment":
|
||||
return frappe.db.get_value(payment_entry.payment_document, payment_entry.payment_entry, "amount_paid")
|
||||
return frappe.db.get_value(
|
||||
payment_entry.payment_document, payment_entry.payment_entry, "amount_paid"
|
||||
)
|
||||
|
||||
else:
|
||||
frappe.throw("Please reconcile {0}: {1} manually".format(payment_entry.payment_document, payment_entry.payment_entry))
|
||||
frappe.throw(
|
||||
"Please reconcile {0}: {1} manually".format(
|
||||
payment_entry.payment_document, payment_entry.payment_entry
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@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)
|
||||
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)
|
||||
|
||||
|
||||
@ -18,12 +18,14 @@ def upload_bank_statement():
|
||||
fcontent = frappe.local.uploaded_file
|
||||
fname = frappe.local.uploaded_filename
|
||||
|
||||
if frappe.safe_encode(fname).lower().endswith("csv".encode('utf-8')):
|
||||
if frappe.safe_encode(fname).lower().endswith("csv".encode("utf-8")):
|
||||
from frappe.utils.csvutils import read_csv_content
|
||||
|
||||
rows = read_csv_content(fcontent, False)
|
||||
|
||||
elif frappe.safe_encode(fname).lower().endswith("xlsx".encode('utf-8')):
|
||||
elif frappe.safe_encode(fname).lower().endswith("xlsx".encode("utf-8")):
|
||||
from frappe.utils.xlsxutils import read_xlsx_file_from_attached_file
|
||||
|
||||
rows = read_xlsx_file_from_attached_file(fcontent=fcontent)
|
||||
|
||||
columns = rows[0]
|
||||
@ -43,12 +45,10 @@ def create_bank_entries(columns, data, bank_account):
|
||||
continue
|
||||
fields = {}
|
||||
for key, value in header_map.items():
|
||||
fields.update({key: d[int(value)-1]})
|
||||
fields.update({key: d[int(value) - 1]})
|
||||
|
||||
try:
|
||||
bank_transaction = frappe.get_doc({
|
||||
"doctype": "Bank Transaction"
|
||||
})
|
||||
bank_transaction = frappe.get_doc({"doctype": "Bank Transaction"})
|
||||
bank_transaction.update(fields)
|
||||
bank_transaction.date = getdate(parse_date(bank_transaction.date))
|
||||
bank_transaction.bank_account = bank_account
|
||||
@ -56,11 +56,12 @@ def create_bank_entries(columns, data, bank_account):
|
||||
bank_transaction.submit()
|
||||
success += 1
|
||||
except Exception:
|
||||
frappe.log_error(frappe.get_traceback())
|
||||
bank_transaction.log_error("Bank entry creation failed")
|
||||
errors += 1
|
||||
|
||||
return {"success": success, "errors": errors}
|
||||
|
||||
|
||||
def get_header_mapping(columns, bank_account):
|
||||
mapping = get_bank_mapping(bank_account)
|
||||
|
||||
@ -71,10 +72,11 @@ def get_header_mapping(columns, bank_account):
|
||||
|
||||
return header_map
|
||||
|
||||
|
||||
def get_bank_mapping(bank_account):
|
||||
bank_name = frappe.db.get_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}
|
||||
mapping = {row.file_field: row.bank_transaction_field for row in bank.bank_transaction_mapping}
|
||||
|
||||
return mapping
|
||||
|
||||
@ -17,6 +17,7 @@ from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sal
|
||||
|
||||
test_dependencies = ["Item", "Cost Center"]
|
||||
|
||||
|
||||
class TestBankTransaction(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
@ -41,21 +42,34 @@ class TestBankTransaction(unittest.TestCase):
|
||||
|
||||
# This test checks if ERPNext is able to provide a linked payment for a bank transaction based on the amount of the bank transaction.
|
||||
def test_linked_payments(self):
|
||||
bank_transaction = frappe.get_doc("Bank Transaction", dict(description="Re 95282925234 FE/000002917 AT171513000281183046 Conrad Electronic"))
|
||||
linked_payments = get_linked_payments(bank_transaction.name, ['payment_entry', 'exact_match'])
|
||||
bank_transaction = frappe.get_doc(
|
||||
"Bank Transaction",
|
||||
dict(description="Re 95282925234 FE/000002917 AT171513000281183046 Conrad Electronic"),
|
||||
)
|
||||
linked_payments = get_linked_payments(bank_transaction.name, ["payment_entry", "exact_match"])
|
||||
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
|
||||
def test_reconcile(self):
|
||||
bank_transaction = frappe.get_doc("Bank Transaction", dict(description="1512567 BG/000003025 OPSKATTUZWXXX AT776000000098709849 Herr G"))
|
||||
bank_transaction = frappe.get_doc(
|
||||
"Bank Transaction",
|
||||
dict(description="1512567 BG/000003025 OPSKATTUZWXXX AT776000000098709849 Herr G"),
|
||||
)
|
||||
payment = frappe.get_doc("Payment Entry", dict(party="Mr G", paid_amount=1700))
|
||||
vouchers = json.dumps([{
|
||||
"payment_doctype":"Payment Entry",
|
||||
"payment_name":payment.name,
|
||||
"amount":bank_transaction.unallocated_amount}])
|
||||
vouchers = json.dumps(
|
||||
[
|
||||
{
|
||||
"payment_doctype": "Payment Entry",
|
||||
"payment_name": payment.name,
|
||||
"amount": bank_transaction.unallocated_amount,
|
||||
}
|
||||
]
|
||||
)
|
||||
reconcile_vouchers(bank_transaction.name, vouchers)
|
||||
|
||||
unallocated_amount = frappe.db.get_value("Bank Transaction", bank_transaction.name, "unallocated_amount")
|
||||
unallocated_amount = frappe.db.get_value(
|
||||
"Bank Transaction", bank_transaction.name, "unallocated_amount"
|
||||
)
|
||||
self.assertTrue(unallocated_amount == 0)
|
||||
|
||||
clearance_date = frappe.db.get_value("Payment Entry", payment.name, "clearance_date")
|
||||
@ -69,122 +83,177 @@ class TestBankTransaction(unittest.TestCase):
|
||||
|
||||
# Check if ERPNext can correctly filter a linked payments based on the debit/credit amount
|
||||
def test_debit_credit_output(self):
|
||||
bank_transaction = frappe.get_doc("Bank Transaction", dict(description="Auszahlung Karte MC/000002916 AUTOMAT 698769 K002 27.10. 14:07"))
|
||||
linked_payments = get_linked_payments(bank_transaction.name, ['payment_entry', 'exact_match'])
|
||||
bank_transaction = frappe.get_doc(
|
||||
"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"])
|
||||
self.assertTrue(linked_payments[0][3])
|
||||
|
||||
# Check error if already reconciled
|
||||
def test_already_reconciled(self):
|
||||
bank_transaction = frappe.get_doc("Bank Transaction", dict(description="1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G"))
|
||||
bank_transaction = frappe.get_doc(
|
||||
"Bank Transaction",
|
||||
dict(description="1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G"),
|
||||
)
|
||||
payment = frappe.get_doc("Payment Entry", dict(party="Mr G", paid_amount=1200))
|
||||
vouchers = json.dumps([{
|
||||
"payment_doctype":"Payment Entry",
|
||||
"payment_name":payment.name,
|
||||
"amount":bank_transaction.unallocated_amount}])
|
||||
vouchers = json.dumps(
|
||||
[
|
||||
{
|
||||
"payment_doctype": "Payment Entry",
|
||||
"payment_name": payment.name,
|
||||
"amount": bank_transaction.unallocated_amount,
|
||||
}
|
||||
]
|
||||
)
|
||||
reconcile_vouchers(bank_transaction.name, vouchers)
|
||||
|
||||
bank_transaction = frappe.get_doc("Bank Transaction", dict(description="1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G"))
|
||||
bank_transaction = frappe.get_doc(
|
||||
"Bank Transaction",
|
||||
dict(description="1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G"),
|
||||
)
|
||||
payment = frappe.get_doc("Payment Entry", dict(party="Mr G", paid_amount=1200))
|
||||
vouchers = json.dumps([{
|
||||
"payment_doctype":"Payment Entry",
|
||||
"payment_name":payment.name,
|
||||
"amount":bank_transaction.unallocated_amount}])
|
||||
self.assertRaises(frappe.ValidationError, reconcile_vouchers, bank_transaction_name=bank_transaction.name, vouchers=vouchers)
|
||||
vouchers = json.dumps(
|
||||
[
|
||||
{
|
||||
"payment_doctype": "Payment Entry",
|
||||
"payment_name": payment.name,
|
||||
"amount": bank_transaction.unallocated_amount,
|
||||
}
|
||||
]
|
||||
)
|
||||
self.assertRaises(
|
||||
frappe.ValidationError,
|
||||
reconcile_vouchers,
|
||||
bank_transaction_name=bank_transaction.name,
|
||||
vouchers=vouchers,
|
||||
)
|
||||
|
||||
# Raise an error if debitor transaction vs debitor payment
|
||||
def test_clear_sales_invoice(self):
|
||||
bank_transaction = frappe.get_doc("Bank Transaction", dict(description="I2015000011 VD/000002514 ATWWXXX AT4701345000003510057 Bio"))
|
||||
bank_transaction = frappe.get_doc(
|
||||
"Bank Transaction",
|
||||
dict(description="I2015000011 VD/000002514 ATWWXXX AT4701345000003510057 Bio"),
|
||||
)
|
||||
payment = frappe.get_doc("Sales Invoice", dict(customer="Fayva", status=["=", "Paid"]))
|
||||
vouchers = json.dumps([{
|
||||
"payment_doctype":"Sales Invoice",
|
||||
"payment_name":payment.name,
|
||||
"amount":bank_transaction.unallocated_amount}])
|
||||
vouchers = json.dumps(
|
||||
[
|
||||
{
|
||||
"payment_doctype": "Sales Invoice",
|
||||
"payment_name": payment.name,
|
||||
"amount": bank_transaction.unallocated_amount,
|
||||
}
|
||||
]
|
||||
)
|
||||
reconcile_vouchers(bank_transaction.name, vouchers=vouchers)
|
||||
|
||||
self.assertEqual(frappe.db.get_value("Bank Transaction", bank_transaction.name, "unallocated_amount"), 0)
|
||||
self.assertTrue(frappe.db.get_value("Sales Invoice Payment", dict(parent=payment.name), "clearance_date") is not None)
|
||||
self.assertEqual(
|
||||
frappe.db.get_value("Bank Transaction", bank_transaction.name, "unallocated_amount"), 0
|
||||
)
|
||||
self.assertTrue(
|
||||
frappe.db.get_value("Sales Invoice Payment", dict(parent=payment.name), "clearance_date")
|
||||
is not None
|
||||
)
|
||||
|
||||
|
||||
def create_bank_account(bank_name="Citi Bank", account_name="_Test Bank - _TC"):
|
||||
try:
|
||||
frappe.get_doc({
|
||||
"doctype": "Bank",
|
||||
"bank_name":bank_name,
|
||||
}).insert(ignore_if_duplicate=True)
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Bank",
|
||||
"bank_name": bank_name,
|
||||
}
|
||||
).insert(ignore_if_duplicate=True)
|
||||
except frappe.DuplicateEntryError:
|
||||
pass
|
||||
|
||||
try:
|
||||
frappe.get_doc({
|
||||
"doctype": "Bank Account",
|
||||
"account_name":"Checking Account",
|
||||
"bank": bank_name,
|
||||
"account": account_name
|
||||
}).insert(ignore_if_duplicate=True)
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Bank Account",
|
||||
"account_name": "Checking Account",
|
||||
"bank": bank_name,
|
||||
"account": account_name,
|
||||
}
|
||||
).insert(ignore_if_duplicate=True)
|
||||
except frappe.DuplicateEntryError:
|
||||
pass
|
||||
|
||||
|
||||
def add_transactions():
|
||||
create_bank_account()
|
||||
|
||||
doc = frappe.get_doc({
|
||||
"doctype": "Bank Transaction",
|
||||
"description":"1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G",
|
||||
"date": "2018-10-23",
|
||||
"deposit": 1200,
|
||||
"currency": "INR",
|
||||
"bank_account": "Checking Account - Citi Bank"
|
||||
}).insert()
|
||||
doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Bank Transaction",
|
||||
"description": "1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G",
|
||||
"date": "2018-10-23",
|
||||
"deposit": 1200,
|
||||
"currency": "INR",
|
||||
"bank_account": "Checking Account - Citi Bank",
|
||||
}
|
||||
).insert()
|
||||
doc.submit()
|
||||
|
||||
doc = frappe.get_doc({
|
||||
"doctype": "Bank Transaction",
|
||||
"description":"1512567 BG/000003025 OPSKATTUZWXXX AT776000000098709849 Herr G",
|
||||
"date": "2018-10-23",
|
||||
"deposit": 1700,
|
||||
"currency": "INR",
|
||||
"bank_account": "Checking Account - Citi Bank"
|
||||
}).insert()
|
||||
doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Bank Transaction",
|
||||
"description": "1512567 BG/000003025 OPSKATTUZWXXX AT776000000098709849 Herr G",
|
||||
"date": "2018-10-23",
|
||||
"deposit": 1700,
|
||||
"currency": "INR",
|
||||
"bank_account": "Checking Account - Citi Bank",
|
||||
}
|
||||
).insert()
|
||||
doc.submit()
|
||||
|
||||
doc = frappe.get_doc({
|
||||
"doctype": "Bank Transaction",
|
||||
"description":"Re 95282925234 FE/000002917 AT171513000281183046 Conrad Electronic",
|
||||
"date": "2018-10-26",
|
||||
"withdrawal": 690,
|
||||
"currency": "INR",
|
||||
"bank_account": "Checking Account - Citi Bank"
|
||||
}).insert()
|
||||
doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Bank Transaction",
|
||||
"description": "Re 95282925234 FE/000002917 AT171513000281183046 Conrad Electronic",
|
||||
"date": "2018-10-26",
|
||||
"withdrawal": 690,
|
||||
"currency": "INR",
|
||||
"bank_account": "Checking Account - Citi Bank",
|
||||
}
|
||||
).insert()
|
||||
doc.submit()
|
||||
|
||||
doc = frappe.get_doc({
|
||||
"doctype": "Bank Transaction",
|
||||
"description":"Auszahlung Karte MC/000002916 AUTOMAT 698769 K002 27.10. 14:07",
|
||||
"date": "2018-10-27",
|
||||
"deposit": 3900,
|
||||
"currency": "INR",
|
||||
"bank_account": "Checking Account - Citi Bank"
|
||||
}).insert()
|
||||
doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Bank Transaction",
|
||||
"description": "Auszahlung Karte MC/000002916 AUTOMAT 698769 K002 27.10. 14:07",
|
||||
"date": "2018-10-27",
|
||||
"deposit": 3900,
|
||||
"currency": "INR",
|
||||
"bank_account": "Checking Account - Citi Bank",
|
||||
}
|
||||
).insert()
|
||||
doc.submit()
|
||||
|
||||
doc = frappe.get_doc({
|
||||
"doctype": "Bank Transaction",
|
||||
"description":"I2015000011 VD/000002514 ATWWXXX AT4701345000003510057 Bio",
|
||||
"date": "2018-10-27",
|
||||
"withdrawal": 109080,
|
||||
"currency": "INR",
|
||||
"bank_account": "Checking Account - Citi Bank"
|
||||
}).insert()
|
||||
doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Bank Transaction",
|
||||
"description": "I2015000011 VD/000002514 ATWWXXX AT4701345000003510057 Bio",
|
||||
"date": "2018-10-27",
|
||||
"withdrawal": 109080,
|
||||
"currency": "INR",
|
||||
"bank_account": "Checking Account - Citi Bank",
|
||||
}
|
||||
).insert()
|
||||
doc.submit()
|
||||
|
||||
|
||||
def add_vouchers():
|
||||
try:
|
||||
frappe.get_doc({
|
||||
"doctype": "Supplier",
|
||||
"supplier_group":"All Supplier Groups",
|
||||
"supplier_type": "Company",
|
||||
"supplier_name": "Conrad Electronic"
|
||||
}).insert(ignore_if_duplicate=True)
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Supplier",
|
||||
"supplier_group": "All Supplier Groups",
|
||||
"supplier_type": "Company",
|
||||
"supplier_name": "Conrad Electronic",
|
||||
}
|
||||
).insert(ignore_if_duplicate=True)
|
||||
|
||||
except frappe.DuplicateEntryError:
|
||||
pass
|
||||
@ -198,12 +267,14 @@ def add_vouchers():
|
||||
pe.submit()
|
||||
|
||||
try:
|
||||
frappe.get_doc({
|
||||
"doctype": "Supplier",
|
||||
"supplier_group":"All Supplier Groups",
|
||||
"supplier_type": "Company",
|
||||
"supplier_name": "Mr G"
|
||||
}).insert(ignore_if_duplicate=True)
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Supplier",
|
||||
"supplier_group": "All Supplier Groups",
|
||||
"supplier_type": "Company",
|
||||
"supplier_name": "Mr G",
|
||||
}
|
||||
).insert(ignore_if_duplicate=True)
|
||||
except frappe.DuplicateEntryError:
|
||||
pass
|
||||
|
||||
@ -222,26 +293,30 @@ def add_vouchers():
|
||||
pe.submit()
|
||||
|
||||
try:
|
||||
frappe.get_doc({
|
||||
"doctype": "Supplier",
|
||||
"supplier_group":"All Supplier Groups",
|
||||
"supplier_type": "Company",
|
||||
"supplier_name": "Poore Simon's"
|
||||
}).insert(ignore_if_duplicate=True)
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Supplier",
|
||||
"supplier_group": "All Supplier Groups",
|
||||
"supplier_type": "Company",
|
||||
"supplier_name": "Poore Simon's",
|
||||
}
|
||||
).insert(ignore_if_duplicate=True)
|
||||
except frappe.DuplicateEntryError:
|
||||
pass
|
||||
|
||||
try:
|
||||
frappe.get_doc({
|
||||
"doctype": "Customer",
|
||||
"customer_group":"All Customer Groups",
|
||||
"customer_type": "Company",
|
||||
"customer_name": "Poore Simon's"
|
||||
}).insert(ignore_if_duplicate=True)
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Customer",
|
||||
"customer_group": "All Customer Groups",
|
||||
"customer_type": "Company",
|
||||
"customer_name": "Poore Simon's",
|
||||
}
|
||||
).insert(ignore_if_duplicate=True)
|
||||
except frappe.DuplicateEntryError:
|
||||
pass
|
||||
|
||||
pi = make_purchase_invoice(supplier="Poore Simon's", qty=1, rate=3900, is_paid=1, do_not_save =1)
|
||||
pi = make_purchase_invoice(supplier="Poore Simon's", qty=1, rate=3900, is_paid=1, do_not_save=1)
|
||||
pi.cash_bank_account = "_Test Bank - _TC"
|
||||
pi.insert()
|
||||
pi.submit()
|
||||
@ -261,33 +336,31 @@ def add_vouchers():
|
||||
pe.submit()
|
||||
|
||||
try:
|
||||
frappe.get_doc({
|
||||
"doctype": "Customer",
|
||||
"customer_group":"All Customer Groups",
|
||||
"customer_type": "Company",
|
||||
"customer_name": "Fayva"
|
||||
}).insert(ignore_if_duplicate=True)
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Customer",
|
||||
"customer_group": "All Customer Groups",
|
||||
"customer_type": "Company",
|
||||
"customer_name": "Fayva",
|
||||
}
|
||||
).insert(ignore_if_duplicate=True)
|
||||
except frappe.DuplicateEntryError:
|
||||
pass
|
||||
|
||||
mode_of_payment = frappe.get_doc({
|
||||
"doctype": "Mode of Payment",
|
||||
"name": "Cash"
|
||||
})
|
||||
mode_of_payment = frappe.get_doc({"doctype": "Mode of Payment", "name": "Cash"})
|
||||
|
||||
if not frappe.db.get_value('Mode of Payment Account', {'company': "_Test Company", 'parent': "Cash"}):
|
||||
mode_of_payment.append("accounts", {
|
||||
"company": "_Test Company",
|
||||
"default_account": "_Test Bank - _TC"
|
||||
})
|
||||
if not frappe.db.get_value(
|
||||
"Mode of Payment Account", {"company": "_Test Company", "parent": "Cash"}
|
||||
):
|
||||
mode_of_payment.append(
|
||||
"accounts", {"company": "_Test Company", "default_account": "_Test Bank - _TC"}
|
||||
)
|
||||
mode_of_payment.save()
|
||||
|
||||
si = create_sales_invoice(customer="Fayva", qty=1, rate=109080, do_not_save=1)
|
||||
si.is_pos = 1
|
||||
si.append("payments", {
|
||||
"mode_of_payment": "Cash",
|
||||
"account": "_Test Bank - _TC",
|
||||
"amount": 109080
|
||||
})
|
||||
si.append(
|
||||
"payments", {"mode_of_payment": "Cash", "account": "_Test Bank - _TC", "amount": 109080}
|
||||
)
|
||||
si.insert()
|
||||
si.submit()
|
||||
|
||||
@ -14,13 +14,19 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
|
||||
|
||||
class BudgetError(frappe.ValidationError): pass
|
||||
class DuplicateBudgetError(frappe.ValidationError): pass
|
||||
class BudgetError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class DuplicateBudgetError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class Budget(Document):
|
||||
def autoname(self):
|
||||
self.name = make_autoname(self.get(frappe.scrub(self.budget_against))
|
||||
+ "/" + self.fiscal_year + "/.###")
|
||||
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)):
|
||||
@ -35,34 +41,44 @@ class Budget(Document):
|
||||
budget_against = self.get(budget_against_field)
|
||||
|
||||
accounts = [d.account for d in self.accounts] or []
|
||||
existing_budget = frappe.db.sql("""
|
||||
existing_budget = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
b.name, ba.account from `tabBudget` b, `tabBudget Account` ba
|
||||
where
|
||||
ba.parent = b.name and b.docstatus < 2 and b.company = %s and %s=%s and
|
||||
b.fiscal_year=%s and b.name != %s and ba.account in (%s) """
|
||||
% ('%s', budget_against_field, '%s', '%s', '%s', ','.join(['%s'] * len(accounts))),
|
||||
(self.company, budget_against, self.fiscal_year, self.name) + tuple(accounts), as_dict=1)
|
||||
% ("%s", budget_against_field, "%s", "%s", "%s", ",".join(["%s"] * len(accounts))),
|
||||
(self.company, budget_against, self.fiscal_year, self.name) + tuple(accounts),
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
for d in existing_budget:
|
||||
frappe.throw(_("Another Budget record '{0}' already exists against {1} '{2}' and account '{3}' for fiscal year {4}")
|
||||
.format(d.name, self.budget_against, budget_against, d.account, self.fiscal_year), DuplicateBudgetError)
|
||||
frappe.throw(
|
||||
_(
|
||||
"Another Budget record '{0}' already exists against {1} '{2}' and account '{3}' for fiscal year {4}"
|
||||
).format(d.name, self.budget_against, budget_against, d.account, self.fiscal_year),
|
||||
DuplicateBudgetError,
|
||||
)
|
||||
|
||||
def validate_accounts(self):
|
||||
account_list = []
|
||||
for d in self.get('accounts'):
|
||||
for d in self.get("accounts"):
|
||||
if d.account:
|
||||
account_details = frappe.db.get_value("Account", d.account,
|
||||
["is_group", "company", "report_type"], as_dict=1)
|
||||
account_details = frappe.db.get_value(
|
||||
"Account", d.account, ["is_group", "company", "report_type"], as_dict=1
|
||||
)
|
||||
|
||||
if account_details.is_group:
|
||||
frappe.throw(_("Budget cannot be assigned against Group Account {0}").format(d.account))
|
||||
elif account_details.company != self.company:
|
||||
frappe.throw(_("Account {0} does not belongs to company {1}")
|
||||
.format(d.account, self.company))
|
||||
frappe.throw(_("Account {0} does not belongs to company {1}").format(d.account, self.company))
|
||||
elif account_details.report_type != "Profit and Loss":
|
||||
frappe.throw(_("Budget cannot be assigned against {0}, as it's not an Income or Expense account")
|
||||
.format(d.account))
|
||||
frappe.throw(
|
||||
_("Budget cannot be assigned against {0}, as it's not an Income or Expense account").format(
|
||||
d.account
|
||||
)
|
||||
)
|
||||
|
||||
if d.account in account_list:
|
||||
frappe.throw(_("Account {0} has been entered multiple times").format(d.account))
|
||||
@ -70,51 +86,66 @@ class Budget(Document):
|
||||
account_list.append(d.account)
|
||||
|
||||
def set_null_value(self):
|
||||
if self.budget_against == 'Cost Center':
|
||||
if self.budget_against == "Cost Center":
|
||||
self.project = None
|
||||
else:
|
||||
self.cost_center = None
|
||||
|
||||
def validate_applicable_for(self):
|
||||
if (self.applicable_on_material_request
|
||||
and not (self.applicable_on_purchase_order and self.applicable_on_booking_actual_expenses)):
|
||||
frappe.throw(_("Please enable Applicable on Purchase Order and Applicable on Booking Actual Expenses"))
|
||||
if self.applicable_on_material_request and not (
|
||||
self.applicable_on_purchase_order and self.applicable_on_booking_actual_expenses
|
||||
):
|
||||
frappe.throw(
|
||||
_("Please enable Applicable on Purchase Order and Applicable on Booking Actual Expenses")
|
||||
)
|
||||
|
||||
elif (self.applicable_on_purchase_order
|
||||
and not (self.applicable_on_booking_actual_expenses)):
|
||||
elif self.applicable_on_purchase_order and not (self.applicable_on_booking_actual_expenses):
|
||||
frappe.throw(_("Please enable Applicable on Booking Actual Expenses"))
|
||||
|
||||
elif not(self.applicable_on_material_request
|
||||
or self.applicable_on_purchase_order or self.applicable_on_booking_actual_expenses):
|
||||
elif not (
|
||||
self.applicable_on_material_request
|
||||
or self.applicable_on_purchase_order
|
||||
or self.applicable_on_booking_actual_expenses
|
||||
):
|
||||
self.applicable_on_booking_actual_expenses = 1
|
||||
|
||||
|
||||
def validate_expense_against_budget(args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
if args.get('company') and not args.fiscal_year:
|
||||
args.fiscal_year = get_fiscal_year(args.get('posting_date'), company=args.get('company'))[0]
|
||||
frappe.flags.exception_approver_role = frappe.get_cached_value('Company',
|
||||
args.get('company'), 'exception_budget_approver_role')
|
||||
if args.get("company") and not args.fiscal_year:
|
||||
args.fiscal_year = get_fiscal_year(args.get("posting_date"), company=args.get("company"))[0]
|
||||
frappe.flags.exception_approver_role = frappe.get_cached_value(
|
||||
"Company", args.get("company"), "exception_budget_approver_role"
|
||||
)
|
||||
|
||||
if not args.account:
|
||||
args.account = args.get("expense_account")
|
||||
|
||||
if not (args.get('account') and args.get('cost_center')) and args.item_code:
|
||||
if not (args.get("account") and args.get("cost_center")) and args.item_code:
|
||||
args.cost_center, args.account = get_item_details(args)
|
||||
|
||||
if not args.account:
|
||||
return
|
||||
|
||||
for budget_against in ['project', 'cost_center'] + get_accounting_dimensions():
|
||||
if (args.get(budget_against) and args.account
|
||||
and frappe.db.get_value("Account", {"name": args.account, "root_type": "Expense"})):
|
||||
for budget_against in ["project", "cost_center"] + get_accounting_dimensions():
|
||||
if (
|
||||
args.get(budget_against)
|
||||
and args.account
|
||||
and frappe.db.get_value("Account", {"name": args.account, "root_type": "Expense"})
|
||||
):
|
||||
|
||||
doctype = frappe.unscrub(budget_against)
|
||||
|
||||
if frappe.get_cached_value('DocType', doctype, 'is_tree'):
|
||||
if frappe.get_cached_value("DocType", doctype, "is_tree"):
|
||||
lft, rgt = frappe.db.get_value(doctype, args.get(budget_against), ["lft", "rgt"])
|
||||
condition = """and exists(select name from `tab%s`
|
||||
where lft<=%s and rgt>=%s and name=b.%s)""" % (doctype, lft, rgt, budget_against) #nosec
|
||||
where lft<=%s and rgt>=%s and name=b.%s)""" % (
|
||||
doctype,
|
||||
lft,
|
||||
rgt,
|
||||
budget_against,
|
||||
) # nosec
|
||||
args.is_tree = True
|
||||
else:
|
||||
condition = "and b.%s=%s" % (budget_against, frappe.db.escape(args.get(budget_against)))
|
||||
@ -123,7 +154,8 @@ def validate_expense_against_budget(args):
|
||||
args.budget_against_field = budget_against
|
||||
args.budget_against_doctype = doctype
|
||||
|
||||
budget_records = frappe.db.sql("""
|
||||
budget_records = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
b.{budget_against_field} as budget_against, ba.budget_amount, b.monthly_distribution,
|
||||
ifnull(b.applicable_on_material_request, 0) as for_material_request,
|
||||
@ -138,11 +170,17 @@ def validate_expense_against_budget(args):
|
||||
b.name=ba.parent and b.fiscal_year=%s
|
||||
and ba.account=%s and b.docstatus=1
|
||||
{condition}
|
||||
""".format(condition=condition, budget_against_field=budget_against), (args.fiscal_year, args.account), as_dict=True) #nosec
|
||||
""".format(
|
||||
condition=condition, budget_against_field=budget_against
|
||||
),
|
||||
(args.fiscal_year, args.account),
|
||||
as_dict=True,
|
||||
) # nosec
|
||||
|
||||
if budget_records:
|
||||
validate_budget_records(args, budget_records)
|
||||
|
||||
|
||||
def validate_budget_records(args, budget_records):
|
||||
for budget in budget_records:
|
||||
if flt(budget.budget_amount):
|
||||
@ -150,88 +188,118 @@ def validate_budget_records(args, budget_records):
|
||||
yearly_action, monthly_action = get_actions(args, budget)
|
||||
|
||||
if monthly_action in ["Stop", "Warn"]:
|
||||
budget_amount = get_accumulated_monthly_budget(budget.monthly_distribution,
|
||||
args.posting_date, args.fiscal_year, budget.budget_amount)
|
||||
budget_amount = get_accumulated_monthly_budget(
|
||||
budget.monthly_distribution, args.posting_date, args.fiscal_year, budget.budget_amount
|
||||
)
|
||||
|
||||
args["month_end_date"] = get_last_day(args.posting_date)
|
||||
|
||||
compare_expense_with_budget(args, budget_amount,
|
||||
_("Accumulated Monthly"), monthly_action, budget.budget_against, amount)
|
||||
compare_expense_with_budget(
|
||||
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
|
||||
)
|
||||
|
||||
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
|
||||
currency = frappe.get_cached_value('Company', args.company, 'default_currency')
|
||||
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(
|
||||
_(action_for), frappe.bold(args.account), args.budget_against_field,
|
||||
frappe.bold(budget_against),
|
||||
frappe.bold(fmt_money(budget_amount, currency=currency)),
|
||||
frappe.bold(fmt_money(diff, currency=currency)))
|
||||
_(action_for),
|
||||
frappe.bold(args.account),
|
||||
args.budget_against_field,
|
||||
frappe.bold(budget_against),
|
||||
frappe.bold(fmt_money(budget_amount, currency=currency)),
|
||||
frappe.bold(fmt_money(diff, currency=currency)),
|
||||
)
|
||||
|
||||
if (frappe.flags.exception_approver_role
|
||||
and frappe.flags.exception_approver_role in frappe.get_roles(frappe.session.user)):
|
||||
if (
|
||||
frappe.flags.exception_approver_role
|
||||
and frappe.flags.exception_approver_role in frappe.get_roles(frappe.session.user)
|
||||
):
|
||||
action = "Warn"
|
||||
|
||||
if action=="Stop":
|
||||
if action == "Stop":
|
||||
frappe.throw(msg, BudgetError)
|
||||
else:
|
||||
frappe.msgprint(msg, indicator='orange')
|
||||
frappe.msgprint(msg, indicator="orange")
|
||||
|
||||
|
||||
def get_actions(args, budget):
|
||||
yearly_action = budget.action_if_annual_budget_exceeded
|
||||
monthly_action = budget.action_if_accumulated_monthly_budget_exceeded
|
||||
|
||||
if args.get('doctype') == 'Material Request' and budget.for_material_request:
|
||||
if args.get("doctype") == "Material Request" and budget.for_material_request:
|
||||
yearly_action = budget.action_if_annual_budget_exceeded_on_mr
|
||||
monthly_action = budget.action_if_accumulated_monthly_budget_exceeded_on_mr
|
||||
|
||||
elif args.get('doctype') == 'Purchase Order' and budget.for_purchase_order:
|
||||
elif args.get("doctype") == "Purchase Order" and budget.for_purchase_order:
|
||||
yearly_action = budget.action_if_annual_budget_exceeded_on_po
|
||||
monthly_action = budget.action_if_accumulated_monthly_budget_exceeded_on_po
|
||||
|
||||
return yearly_action, monthly_action
|
||||
|
||||
|
||||
def get_amount(args, budget):
|
||||
amount = 0
|
||||
|
||||
if args.get('doctype') == 'Material Request' and budget.for_material_request:
|
||||
amount = (get_requested_amount(args, budget)
|
||||
+ get_ordered_amount(args, budget) + get_actual_expense(args))
|
||||
if args.get("doctype") == "Material Request" and budget.for_material_request:
|
||||
amount = (
|
||||
get_requested_amount(args, budget) + get_ordered_amount(args, budget) + get_actual_expense(args)
|
||||
)
|
||||
|
||||
elif args.get('doctype') == 'Purchase Order' and budget.for_purchase_order:
|
||||
elif args.get("doctype") == "Purchase Order" and budget.for_purchase_order:
|
||||
amount = get_ordered_amount(args, budget) + get_actual_expense(args)
|
||||
|
||||
return amount
|
||||
|
||||
def get_requested_amount(args, budget):
|
||||
item_code = args.get('item_code')
|
||||
condition = get_other_condition(args, budget, 'Material Request')
|
||||
|
||||
data = frappe.db.sql(""" select ifnull((sum(child.stock_qty - child.ordered_qty) * rate), 0) as amount
|
||||
def get_requested_amount(args, budget):
|
||||
item_code = args.get("item_code")
|
||||
condition = get_other_condition(args, budget, "Material Request")
|
||||
|
||||
data = frappe.db.sql(
|
||||
""" select ifnull((sum(child.stock_qty - child.ordered_qty) * rate), 0) as amount
|
||||
from `tabMaterial Request Item` child, `tabMaterial Request` parent where parent.name = child.parent and
|
||||
child.item_code = %s and parent.docstatus = 1 and child.stock_qty > child.ordered_qty and {0} and
|
||||
parent.material_request_type = 'Purchase' and parent.status != 'Stopped'""".format(condition), item_code, as_list=1)
|
||||
parent.material_request_type = 'Purchase' and parent.status != 'Stopped'""".format(
|
||||
condition
|
||||
),
|
||||
item_code,
|
||||
as_list=1,
|
||||
)
|
||||
|
||||
return data[0][0] if data else 0
|
||||
|
||||
|
||||
def get_ordered_amount(args, budget):
|
||||
item_code = args.get('item_code')
|
||||
condition = get_other_condition(args, budget, 'Purchase Order')
|
||||
item_code = args.get("item_code")
|
||||
condition = get_other_condition(args, budget, "Purchase Order")
|
||||
|
||||
data = frappe.db.sql(""" select ifnull(sum(child.amount - child.billed_amt), 0) as amount
|
||||
data = frappe.db.sql(
|
||||
""" select ifnull(sum(child.amount - child.billed_amt), 0) as amount
|
||||
from `tabPurchase Order Item` child, `tabPurchase Order` parent where
|
||||
parent.name = child.parent and child.item_code = %s and parent.docstatus = 1 and child.amount > child.billed_amt
|
||||
and parent.status != 'Closed' and {0}""".format(condition), item_code, as_list=1)
|
||||
and parent.status != 'Closed' and {0}""".format(
|
||||
condition
|
||||
),
|
||||
item_code,
|
||||
as_list=1,
|
||||
)
|
||||
|
||||
return data[0][0] if data else 0
|
||||
|
||||
|
||||
def get_other_condition(args, budget, for_doc):
|
||||
condition = "expense_account = '%s'" % (args.expense_account)
|
||||
budget_against_field = args.get("budget_against_field")
|
||||
@ -239,41 +307,51 @@ def get_other_condition(args, budget, for_doc):
|
||||
if budget_against_field and args.get(budget_against_field):
|
||||
condition += " and child.%s = '%s'" % (budget_against_field, args.get(budget_against_field))
|
||||
|
||||
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('Fiscal Year', args.get('fiscal_year'),
|
||||
['year_start_date', 'year_end_date'])
|
||||
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(
|
||||
"Fiscal Year", args.get("fiscal_year"), ["year_start_date", "year_end_date"]
|
||||
)
|
||||
|
||||
condition += """ and parent.%s
|
||||
between '%s' and '%s' """ %(date_field, start_date, end_date)
|
||||
between '%s' and '%s' """ % (
|
||||
date_field,
|
||||
start_date,
|
||||
end_date,
|
||||
)
|
||||
|
||||
return condition
|
||||
|
||||
|
||||
def get_actual_expense(args):
|
||||
if not args.budget_against_doctype:
|
||||
args.budget_against_doctype = frappe.unscrub(args.budget_against_field)
|
||||
|
||||
budget_against_field = args.get('budget_against_field')
|
||||
condition1 = " and gle.posting_date <= %(month_end_date)s" \
|
||||
if args.get("month_end_date") else ""
|
||||
budget_against_field = args.get("budget_against_field")
|
||||
condition1 = " and gle.posting_date <= %(month_end_date)s" if args.get("month_end_date") else ""
|
||||
|
||||
if args.is_tree:
|
||||
lft_rgt = frappe.db.get_value(args.budget_against_doctype,
|
||||
args.get(budget_against_field), ["lft", "rgt"], as_dict=1)
|
||||
lft_rgt = frappe.db.get_value(
|
||||
args.budget_against_doctype, args.get(budget_against_field), ["lft", "rgt"], as_dict=1
|
||||
)
|
||||
|
||||
args.update(lft_rgt)
|
||||
|
||||
condition2 = """and exists(select name from `tab{doctype}`
|
||||
where lft>=%(lft)s and rgt<=%(rgt)s
|
||||
and name=gle.{budget_against_field})""".format(doctype=args.budget_against_doctype, #nosec
|
||||
budget_against_field=budget_against_field)
|
||||
and name=gle.{budget_against_field})""".format(
|
||||
doctype=args.budget_against_doctype, budget_against_field=budget_against_field # nosec
|
||||
)
|
||||
else:
|
||||
condition2 = """and exists(select name from `tab{doctype}`
|
||||
where name=gle.{budget_against} and
|
||||
gle.{budget_against} = %({budget_against})s)""".format(doctype=args.budget_against_doctype,
|
||||
budget_against = budget_against_field)
|
||||
gle.{budget_against} = %({budget_against})s)""".format(
|
||||
doctype=args.budget_against_doctype, budget_against=budget_against_field
|
||||
)
|
||||
|
||||
amount = flt(frappe.db.sql("""
|
||||
amount = flt(
|
||||
frappe.db.sql(
|
||||
"""
|
||||
select sum(gle.debit) - sum(gle.credit)
|
||||
from `tabGL Entry` gle
|
||||
where gle.account=%(account)s
|
||||
@ -282,46 +360,59 @@ def get_actual_expense(args):
|
||||
and gle.company=%(company)s
|
||||
and gle.docstatus=1
|
||||
{condition2}
|
||||
""".format(condition1=condition1, condition2=condition2), (args))[0][0]) #nosec
|
||||
""".format(
|
||||
condition1=condition1, condition2=condition2
|
||||
),
|
||||
(args),
|
||||
)[0][0]
|
||||
) # nosec
|
||||
|
||||
return amount
|
||||
|
||||
|
||||
def get_accumulated_monthly_budget(monthly_distribution, posting_date, fiscal_year, annual_budget):
|
||||
distribution = {}
|
||||
if monthly_distribution:
|
||||
for d in frappe.db.sql("""select mdp.month, mdp.percentage_allocation
|
||||
for d in frappe.db.sql(
|
||||
"""select mdp.month, mdp.percentage_allocation
|
||||
from `tabMonthly Distribution Percentage` mdp, `tabMonthly Distribution` md
|
||||
where mdp.parent=md.name and md.fiscal_year=%s""", fiscal_year, as_dict=1):
|
||||
distribution.setdefault(d.month, d.percentage_allocation)
|
||||
where mdp.parent=md.name and md.fiscal_year=%s""",
|
||||
fiscal_year,
|
||||
as_dict=1,
|
||||
):
|
||||
distribution.setdefault(d.month, d.percentage_allocation)
|
||||
|
||||
dt = frappe.db.get_value("Fiscal Year", fiscal_year, "year_start_date")
|
||||
accumulated_percentage = 0.0
|
||||
|
||||
while(dt <= getdate(posting_date)):
|
||||
while dt <= getdate(posting_date):
|
||||
if monthly_distribution:
|
||||
accumulated_percentage += distribution.get(getdate(dt).strftime("%B"), 0)
|
||||
else:
|
||||
accumulated_percentage += 100.0/12
|
||||
accumulated_percentage += 100.0 / 12
|
||||
|
||||
dt = add_months(dt, 1)
|
||||
|
||||
return annual_budget * accumulated_percentage / 100
|
||||
|
||||
|
||||
def get_item_details(args):
|
||||
cost_center, expense_account = None, None
|
||||
|
||||
if not args.get('company'):
|
||||
if not args.get("company"):
|
||||
return cost_center, expense_account
|
||||
|
||||
if args.item_code:
|
||||
item_defaults = frappe.db.get_value('Item Default',
|
||||
{'parent': args.item_code, 'company': args.get('company')},
|
||||
['buying_cost_center', 'expense_account'])
|
||||
item_defaults = frappe.db.get_value(
|
||||
"Item Default",
|
||||
{"parent": args.item_code, "company": args.get("company")},
|
||||
["buying_cost_center", "expense_account"],
|
||||
)
|
||||
if item_defaults:
|
||||
cost_center, expense_account = item_defaults
|
||||
|
||||
if not (cost_center and expense_account):
|
||||
for doctype in ['Item Group', 'Company']:
|
||||
for doctype in ["Item Group", "Company"]:
|
||||
data = get_expense_cost_center(doctype, args)
|
||||
|
||||
if not cost_center and data:
|
||||
@ -335,11 +426,15 @@ def get_item_details(args):
|
||||
|
||||
return cost_center, expense_account
|
||||
|
||||
|
||||
def get_expense_cost_center(doctype, args):
|
||||
if doctype == 'Item Group':
|
||||
return frappe.db.get_value('Item Default',
|
||||
{'parent': args.get(frappe.scrub(doctype)), 'company': args.get('company')},
|
||||
['buying_cost_center', 'expense_account'])
|
||||
if doctype == "Item Group":
|
||||
return frappe.db.get_value(
|
||||
"Item Default",
|
||||
{"parent": args.get(frappe.scrub(doctype)), "company": args.get("company")},
|
||||
["buying_cost_center", "expense_account"],
|
||||
)
|
||||
else:
|
||||
return frappe.db.get_value(doctype, args.get(frappe.scrub(doctype)),\
|
||||
['cost_center', 'default_expense_account'])
|
||||
return frappe.db.get_value(
|
||||
doctype, args.get(frappe.scrub(doctype)), ["cost_center", "default_expense_account"]
|
||||
)
|
||||
|
||||
@ -11,7 +11,8 @@ from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journ
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
|
||||
test_dependencies = ['Monthly Distribution']
|
||||
test_dependencies = ["Monthly Distribution"]
|
||||
|
||||
|
||||
class TestBudget(unittest.TestCase):
|
||||
def test_monthly_budget_crossed_ignore(self):
|
||||
@ -19,11 +20,18 @@ class TestBudget(unittest.TestCase):
|
||||
|
||||
budget = make_budget(budget_against="Cost Center")
|
||||
|
||||
jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
|
||||
"_Test Bank - _TC", 40000, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True)
|
||||
jv = make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC",
|
||||
"_Test Bank - _TC",
|
||||
40000,
|
||||
"_Test Cost Center - _TC",
|
||||
posting_date=nowdate(),
|
||||
submit=True,
|
||||
)
|
||||
|
||||
self.assertTrue(frappe.db.get_value("GL Entry",
|
||||
{"voucher_type": "Journal Entry", "voucher_no": jv.name}))
|
||||
self.assertTrue(
|
||||
frappe.db.get_value("GL Entry", {"voucher_type": "Journal Entry", "voucher_no": jv.name})
|
||||
)
|
||||
|
||||
budget.cancel()
|
||||
jv.cancel()
|
||||
@ -33,10 +41,17 @@ class TestBudget(unittest.TestCase):
|
||||
|
||||
budget = make_budget(budget_against="Cost Center")
|
||||
|
||||
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
||||
frappe.db.set_value(
|
||||
"Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop"
|
||||
)
|
||||
|
||||
jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
|
||||
"_Test Bank - _TC", 40000, "_Test Cost Center - _TC", posting_date=nowdate())
|
||||
jv = make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC",
|
||||
"_Test Bank - _TC",
|
||||
40000,
|
||||
"_Test Cost Center - _TC",
|
||||
posting_date=nowdate(),
|
||||
)
|
||||
|
||||
self.assertRaises(BudgetError, jv.submit)
|
||||
|
||||
@ -48,49 +63,65 @@ class TestBudget(unittest.TestCase):
|
||||
|
||||
budget = make_budget(budget_against="Cost Center")
|
||||
|
||||
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
||||
frappe.db.set_value(
|
||||
"Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop"
|
||||
)
|
||||
|
||||
jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
|
||||
"_Test Bank - _TC", 40000, "_Test Cost Center - _TC", posting_date=nowdate())
|
||||
jv = make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC",
|
||||
"_Test Bank - _TC",
|
||||
40000,
|
||||
"_Test Cost Center - _TC",
|
||||
posting_date=nowdate(),
|
||||
)
|
||||
|
||||
self.assertRaises(BudgetError, jv.submit)
|
||||
|
||||
frappe.db.set_value('Company', budget.company, 'exception_budget_approver_role', 'Accounts User')
|
||||
frappe.db.set_value("Company", budget.company, "exception_budget_approver_role", "Accounts User")
|
||||
|
||||
jv.submit()
|
||||
self.assertEqual(frappe.db.get_value('Journal Entry', jv.name, 'docstatus'), 1)
|
||||
self.assertEqual(frappe.db.get_value("Journal Entry", jv.name, "docstatus"), 1)
|
||||
jv.cancel()
|
||||
|
||||
frappe.db.set_value('Company', budget.company, 'exception_budget_approver_role', '')
|
||||
frappe.db.set_value("Company", budget.company, "exception_budget_approver_role", "")
|
||||
|
||||
budget.load_from_db()
|
||||
budget.cancel()
|
||||
|
||||
def test_monthly_budget_crossed_for_mr(self):
|
||||
budget = make_budget(applicable_on_material_request=1,
|
||||
applicable_on_purchase_order=1, action_if_accumulated_monthly_budget_exceeded_on_mr="Stop",
|
||||
budget_against="Cost Center")
|
||||
budget = make_budget(
|
||||
applicable_on_material_request=1,
|
||||
applicable_on_purchase_order=1,
|
||||
action_if_accumulated_monthly_budget_exceeded_on_mr="Stop",
|
||||
budget_against="Cost Center",
|
||||
)
|
||||
|
||||
fiscal_year = get_fiscal_year(nowdate())[0]
|
||||
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
||||
frappe.db.set_value(
|
||||
"Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop"
|
||||
)
|
||||
frappe.db.set_value("Budget", budget.name, "fiscal_year", fiscal_year)
|
||||
|
||||
mr = frappe.get_doc({
|
||||
"doctype": "Material Request",
|
||||
"material_request_type": "Purchase",
|
||||
"transaction_date": nowdate(),
|
||||
"company": budget.company,
|
||||
"items": [{
|
||||
'item_code': '_Test Item',
|
||||
'qty': 1,
|
||||
'uom': "_Test UOM",
|
||||
'warehouse': '_Test Warehouse - _TC',
|
||||
'schedule_date': nowdate(),
|
||||
'rate': 100000,
|
||||
'expense_account': '_Test Account Cost for Goods Sold - _TC',
|
||||
'cost_center': '_Test Cost Center - _TC'
|
||||
}]
|
||||
})
|
||||
mr = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Material Request",
|
||||
"material_request_type": "Purchase",
|
||||
"transaction_date": nowdate(),
|
||||
"company": budget.company,
|
||||
"items": [
|
||||
{
|
||||
"item_code": "_Test Item",
|
||||
"qty": 1,
|
||||
"uom": "_Test UOM",
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"schedule_date": nowdate(),
|
||||
"rate": 100000,
|
||||
"expense_account": "_Test Account Cost for Goods Sold - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
mr.set_missing_values()
|
||||
|
||||
@ -100,11 +131,16 @@ class TestBudget(unittest.TestCase):
|
||||
budget.cancel()
|
||||
|
||||
def test_monthly_budget_crossed_for_po(self):
|
||||
budget = make_budget(applicable_on_purchase_order=1,
|
||||
action_if_accumulated_monthly_budget_exceeded_on_po="Stop", budget_against="Cost Center")
|
||||
budget = make_budget(
|
||||
applicable_on_purchase_order=1,
|
||||
action_if_accumulated_monthly_budget_exceeded_on_po="Stop",
|
||||
budget_against="Cost Center",
|
||||
)
|
||||
|
||||
fiscal_year = get_fiscal_year(nowdate())[0]
|
||||
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
||||
frappe.db.set_value(
|
||||
"Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop"
|
||||
)
|
||||
frappe.db.set_value("Budget", budget.name, "fiscal_year", fiscal_year)
|
||||
|
||||
po = create_purchase_order(transaction_date=nowdate(), do_not_submit=True)
|
||||
@ -122,12 +158,20 @@ class TestBudget(unittest.TestCase):
|
||||
|
||||
budget = make_budget(budget_against="Project")
|
||||
|
||||
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
||||
frappe.db.set_value(
|
||||
"Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop"
|
||||
)
|
||||
|
||||
project = frappe.get_value("Project", {"project_name": "_Test Project"})
|
||||
|
||||
jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
|
||||
"_Test Bank - _TC", 40000, "_Test Cost Center - _TC", project=project, posting_date=nowdate())
|
||||
jv = make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC",
|
||||
"_Test Bank - _TC",
|
||||
40000,
|
||||
"_Test Cost Center - _TC",
|
||||
project=project,
|
||||
posting_date=nowdate(),
|
||||
)
|
||||
|
||||
self.assertRaises(BudgetError, jv.submit)
|
||||
|
||||
@ -139,8 +183,13 @@ class TestBudget(unittest.TestCase):
|
||||
|
||||
budget = make_budget(budget_against="Cost Center")
|
||||
|
||||
jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
|
||||
"_Test Bank - _TC", 250000, "_Test Cost Center - _TC", posting_date=nowdate())
|
||||
jv = make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC",
|
||||
"_Test Bank - _TC",
|
||||
250000,
|
||||
"_Test Cost Center - _TC",
|
||||
posting_date=nowdate(),
|
||||
)
|
||||
|
||||
self.assertRaises(BudgetError, jv.submit)
|
||||
|
||||
@ -153,9 +202,14 @@ class TestBudget(unittest.TestCase):
|
||||
|
||||
project = frappe.get_value("Project", {"project_name": "_Test Project"})
|
||||
|
||||
jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
|
||||
"_Test Bank - _TC", 250000, "_Test Cost Center - _TC",
|
||||
project=project, posting_date=nowdate())
|
||||
jv = make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC",
|
||||
"_Test Bank - _TC",
|
||||
250000,
|
||||
"_Test Cost Center - _TC",
|
||||
project=project,
|
||||
posting_date=nowdate(),
|
||||
)
|
||||
|
||||
self.assertRaises(BudgetError, jv.submit)
|
||||
|
||||
@ -169,14 +223,23 @@ class TestBudget(unittest.TestCase):
|
||||
if month > 9:
|
||||
month = 9
|
||||
|
||||
for i in range(month+1):
|
||||
jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
|
||||
"_Test Bank - _TC", 20000, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True)
|
||||
for i in range(month + 1):
|
||||
jv = make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC",
|
||||
"_Test Bank - _TC",
|
||||
20000,
|
||||
"_Test Cost Center - _TC",
|
||||
posting_date=nowdate(),
|
||||
submit=True,
|
||||
)
|
||||
|
||||
self.assertTrue(frappe.db.get_value("GL Entry",
|
||||
{"voucher_type": "Journal Entry", "voucher_no": jv.name}))
|
||||
self.assertTrue(
|
||||
frappe.db.get_value("GL Entry", {"voucher_type": "Journal Entry", "voucher_no": jv.name})
|
||||
)
|
||||
|
||||
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
||||
frappe.db.set_value(
|
||||
"Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop"
|
||||
)
|
||||
|
||||
self.assertRaises(BudgetError, jv.cancel)
|
||||
|
||||
@ -193,14 +256,23 @@ class TestBudget(unittest.TestCase):
|
||||
|
||||
project = frappe.get_value("Project", {"project_name": "_Test Project"})
|
||||
for i in range(month + 1):
|
||||
jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
|
||||
"_Test Bank - _TC", 20000, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True,
|
||||
project=project)
|
||||
jv = make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC",
|
||||
"_Test Bank - _TC",
|
||||
20000,
|
||||
"_Test Cost Center - _TC",
|
||||
posting_date=nowdate(),
|
||||
submit=True,
|
||||
project=project,
|
||||
)
|
||||
|
||||
self.assertTrue(frappe.db.get_value("GL Entry",
|
||||
{"voucher_type": "Journal Entry", "voucher_no": jv.name}))
|
||||
self.assertTrue(
|
||||
frappe.db.get_value("GL Entry", {"voucher_type": "Journal Entry", "voucher_no": jv.name})
|
||||
)
|
||||
|
||||
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
||||
frappe.db.set_value(
|
||||
"Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop"
|
||||
)
|
||||
|
||||
self.assertRaises(BudgetError, jv.cancel)
|
||||
|
||||
@ -212,10 +284,17 @@ class TestBudget(unittest.TestCase):
|
||||
set_total_expense_zero(nowdate(), "cost_center", "_Test Cost Center 2 - _TC")
|
||||
|
||||
budget = make_budget(budget_against="Cost Center", cost_center="_Test Company - _TC")
|
||||
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
||||
frappe.db.set_value(
|
||||
"Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop"
|
||||
)
|
||||
|
||||
jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
|
||||
"_Test Bank - _TC", 40000, "_Test Cost Center 2 - _TC", posting_date=nowdate())
|
||||
jv = make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC",
|
||||
"_Test Bank - _TC",
|
||||
40000,
|
||||
"_Test Cost Center 2 - _TC",
|
||||
posting_date=nowdate(),
|
||||
)
|
||||
|
||||
self.assertRaises(BudgetError, jv.submit)
|
||||
|
||||
@ -226,19 +305,28 @@ class TestBudget(unittest.TestCase):
|
||||
cost_center = "_Test Cost Center 3 - _TC"
|
||||
|
||||
if not frappe.db.exists("Cost Center", cost_center):
|
||||
frappe.get_doc({
|
||||
'doctype': 'Cost Center',
|
||||
'cost_center_name': '_Test Cost Center 3',
|
||||
'parent_cost_center': "_Test Company - _TC",
|
||||
'company': '_Test Company',
|
||||
'is_group': 0
|
||||
}).insert(ignore_permissions=True)
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Cost Center",
|
||||
"cost_center_name": "_Test Cost Center 3",
|
||||
"parent_cost_center": "_Test Company - _TC",
|
||||
"company": "_Test Company",
|
||||
"is_group": 0,
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
budget = make_budget(budget_against="Cost Center", cost_center=cost_center)
|
||||
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
||||
frappe.db.set_value(
|
||||
"Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop"
|
||||
)
|
||||
|
||||
jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
|
||||
"_Test Bank - _TC", 40000, cost_center, posting_date=nowdate())
|
||||
jv = make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC",
|
||||
"_Test Bank - _TC",
|
||||
40000,
|
||||
cost_center,
|
||||
posting_date=nowdate(),
|
||||
)
|
||||
|
||||
self.assertRaises(BudgetError, jv.submit)
|
||||
|
||||
@ -255,14 +343,16 @@ def set_total_expense_zero(posting_date, budget_against_field=None, budget_again
|
||||
|
||||
fiscal_year = get_fiscal_year(nowdate())[0]
|
||||
|
||||
args = frappe._dict({
|
||||
"account": "_Test Account Cost for Goods Sold - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"monthly_end_date": posting_date,
|
||||
"company": "_Test Company",
|
||||
"fiscal_year": fiscal_year,
|
||||
"budget_against_field": budget_against_field,
|
||||
})
|
||||
args = frappe._dict(
|
||||
{
|
||||
"account": "_Test Account Cost for Goods Sold - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"monthly_end_date": posting_date,
|
||||
"company": "_Test Company",
|
||||
"fiscal_year": fiscal_year,
|
||||
"budget_against_field": budget_against_field,
|
||||
}
|
||||
)
|
||||
|
||||
if not args.get(budget_against_field):
|
||||
args[budget_against_field] = budget_against
|
||||
@ -271,26 +361,42 @@ def set_total_expense_zero(posting_date, budget_against_field=None, budget_again
|
||||
|
||||
if existing_expense:
|
||||
if budget_against_field == "cost_center":
|
||||
make_journal_entry("_Test Account Cost for Goods Sold - _TC",
|
||||
"_Test Bank - _TC", -existing_expense, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True)
|
||||
make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC",
|
||||
"_Test Bank - _TC",
|
||||
-existing_expense,
|
||||
"_Test Cost Center - _TC",
|
||||
posting_date=nowdate(),
|
||||
submit=True,
|
||||
)
|
||||
elif budget_against_field == "project":
|
||||
make_journal_entry("_Test Account Cost for Goods Sold - _TC",
|
||||
"_Test Bank - _TC", -existing_expense, "_Test Cost Center - _TC", submit=True, project=budget_against, posting_date=nowdate())
|
||||
make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC",
|
||||
"_Test Bank - _TC",
|
||||
-existing_expense,
|
||||
"_Test Cost Center - _TC",
|
||||
submit=True,
|
||||
project=budget_against,
|
||||
posting_date=nowdate(),
|
||||
)
|
||||
|
||||
|
||||
def make_budget(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
budget_against=args.budget_against
|
||||
cost_center=args.cost_center
|
||||
budget_against = args.budget_against
|
||||
cost_center = args.cost_center
|
||||
|
||||
fiscal_year = get_fiscal_year(nowdate())[0]
|
||||
|
||||
if budget_against == "Project":
|
||||
project_name = "{0}%".format("_Test Project/" + fiscal_year)
|
||||
budget_list = frappe.get_all("Budget", fields=["name"], filters = {"name": ("like", project_name)})
|
||||
budget_list = frappe.get_all("Budget", fields=["name"], filters={"name": ("like", project_name)})
|
||||
else:
|
||||
cost_center_name = "{0}%".format(cost_center or "_Test Cost Center - _TC/" + fiscal_year)
|
||||
budget_list = frappe.get_all("Budget", fields=["name"], filters = {"name": ("like", cost_center_name)})
|
||||
budget_list = frappe.get_all(
|
||||
"Budget", fields=["name"], filters={"name": ("like", cost_center_name)}
|
||||
)
|
||||
for d in budget_list:
|
||||
frappe.db.sql("delete from `tabBudget` where name = %(name)s", d)
|
||||
frappe.db.sql("delete from `tabBudget Account` where parent = %(name)s", d)
|
||||
@ -300,7 +406,7 @@ def make_budget(**args):
|
||||
if budget_against == "Project":
|
||||
budget.project = frappe.get_value("Project", {"project_name": "_Test Project"})
|
||||
else:
|
||||
budget.cost_center =cost_center or "_Test Cost Center - _TC"
|
||||
budget.cost_center = cost_center or "_Test Cost Center - _TC"
|
||||
|
||||
monthly_distribution = frappe.get_doc("Monthly Distribution", "_Test Distribution")
|
||||
monthly_distribution.fiscal_year = fiscal_year
|
||||
@ -312,20 +418,27 @@ def make_budget(**args):
|
||||
budget.action_if_annual_budget_exceeded = "Stop"
|
||||
budget.action_if_accumulated_monthly_budget_exceeded = "Ignore"
|
||||
budget.budget_against = budget_against
|
||||
budget.append("accounts", {
|
||||
"account": "_Test Account Cost for Goods Sold - _TC",
|
||||
"budget_amount": 200000
|
||||
})
|
||||
budget.append(
|
||||
"accounts", {"account": "_Test Account Cost for Goods Sold - _TC", "budget_amount": 200000}
|
||||
)
|
||||
|
||||
if args.applicable_on_material_request:
|
||||
budget.applicable_on_material_request = 1
|
||||
budget.action_if_annual_budget_exceeded_on_mr = args.action_if_annual_budget_exceeded_on_mr or 'Warn'
|
||||
budget.action_if_accumulated_monthly_budget_exceeded_on_mr = args.action_if_accumulated_monthly_budget_exceeded_on_mr or 'Warn'
|
||||
budget.action_if_annual_budget_exceeded_on_mr = (
|
||||
args.action_if_annual_budget_exceeded_on_mr or "Warn"
|
||||
)
|
||||
budget.action_if_accumulated_monthly_budget_exceeded_on_mr = (
|
||||
args.action_if_accumulated_monthly_budget_exceeded_on_mr or "Warn"
|
||||
)
|
||||
|
||||
if args.applicable_on_purchase_order:
|
||||
budget.applicable_on_purchase_order = 1
|
||||
budget.action_if_annual_budget_exceeded_on_po = args.action_if_annual_budget_exceeded_on_po or 'Warn'
|
||||
budget.action_if_accumulated_monthly_budget_exceeded_on_po = args.action_if_accumulated_monthly_budget_exceeded_on_po or 'Warn'
|
||||
budget.action_if_annual_budget_exceeded_on_po = (
|
||||
args.action_if_annual_budget_exceeded_on_po or "Warn"
|
||||
)
|
||||
budget.action_if_accumulated_monthly_budget_exceeded_on_po = (
|
||||
args.action_if_accumulated_monthly_budget_exceeded_on_po or "Warn"
|
||||
)
|
||||
|
||||
budget.insert()
|
||||
budget.submit()
|
||||
|
||||
@ -1,25 +1,25 @@
|
||||
DEFAULT_MAPPERS = [
|
||||
{
|
||||
'doctype': 'Cash Flow Mapper',
|
||||
'section_footer': 'Net cash generated by operating activities',
|
||||
'section_header': 'Cash flows from operating activities',
|
||||
'section_leader': 'Adjustments for',
|
||||
'section_name': 'Operating Activities',
|
||||
'position': 0,
|
||||
'section_subtotal': 'Cash generated from operations',
|
||||
},
|
||||
{
|
||||
'doctype': 'Cash Flow Mapper',
|
||||
'position': 1,
|
||||
'section_footer': 'Net cash used in investing activities',
|
||||
'section_header': 'Cash flows from investing activities',
|
||||
'section_name': 'Investing Activities'
|
||||
},
|
||||
{
|
||||
'doctype': 'Cash Flow Mapper',
|
||||
'position': 2,
|
||||
'section_footer': 'Net cash used in financing activites',
|
||||
'section_header': 'Cash flows from financing activities',
|
||||
'section_name': 'Financing Activities',
|
||||
}
|
||||
{
|
||||
"doctype": "Cash Flow Mapper",
|
||||
"section_footer": "Net cash generated by operating activities",
|
||||
"section_header": "Cash flows from operating activities",
|
||||
"section_leader": "Adjustments for",
|
||||
"section_name": "Operating Activities",
|
||||
"position": 0,
|
||||
"section_subtotal": "Cash generated from operations",
|
||||
},
|
||||
{
|
||||
"doctype": "Cash Flow Mapper",
|
||||
"position": 1,
|
||||
"section_footer": "Net cash used in investing activities",
|
||||
"section_header": "Cash flows from investing activities",
|
||||
"section_name": "Investing Activities",
|
||||
},
|
||||
{
|
||||
"doctype": "Cash Flow Mapper",
|
||||
"position": 2,
|
||||
"section_footer": "Net cash used in financing activites",
|
||||
"section_header": "Cash flows from financing activities",
|
||||
"section_name": "Financing Activities",
|
||||
},
|
||||
]
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
@ -11,9 +12,11 @@ class CashFlowMapping(Document):
|
||||
self.validate_checked_options()
|
||||
|
||||
def validate_checked_options(self):
|
||||
checked_fields = [d for d in self.meta.fields if d.fieldtype == 'Check' and self.get(d.fieldname) == 1]
|
||||
checked_fields = [
|
||||
d for d in self.meta.fields if d.fieldtype == "Check" and self.get(d.fieldname) == 1
|
||||
]
|
||||
if len(checked_fields) > 1:
|
||||
frappe.throw(
|
||||
frappe._('You can only select a maximum of one option from the list of check boxes.'),
|
||||
title='Error'
|
||||
_("You can only select a maximum of one option from the list of check boxes."),
|
||||
title=_("Error"),
|
||||
)
|
||||
|
||||
@ -9,19 +9,16 @@ import frappe
|
||||
class TestCashFlowMapping(unittest.TestCase):
|
||||
def setUp(self):
|
||||
if frappe.db.exists("Cash Flow Mapping", "Test Mapping"):
|
||||
frappe.delete_doc('Cash Flow Mappping', 'Test Mapping')
|
||||
frappe.delete_doc("Cash Flow Mappping", "Test Mapping")
|
||||
|
||||
def tearDown(self):
|
||||
frappe.delete_doc('Cash Flow Mapping', 'Test Mapping')
|
||||
frappe.delete_doc("Cash Flow Mapping", "Test Mapping")
|
||||
|
||||
def test_multiple_selections_not_allowed(self):
|
||||
doc = frappe.new_doc('Cash Flow Mapping')
|
||||
doc.mapping_name = 'Test Mapping'
|
||||
doc.label = 'Test label'
|
||||
doc.append(
|
||||
'accounts',
|
||||
{'account': 'Accounts Receivable - _TC'}
|
||||
)
|
||||
doc = frappe.new_doc("Cash Flow Mapping")
|
||||
doc.mapping_name = "Test Mapping"
|
||||
doc.label = "Test label"
|
||||
doc.append("accounts", {"account": "Accounts Receivable - _TC"})
|
||||
doc.is_working_capital = 1
|
||||
doc.is_finance_cost = 1
|
||||
|
||||
|
||||
@ -17,11 +17,14 @@ class CashierClosing(Document):
|
||||
self.make_calculations()
|
||||
|
||||
def get_outstanding(self):
|
||||
values = frappe.db.sql("""
|
||||
values = frappe.db.sql(
|
||||
"""
|
||||
select sum(outstanding_amount)
|
||||
from `tabSales Invoice`
|
||||
where posting_date=%s and posting_time>=%s and posting_time<=%s and owner=%s
|
||||
""", (self.date, self.from_time, self.time, self.user))
|
||||
""",
|
||||
(self.date, self.from_time, self.time, self.user),
|
||||
)
|
||||
self.outstanding_amount = flt(values[0][0] if values else 0)
|
||||
|
||||
def make_calculations(self):
|
||||
@ -29,7 +32,9 @@ class CashierClosing(Document):
|
||||
for i in self.payments:
|
||||
total += flt(i.amount)
|
||||
|
||||
self.net_amount = total + self.outstanding_amount + flt(self.expense) - flt(self.custody) + flt(self.returns)
|
||||
self.net_amount = (
|
||||
total + self.outstanding_amount + flt(self.expense) - flt(self.custody) + flt(self.returns)
|
||||
)
|
||||
|
||||
def validate_time(self):
|
||||
if self.from_time >= self.time:
|
||||
|
||||
@ -25,33 +25,41 @@ from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import
|
||||
class ChartofAccountsImporter(Document):
|
||||
def validate(self):
|
||||
if self.import_file:
|
||||
get_coa('Chart of Accounts Importer', 'All Accounts', file_name=self.import_file, for_validate=1)
|
||||
get_coa(
|
||||
"Chart of Accounts Importer", "All Accounts", file_name=self.import_file, for_validate=1
|
||||
)
|
||||
|
||||
|
||||
def validate_columns(data):
|
||||
if not data:
|
||||
frappe.throw(_('No data found. Seems like you uploaded a blank file'))
|
||||
frappe.throw(_("No data found. Seems like you uploaded a blank file"))
|
||||
|
||||
no_of_columns = max([len(d) for d in data])
|
||||
|
||||
if no_of_columns > 7:
|
||||
frappe.throw(_('More columns found than expected. Please compare the uploaded file with standard template'),
|
||||
title=(_("Wrong Template")))
|
||||
frappe.throw(
|
||||
_("More columns found than expected. Please compare the uploaded file with standard template"),
|
||||
title=(_("Wrong Template")),
|
||||
)
|
||||
|
||||
|
||||
@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.db.get_value(
|
||||
"Company", {"name": 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.throw(msg, title=_('Wrong Company'))
|
||||
frappe.bold("Allow Account Creation Against Child Company")
|
||||
)
|
||||
frappe.throw(msg, title=_("Wrong Company"))
|
||||
|
||||
if frappe.db.get_all('GL Entry', {"company": company}, "name", limit=1):
|
||||
if frappe.db.get_all("GL Entry", {"company": company}, "name", limit=1):
|
||||
return False
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def import_coa(file_name, company):
|
||||
# delete existing data for accounts
|
||||
@ -60,7 +68,7 @@ def import_coa(file_name, company):
|
||||
# create accounts
|
||||
file_doc, extension = get_file(file_name)
|
||||
|
||||
if extension == 'csv':
|
||||
if extension == "csv":
|
||||
data = generate_data_from_csv(file_doc)
|
||||
else:
|
||||
data = generate_data_from_excel(file_doc, extension)
|
||||
@ -72,27 +80,33 @@ def import_coa(file_name, company):
|
||||
# trigger on_update for company to reset default accounts
|
||||
set_default_accounts(company)
|
||||
|
||||
|
||||
def get_file(file_name):
|
||||
file_doc = frappe.get_doc("File", {"file_url": file_name})
|
||||
parts = file_doc.get_extension()
|
||||
extension = parts[1]
|
||||
extension = extension.lstrip(".")
|
||||
|
||||
if extension not in ('csv', 'xlsx', 'xls'):
|
||||
frappe.throw(_("Only CSV and Excel files can be used to for importing data. Please check the file format you are trying to upload"))
|
||||
if extension not in ("csv", "xlsx", "xls"):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Only CSV and Excel files can be used to for importing data. Please check the file format you are trying to upload"
|
||||
)
|
||||
)
|
||||
|
||||
return file_doc, extension
|
||||
|
||||
return file_doc, extension
|
||||
|
||||
def generate_data_from_csv(file_doc, as_dict=False):
|
||||
''' read csv file and return the generated nested tree '''
|
||||
"""read csv file and return the generated nested tree"""
|
||||
|
||||
file_path = file_doc.get_full_path()
|
||||
|
||||
data = []
|
||||
with open(file_path, 'r') as in_file:
|
||||
with open(file_path, "r") as in_file:
|
||||
csv_reader = list(csv.reader(in_file))
|
||||
headers = csv_reader[0]
|
||||
del csv_reader[0] # delete top row and headers row
|
||||
del csv_reader[0] # delete top row and headers row
|
||||
|
||||
for row in csv_reader:
|
||||
if as_dict:
|
||||
@ -106,6 +120,7 @@ def generate_data_from_csv(file_doc, as_dict=False):
|
||||
# convert csv data
|
||||
return data
|
||||
|
||||
|
||||
def generate_data_from_excel(file_doc, extension, as_dict=False):
|
||||
content = file_doc.get_content()
|
||||
|
||||
@ -123,20 +138,21 @@ def generate_data_from_excel(file_doc, extension, as_dict=False):
|
||||
data.append({frappe.scrub(header): row[index] for index, header in enumerate(headers)})
|
||||
else:
|
||||
if not row[1]:
|
||||
row[1] = row[0]
|
||||
row[3] = row[2]
|
||||
row[1] = row[0]
|
||||
row[3] = row[2]
|
||||
data.append(row)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_coa(doctype, parent, is_root=False, file_name=None, for_validate=0):
|
||||
''' called by tree view (to fetch node's children) '''
|
||||
"""called by tree view (to fetch node's children)"""
|
||||
|
||||
file_doc, extension = get_file(file_name)
|
||||
parent = None if parent==_('All Accounts') else parent
|
||||
parent = None if parent == _("All Accounts") else parent
|
||||
|
||||
if extension == 'csv':
|
||||
if extension == "csv":
|
||||
data = generate_data_from_csv(file_doc)
|
||||
else:
|
||||
data = generate_data_from_excel(file_doc, extension)
|
||||
@ -146,32 +162,33 @@ def get_coa(doctype, parent, is_root=False, file_name=None, for_validate=0):
|
||||
|
||||
if not for_validate:
|
||||
forest = build_forest(data)
|
||||
accounts = build_tree_from_json("", chart_data=forest, from_coa_importer=True) # returns a list of dict in a tree render-able form
|
||||
accounts = build_tree_from_json(
|
||||
"", chart_data=forest, from_coa_importer=True
|
||||
) # returns a list of dict in a tree render-able form
|
||||
|
||||
# filter out to show data for the selected node only
|
||||
accounts = [d for d in accounts if d['parent_account']==parent]
|
||||
accounts = [d for d in accounts if d["parent_account"] == parent]
|
||||
|
||||
return accounts
|
||||
else:
|
||||
return {
|
||||
'show_import_button': 1
|
||||
}
|
||||
return {"show_import_button": 1}
|
||||
|
||||
|
||||
def build_forest(data):
|
||||
'''
|
||||
converts list of list into a nested tree
|
||||
if a = [[1,1], [1,2], [3,2], [4,4], [5,4]]
|
||||
tree = {
|
||||
1: {
|
||||
2: {
|
||||
3: {}
|
||||
}
|
||||
},
|
||||
4: {
|
||||
5: {}
|
||||
}
|
||||
}
|
||||
'''
|
||||
"""
|
||||
converts list of list into a nested tree
|
||||
if a = [[1,1], [1,2], [3,2], [4,4], [5,4]]
|
||||
tree = {
|
||||
1: {
|
||||
2: {
|
||||
3: {}
|
||||
}
|
||||
},
|
||||
4: {
|
||||
5: {}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
# set the value of nested dictionary
|
||||
def set_nested(d, path, value):
|
||||
@ -195,8 +212,11 @@ def build_forest(data):
|
||||
elif account_name == child:
|
||||
parent_account_list = return_parent(data, parent_account)
|
||||
if not parent_account_list and parent_account:
|
||||
frappe.throw(_("The parent account {0} does not exists in the uploaded template").format(
|
||||
frappe.bold(parent_account)))
|
||||
frappe.throw(
|
||||
_("The parent account {0} does not exists in the uploaded template").format(
|
||||
frappe.bold(parent_account)
|
||||
)
|
||||
)
|
||||
return [child] + parent_account_list
|
||||
|
||||
charts_map, paths = {}, []
|
||||
@ -205,7 +225,15 @@ def build_forest(data):
|
||||
error_messages = []
|
||||
|
||||
for i in data:
|
||||
account_name, parent_account, account_number, parent_account_number, is_group, account_type, root_type = i
|
||||
(
|
||||
account_name,
|
||||
parent_account,
|
||||
account_number,
|
||||
parent_account_number,
|
||||
is_group,
|
||||
account_type,
|
||||
root_type,
|
||||
) = i
|
||||
|
||||
if not account_name:
|
||||
error_messages.append("Row {0}: Please enter Account Name".format(line_no))
|
||||
@ -216,13 +244,17 @@ def build_forest(data):
|
||||
account_name = "{} - {}".format(account_number, account_name)
|
||||
|
||||
charts_map[account_name] = {}
|
||||
charts_map[account_name]['account_name'] = name
|
||||
if account_number: charts_map[account_name]["account_number"] = account_number
|
||||
if cint(is_group) == 1: charts_map[account_name]["is_group"] = is_group
|
||||
if account_type: charts_map[account_name]["account_type"] = account_type
|
||||
if root_type: charts_map[account_name]["root_type"] = root_type
|
||||
charts_map[account_name]["account_name"] = name
|
||||
if account_number:
|
||||
charts_map[account_name]["account_number"] = account_number
|
||||
if cint(is_group) == 1:
|
||||
charts_map[account_name]["is_group"] = is_group
|
||||
if account_type:
|
||||
charts_map[account_name]["account_type"] = account_type
|
||||
if root_type:
|
||||
charts_map[account_name]["root_type"] = root_type
|
||||
path = return_parent(data, account_name)[::-1]
|
||||
paths.append(path) # List of path is created
|
||||
paths.append(path) # List of path is created
|
||||
line_no += 1
|
||||
|
||||
if error_messages:
|
||||
@ -231,27 +263,32 @@ def build_forest(data):
|
||||
out = {}
|
||||
for path in paths:
|
||||
for n, account_name in enumerate(path):
|
||||
set_nested(out, path[:n+1], charts_map[account_name]) # setting the value of nested dictionary.
|
||||
set_nested(
|
||||
out, path[: n + 1], charts_map[account_name]
|
||||
) # setting the value of nested dictionary.
|
||||
|
||||
return out
|
||||
|
||||
|
||||
def build_response_as_excel(writer):
|
||||
filename = frappe.generate_hash("", 10)
|
||||
with open(filename, 'wb') as f:
|
||||
f.write(cstr(writer.getvalue()).encode('utf-8'))
|
||||
with open(filename, "wb") as f:
|
||||
f.write(cstr(writer.getvalue()).encode("utf-8"))
|
||||
f = open(filename)
|
||||
reader = csv.reader(f)
|
||||
|
||||
from frappe.utils.xlsxutils import make_xlsx
|
||||
|
||||
xlsx_file = make_xlsx(reader, "Chart of Accounts Importer Template")
|
||||
|
||||
f.close()
|
||||
os.remove(filename)
|
||||
|
||||
# write out response as a xlsx type
|
||||
frappe.response['filename'] = 'coa_importer_template.xlsx'
|
||||
frappe.response['filecontent'] = xlsx_file.getvalue()
|
||||
frappe.response['type'] = 'binary'
|
||||
frappe.response["filename"] = "coa_importer_template.xlsx"
|
||||
frappe.response["filecontent"] = xlsx_file.getvalue()
|
||||
frappe.response["type"] = "binary"
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def download_template(file_type, template_type):
|
||||
@ -259,34 +296,46 @@ def download_template(file_type, template_type):
|
||||
|
||||
writer = get_template(template_type)
|
||||
|
||||
if file_type == 'CSV':
|
||||
if file_type == "CSV":
|
||||
# download csv file
|
||||
frappe.response['result'] = cstr(writer.getvalue())
|
||||
frappe.response['type'] = 'csv'
|
||||
frappe.response['doctype'] = 'Chart of Accounts Importer'
|
||||
frappe.response["result"] = cstr(writer.getvalue())
|
||||
frappe.response["type"] = "csv"
|
||||
frappe.response["doctype"] = "Chart of Accounts Importer"
|
||||
else:
|
||||
build_response_as_excel(writer)
|
||||
|
||||
|
||||
def get_template(template_type):
|
||||
|
||||
fields = ["Account Name", "Parent Account", "Account Number", "Parent Account Number", "Is Group", "Account Type", "Root Type"]
|
||||
fields = [
|
||||
"Account Name",
|
||||
"Parent Account",
|
||||
"Account Number",
|
||||
"Parent Account Number",
|
||||
"Is Group",
|
||||
"Account Type",
|
||||
"Root Type",
|
||||
]
|
||||
writer = UnicodeWriter()
|
||||
writer.writerow(fields)
|
||||
|
||||
if template_type == 'Blank Template':
|
||||
for root_type in get_root_types():
|
||||
writer.writerow(['', '', '', 1, '', root_type])
|
||||
if template_type == "Blank Template":
|
||||
for root_type in get_root_types():
|
||||
writer.writerow(["", "", "", 1, "", root_type])
|
||||
|
||||
for account in get_mandatory_group_accounts():
|
||||
writer.writerow(['', '', '', 1, account, "Asset"])
|
||||
writer.writerow(["", "", "", 1, account, "Asset"])
|
||||
|
||||
for account_type in get_mandatory_account_types():
|
||||
writer.writerow(['', '', '', 0, account_type.get('account_type'), account_type.get('root_type')])
|
||||
writer.writerow(
|
||||
["", "", "", 0, account_type.get("account_type"), account_type.get("root_type")]
|
||||
)
|
||||
else:
|
||||
writer = get_sample_template(writer)
|
||||
|
||||
return writer
|
||||
|
||||
|
||||
def get_sample_template(writer):
|
||||
template = [
|
||||
["Application Of Funds(Assets)", "", "", "", 1, "", "Asset"],
|
||||
@ -316,7 +365,7 @@ def get_sample_template(writer):
|
||||
|
||||
@frappe.whitelist()
|
||||
def validate_accounts(file_doc, extension):
|
||||
if extension == 'csv':
|
||||
if extension == "csv":
|
||||
accounts = generate_data_from_csv(file_doc, as_dict=True)
|
||||
else:
|
||||
accounts = generate_data_from_excel(file_doc, extension, as_dict=True)
|
||||
@ -325,7 +374,9 @@ def validate_accounts(file_doc, extension):
|
||||
for account in accounts:
|
||||
accounts_dict.setdefault(account["account_name"], account)
|
||||
if "parent_account" not in account:
|
||||
msg = _("Please make sure the file you are using has 'Parent Account' column present in the header.")
|
||||
msg = _(
|
||||
"Please make sure the file you are using has 'Parent Account' column present in the header."
|
||||
)
|
||||
msg += "<br><br>"
|
||||
msg += _("Alternatively, you can download the template and fill your data in.")
|
||||
frappe.throw(msg, title=_("Parent Account Missing"))
|
||||
@ -336,77 +387,106 @@ def validate_accounts(file_doc, extension):
|
||||
|
||||
return [True, len(accounts)]
|
||||
|
||||
|
||||
def validate_root(accounts):
|
||||
roots = [accounts[d] for d in accounts if not accounts[d].get('parent_account')]
|
||||
roots = [accounts[d] for d in accounts if not accounts[d].get("parent_account")]
|
||||
error_messages = []
|
||||
|
||||
for account in roots:
|
||||
if not account.get("root_type") and account.get("account_name"):
|
||||
error_messages.append(_("Please enter Root Type for account- {0}").format(account.get("account_name")))
|
||||
error_messages.append(
|
||||
_("Please enter Root Type for account- {0}").format(account.get("account_name"))
|
||||
)
|
||||
elif account.get("root_type") not in get_root_types() and account.get("account_name"):
|
||||
error_messages.append(_("Root Type for {0} must be one of the Asset, Liability, Income, Expense and Equity").format(account.get("account_name")))
|
||||
error_messages.append(
|
||||
_("Root Type for {0} must be one of the Asset, Liability, Income, Expense and Equity").format(
|
||||
account.get("account_name")
|
||||
)
|
||||
)
|
||||
|
||||
validate_missing_roots(roots)
|
||||
|
||||
if error_messages:
|
||||
frappe.throw("<br>".join(error_messages))
|
||||
|
||||
|
||||
def validate_missing_roots(roots):
|
||||
root_types_added = set(d.get('root_type') for d in roots)
|
||||
root_types_added = set(d.get("root_type") for d in roots)
|
||||
|
||||
missing = list(set(get_root_types()) - root_types_added)
|
||||
|
||||
if missing:
|
||||
frappe.throw(_("Please add Root Account for - {0}").format(' , '.join(missing)))
|
||||
frappe.throw(_("Please add Root Account for - {0}").format(" , ".join(missing)))
|
||||
|
||||
|
||||
def get_root_types():
|
||||
return ('Asset', 'Liability', 'Expense', 'Income', 'Equity')
|
||||
return ("Asset", "Liability", "Expense", "Income", "Equity")
|
||||
|
||||
|
||||
def get_report_type(root_type):
|
||||
if root_type in ('Asset', 'Liability', 'Equity'):
|
||||
return 'Balance Sheet'
|
||||
if root_type in ("Asset", "Liability", "Equity"):
|
||||
return "Balance Sheet"
|
||||
else:
|
||||
return 'Profit and Loss'
|
||||
return "Profit and Loss"
|
||||
|
||||
|
||||
def get_mandatory_group_accounts():
|
||||
return ('Bank', 'Cash', 'Stock')
|
||||
return ("Bank", "Cash", "Stock")
|
||||
|
||||
|
||||
def get_mandatory_account_types():
|
||||
return [
|
||||
{'account_type': 'Cost of Goods Sold', 'root_type': 'Expense'},
|
||||
{'account_type': 'Depreciation', 'root_type': 'Expense'},
|
||||
{'account_type': 'Fixed Asset', 'root_type': 'Asset'},
|
||||
{'account_type': 'Payable', 'root_type': 'Liability'},
|
||||
{'account_type': 'Receivable', 'root_type': 'Asset'},
|
||||
{'account_type': 'Stock Adjustment', 'root_type': 'Expense'},
|
||||
{'account_type': 'Bank', 'root_type': 'Asset'},
|
||||
{'account_type': 'Cash', 'root_type': 'Asset'},
|
||||
{'account_type': 'Stock', 'root_type': 'Asset'}
|
||||
{"account_type": "Cost of Goods Sold", "root_type": "Expense"},
|
||||
{"account_type": "Depreciation", "root_type": "Expense"},
|
||||
{"account_type": "Fixed Asset", "root_type": "Asset"},
|
||||
{"account_type": "Payable", "root_type": "Liability"},
|
||||
{"account_type": "Receivable", "root_type": "Asset"},
|
||||
{"account_type": "Stock Adjustment", "root_type": "Expense"},
|
||||
{"account_type": "Bank", "root_type": "Asset"},
|
||||
{"account_type": "Cash", "root_type": "Asset"},
|
||||
{"account_type": "Stock", "root_type": "Asset"},
|
||||
]
|
||||
|
||||
|
||||
def unset_existing_data(company):
|
||||
linked = frappe.db.sql('''select fieldname from tabDocField
|
||||
where fieldtype="Link" and options="Account" and parent="Company"''', as_dict=True)
|
||||
linked = frappe.db.sql(
|
||||
'''select fieldname from tabDocField
|
||||
where fieldtype="Link" and options="Account" and parent="Company"''',
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
# remove accounts data from company
|
||||
update_values = {d.fieldname: '' for d in linked}
|
||||
frappe.db.set_value('Company', company, update_values, update_values)
|
||||
update_values = {d.fieldname: "" for d in linked}
|
||||
frappe.db.set_value("Company", company, update_values, update_values)
|
||||
|
||||
# remove accounts data from various doctypes
|
||||
for doctype in ["Account", "Party Account", "Mode of Payment Account", "Tax Withholding Account",
|
||||
"Sales Taxes and Charges Template", "Purchase Taxes and Charges Template"]:
|
||||
frappe.db.sql('''delete from `tab{0}` where `company`="%s"''' # nosec
|
||||
.format(doctype) % (company))
|
||||
for doctype in [
|
||||
"Account",
|
||||
"Party Account",
|
||||
"Mode of Payment Account",
|
||||
"Tax Withholding Account",
|
||||
"Sales Taxes and Charges Template",
|
||||
"Purchase Taxes and Charges Template",
|
||||
]:
|
||||
frappe.db.sql(
|
||||
'''delete from `tab{0}` where `company`="%s"'''.format(doctype) % (company) # nosec
|
||||
)
|
||||
|
||||
|
||||
def set_default_accounts(company):
|
||||
from erpnext.setup.doctype.company.company import install_country_fixtures
|
||||
company = frappe.get_doc('Company', company)
|
||||
company.update({
|
||||
"default_receivable_account": frappe.db.get_value("Account",
|
||||
{"company": company.name, "account_type": "Receivable", "is_group": 0}),
|
||||
"default_payable_account": frappe.db.get_value("Account",
|
||||
{"company": company.name, "account_type": "Payable", "is_group": 0})
|
||||
})
|
||||
|
||||
company = frappe.get_doc("Company", company)
|
||||
company.update(
|
||||
{
|
||||
"default_receivable_account": frappe.db.get_value(
|
||||
"Account", {"company": company.name, "account_type": "Receivable", "is_group": 0}
|
||||
),
|
||||
"default_payable_account": frappe.db.get_value(
|
||||
"Account", {"company": company.name, "account_type": "Payable", "is_group": 0}
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
company.save()
|
||||
install_country_fixtures(company.name, company.country)
|
||||
|
||||
@ -10,17 +10,20 @@ from frappe.model.document import Document
|
||||
class ChequePrintTemplate(Document):
|
||||
pass
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_or_update_cheque_print_format(template_name):
|
||||
if not frappe.db.exists("Print Format", template_name):
|
||||
cheque_print = frappe.new_doc("Print Format")
|
||||
cheque_print.update({
|
||||
"doc_type": "Payment Entry",
|
||||
"standard": "No",
|
||||
"custom_format": 1,
|
||||
"print_format_type": "Jinja",
|
||||
"name": template_name
|
||||
})
|
||||
cheque_print.update(
|
||||
{
|
||||
"doc_type": "Payment Entry",
|
||||
"standard": "No",
|
||||
"custom_format": 1,
|
||||
"print_format_type": "Jinja",
|
||||
"name": template_name,
|
||||
}
|
||||
)
|
||||
else:
|
||||
cheque_print = frappe.get_doc("Print Format", template_name)
|
||||
|
||||
@ -69,10 +72,12 @@ def create_or_update_cheque_print_format(template_name):
|
||||
{{doc.company}}
|
||||
</span>
|
||||
</div>
|
||||
</div>"""%{
|
||||
"starting_position_from_top_edge": doc.starting_position_from_top_edge \
|
||||
if doc.cheque_size == "A4" else 0.0,
|
||||
"cheque_width": doc.cheque_width, "cheque_height": doc.cheque_height,
|
||||
</div>""" % {
|
||||
"starting_position_from_top_edge": doc.starting_position_from_top_edge
|
||||
if doc.cheque_size == "A4"
|
||||
else 0.0,
|
||||
"cheque_width": doc.cheque_width,
|
||||
"cheque_height": doc.cheque_height,
|
||||
"acc_pay_dist_from_top_edge": doc.acc_pay_dist_from_top_edge,
|
||||
"acc_pay_dist_from_left_edge": doc.acc_pay_dist_from_left_edge,
|
||||
"message_to_show": doc.message_to_show if doc.message_to_show else _("Account Pay Only"),
|
||||
@ -89,7 +94,7 @@ def create_or_update_cheque_print_format(template_name):
|
||||
"amt_in_figures_from_top_edge": doc.amt_in_figures_from_top_edge,
|
||||
"amt_in_figures_from_left_edge": doc.amt_in_figures_from_left_edge,
|
||||
"signatory_from_top_edge": doc.signatory_from_top_edge,
|
||||
"signatory_from_left_edge": doc.signatory_from_left_edge
|
||||
"signatory_from_left_edge": doc.signatory_from_left_edge,
|
||||
}
|
||||
|
||||
cheque_print.save(ignore_permissions=True)
|
||||
|
||||
@ -5,5 +5,6 @@ import unittest
|
||||
|
||||
# test_records = frappe.get_test_records('Cheque Print Template')
|
||||
|
||||
|
||||
class TestChequePrintTemplate(unittest.TestCase):
|
||||
pass
|
||||
|
||||
@ -10,11 +10,14 @@ from erpnext.accounts.utils import validate_field_number
|
||||
|
||||
|
||||
class CostCenter(NestedSet):
|
||||
nsm_parent_field = 'parent_cost_center'
|
||||
nsm_parent_field = "parent_cost_center"
|
||||
|
||||
def autoname(self):
|
||||
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.name = get_autoname_with_number(
|
||||
self.cost_center_number, self.cost_center_name, None, self.company
|
||||
)
|
||||
|
||||
def validate(self):
|
||||
self.validate_mandatory()
|
||||
@ -28,9 +31,12 @@ class CostCenter(NestedSet):
|
||||
|
||||
def validate_parent_cost_center(self):
|
||||
if self.parent_cost_center:
|
||||
if not frappe.db.get_value('Cost Center', self.parent_cost_center, 'is_group'):
|
||||
frappe.throw(_("{0} is not a group node. Please select a group node as parent cost center").format(
|
||||
frappe.bold(self.parent_cost_center)))
|
||||
if not frappe.db.get_value("Cost Center", self.parent_cost_center, "is_group"):
|
||||
frappe.throw(
|
||||
_("{0} is not a group node. Please select a group node as parent cost center").format(
|
||||
frappe.bold(self.parent_cost_center)
|
||||
)
|
||||
)
|
||||
|
||||
@frappe.whitelist()
|
||||
def convert_group_to_ledger(self):
|
||||
@ -48,7 +54,9 @@ class CostCenter(NestedSet):
|
||||
if self.if_allocation_exists_against_cost_center():
|
||||
frappe.throw(_("Cost Center with Allocation records can not be converted to a group"))
|
||||
if self.check_if_part_of_cost_center_allocation():
|
||||
frappe.throw(_("Cost Center is a part of Cost Center Allocation, hence cannot be converted to a group"))
|
||||
frappe.throw(
|
||||
_("Cost Center is a part of Cost Center Allocation, hence cannot be converted to a group")
|
||||
)
|
||||
if self.check_gle_exists():
|
||||
frappe.throw(_("Cost Center with existing transactions can not be converted to group"))
|
||||
self.is_group = 1
|
||||
@ -59,24 +67,26 @@ class CostCenter(NestedSet):
|
||||
return frappe.db.get_value("GL Entry", {"cost_center": self.name})
|
||||
|
||||
def check_if_child_exists(self):
|
||||
return frappe.db.sql("select name from `tabCost Center` where \
|
||||
parent_cost_center = %s and docstatus != 2", self.name)
|
||||
return frappe.db.sql(
|
||||
"select name from `tabCost Center` where \
|
||||
parent_cost_center = %s and docstatus != 2",
|
||||
self.name,
|
||||
)
|
||||
|
||||
def if_allocation_exists_against_cost_center(self):
|
||||
return frappe.db.get_value("Cost Center Allocation", filters = {
|
||||
"main_cost_center": self.name,
|
||||
"docstatus": 1
|
||||
})
|
||||
return frappe.db.get_value(
|
||||
"Cost Center Allocation", filters={"main_cost_center": self.name, "docstatus": 1}
|
||||
)
|
||||
|
||||
def check_if_part_of_cost_center_allocation(self):
|
||||
return frappe.db.get_value("Cost Center Allocation Percentage", filters = {
|
||||
"cost_center": self.name,
|
||||
"docstatus": 1
|
||||
})
|
||||
return frappe.db.get_value(
|
||||
"Cost Center Allocation Percentage", filters={"cost_center": self.name, "docstatus": 1}
|
||||
)
|
||||
|
||||
def before_rename(self, olddn, newdn, merge=False):
|
||||
# Add company abbr if not provided
|
||||
from erpnext.setup.doctype.company.company import get_name_with_abbr
|
||||
|
||||
new_cost_center = get_name_with_abbr(newdn, self.company)
|
||||
|
||||
# Validate properties before merging
|
||||
@ -90,7 +100,9 @@ class CostCenter(NestedSet):
|
||||
super(CostCenter, self).after_rename(olddn, newdn, merge)
|
||||
|
||||
if not merge:
|
||||
new_cost_center = frappe.db.get_value("Cost Center", newdn, ["cost_center_name", "cost_center_number"], as_dict=1)
|
||||
new_cost_center = frappe.db.get_value(
|
||||
"Cost Center", newdn, ["cost_center_name", "cost_center_number"], as_dict=1
|
||||
)
|
||||
|
||||
# exclude company abbr
|
||||
new_parts = newdn.split(" - ")[:-1]
|
||||
@ -99,7 +111,9 @@ class CostCenter(NestedSet):
|
||||
if len(new_parts) == 1:
|
||||
new_parts = newdn.split(" ")
|
||||
if new_cost_center.cost_center_number != new_parts[0]:
|
||||
validate_field_number("Cost Center", self.name, new_parts[0], self.company, "cost_center_number")
|
||||
validate_field_number(
|
||||
"Cost Center", self.name, new_parts[0], self.company, "cost_center_number"
|
||||
)
|
||||
self.cost_center_number = new_parts[0]
|
||||
self.db_set("cost_center_number", new_parts[0])
|
||||
new_parts = new_parts[1:]
|
||||
@ -110,10 +124,12 @@ class CostCenter(NestedSet):
|
||||
self.cost_center_name = cost_center_name
|
||||
self.db_set("cost_center_name", cost_center_name)
|
||||
|
||||
|
||||
def on_doctype_update():
|
||||
frappe.db.add_index("Cost Center", ["lft", "rgt"])
|
||||
|
||||
|
||||
def get_name_with_number(new_account, account_number):
|
||||
if account_number and not new_account[0].isdigit():
|
||||
new_account = account_number + " - " + new_account
|
||||
return new_account
|
||||
return new_account
|
||||
|
||||
@ -3,11 +3,6 @@ from frappe import _
|
||||
|
||||
def get_data():
|
||||
return {
|
||||
'fieldname': 'cost_center',
|
||||
'reports': [
|
||||
{
|
||||
'label': _('Reports'),
|
||||
'items': ['Budget Variance Report', 'General Ledger']
|
||||
}
|
||||
]
|
||||
"fieldname": "cost_center",
|
||||
"reports": [{"label": _("Reports"), "items": ["Budget Variance Report", "General Ledger"]}],
|
||||
}
|
||||
|
||||
@ -5,24 +5,28 @@ import unittest
|
||||
|
||||
import frappe
|
||||
|
||||
test_records = frappe.get_test_records('Cost Center')
|
||||
test_records = frappe.get_test_records("Cost Center")
|
||||
|
||||
|
||||
class TestCostCenter(unittest.TestCase):
|
||||
def test_cost_center_creation_against_child_node(self):
|
||||
|
||||
if not frappe.db.get_value('Cost Center', {'name': '_Test Cost Center 2 - _TC'}):
|
||||
if not frappe.db.get_value("Cost Center", {"name": "_Test Cost Center 2 - _TC"}):
|
||||
frappe.get_doc(test_records[1]).insert()
|
||||
|
||||
cost_center = frappe.get_doc({
|
||||
'doctype': 'Cost Center',
|
||||
'cost_center_name': '_Test Cost Center 3',
|
||||
'parent_cost_center': '_Test Cost Center 2 - _TC',
|
||||
'is_group': 0,
|
||||
'company': '_Test Company'
|
||||
})
|
||||
cost_center = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Cost Center",
|
||||
"cost_center_name": "_Test Cost Center 3",
|
||||
"parent_cost_center": "_Test Cost Center 2 - _TC",
|
||||
"is_group": 0,
|
||||
"company": "_Test Company",
|
||||
}
|
||||
)
|
||||
|
||||
self.assertRaises(frappe.ValidationError, cost_center.save)
|
||||
|
||||
|
||||
def create_cost_center(**args):
|
||||
args = frappe._dict(args)
|
||||
if args.cost_center_name:
|
||||
|
||||
@ -9,15 +9,24 @@ from frappe.utils import add_days, format_date, getdate
|
||||
|
||||
class MainCostCenterCantBeChild(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidMainCostCenter(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidChildCostCenter(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class WrongPercentageAllocation(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidDateError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class CostCenterAllocation(Document):
|
||||
def validate(self):
|
||||
self.validate_total_allocation_percentage()
|
||||
@ -30,61 +39,96 @@ class CostCenterAllocation(Document):
|
||||
total_percentage = sum([d.percentage for d in self.get("allocation_percentages", [])])
|
||||
|
||||
if total_percentage != 100:
|
||||
frappe.throw(_("Total percentage against cost centers should be 100"), WrongPercentageAllocation)
|
||||
frappe.throw(
|
||||
_("Total percentage against cost centers should be 100"), WrongPercentageAllocation
|
||||
)
|
||||
|
||||
def validate_from_date_based_on_existing_gle(self):
|
||||
# Check if GLE exists against the main cost center
|
||||
# If exists ensure from date is set after posting date of last GLE
|
||||
|
||||
last_gle_date = frappe.db.get_value("GL Entry",
|
||||
last_gle_date = frappe.db.get_value(
|
||||
"GL Entry",
|
||||
{"cost_center": self.main_cost_center, "is_cancelled": 0},
|
||||
"posting_date", order_by="posting_date desc")
|
||||
"posting_date",
|
||||
order_by="posting_date desc",
|
||||
)
|
||||
|
||||
if last_gle_date:
|
||||
if getdate(self.valid_from) <= getdate(last_gle_date):
|
||||
frappe.throw(_("Valid From must be after {0} as last GL Entry against the cost center {1} posted on this date")
|
||||
.format(last_gle_date, self.main_cost_center), InvalidDateError)
|
||||
frappe.throw(
|
||||
_(
|
||||
"Valid From must be after {0} as last GL Entry against the cost center {1} posted on this date"
|
||||
).format(last_gle_date, self.main_cost_center),
|
||||
InvalidDateError,
|
||||
)
|
||||
|
||||
def validate_backdated_allocation(self):
|
||||
# Check if there are any future existing allocation records against the main cost center
|
||||
# If exists, warn the user about it
|
||||
|
||||
future_allocation = frappe.db.get_value("Cost Center Allocation", filters = {
|
||||
"main_cost_center": self.main_cost_center,
|
||||
"valid_from": (">=", self.valid_from),
|
||||
"name": ("!=", self.name),
|
||||
"docstatus": 1
|
||||
}, fieldname=['valid_from', 'name'], order_by='valid_from', as_dict=1)
|
||||
future_allocation = frappe.db.get_value(
|
||||
"Cost Center Allocation",
|
||||
filters={
|
||||
"main_cost_center": self.main_cost_center,
|
||||
"valid_from": (">=", self.valid_from),
|
||||
"name": ("!=", self.name),
|
||||
"docstatus": 1,
|
||||
},
|
||||
fieldname=["valid_from", "name"],
|
||||
order_by="valid_from",
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
if future_allocation:
|
||||
frappe.msgprint(_("Another Cost Center Allocation record {0} applicable from {1}, hence this allocation will be applicable upto {2}")
|
||||
.format(frappe.bold(future_allocation.name), frappe.bold(format_date(future_allocation.valid_from)),
|
||||
frappe.bold(format_date(add_days(future_allocation.valid_from, -1)))),
|
||||
title=_("Warning!"), indicator="orange", alert=1
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"Another Cost Center Allocation record {0} applicable from {1}, hence this allocation will be applicable upto {2}"
|
||||
).format(
|
||||
frappe.bold(future_allocation.name),
|
||||
frappe.bold(format_date(future_allocation.valid_from)),
|
||||
frappe.bold(format_date(add_days(future_allocation.valid_from, -1))),
|
||||
),
|
||||
title=_("Warning!"),
|
||||
indicator="orange",
|
||||
alert=1,
|
||||
)
|
||||
|
||||
def validate_main_cost_center(self):
|
||||
# Main cost center itself cannot be entered in child table
|
||||
if self.main_cost_center in [d.cost_center for d in self.allocation_percentages]:
|
||||
frappe.throw(_("Main Cost Center {0} cannot be entered in the child table")
|
||||
.format(self.main_cost_center), MainCostCenterCantBeChild)
|
||||
frappe.throw(
|
||||
_("Main Cost Center {0} cannot be entered in the child table").format(self.main_cost_center),
|
||||
MainCostCenterCantBeChild,
|
||||
)
|
||||
|
||||
# If main cost center is used for allocation under any other cost center,
|
||||
# allocation cannot be done against it
|
||||
parent = frappe.db.get_value("Cost Center Allocation Percentage", filters = {
|
||||
"cost_center": self.main_cost_center,
|
||||
"docstatus": 1
|
||||
}, fieldname='parent')
|
||||
parent = frappe.db.get_value(
|
||||
"Cost Center Allocation Percentage",
|
||||
filters={"cost_center": self.main_cost_center, "docstatus": 1},
|
||||
fieldname="parent",
|
||||
)
|
||||
if parent:
|
||||
frappe.throw(_("{0} cannot be used as a Main Cost Center because it has been used as child in Cost Center Allocation {1}")
|
||||
.format(self.main_cost_center, parent), InvalidMainCostCenter)
|
||||
frappe.throw(
|
||||
_(
|
||||
"{0} cannot be used as a Main Cost Center because it has been used as child in Cost Center Allocation {1}"
|
||||
).format(self.main_cost_center, parent),
|
||||
InvalidMainCostCenter,
|
||||
)
|
||||
|
||||
def validate_child_cost_centers(self):
|
||||
# Check if child cost center is used as main cost center in any existing allocation
|
||||
main_cost_centers = [d.main_cost_center for d in
|
||||
frappe.get_all("Cost Center Allocation", {'docstatus': 1}, 'main_cost_center')]
|
||||
main_cost_centers = [
|
||||
d.main_cost_center
|
||||
for d in frappe.get_all("Cost Center Allocation", {"docstatus": 1}, "main_cost_center")
|
||||
]
|
||||
|
||||
for d in self.allocation_percentages:
|
||||
if d.cost_center in main_cost_centers:
|
||||
frappe.throw(_("Cost Center {0} cannot be used for allocation as it is used as main cost center in other allocation record.")
|
||||
.format(d.cost_center), InvalidChildCostCenter)
|
||||
frappe.throw(
|
||||
_(
|
||||
"Cost Center {0} cannot be used for allocation as it is used as main cost center in other allocation record."
|
||||
).format(d.cost_center),
|
||||
InvalidChildCostCenter,
|
||||
)
|
||||
|
||||
@ -19,33 +19,35 @@ from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journ
|
||||
|
||||
class TestCostCenterAllocation(unittest.TestCase):
|
||||
def setUp(self):
|
||||
cost_centers = ["Main Cost Center 1", "Main Cost Center 2", "Sub Cost Center 1", "Sub Cost Center 2"]
|
||||
cost_centers = [
|
||||
"Main Cost Center 1",
|
||||
"Main Cost Center 2",
|
||||
"Sub Cost Center 1",
|
||||
"Sub Cost Center 2",
|
||||
]
|
||||
for cc in cost_centers:
|
||||
create_cost_center(cost_center_name=cc, company="_Test Company")
|
||||
|
||||
def test_gle_based_on_cost_center_allocation(self):
|
||||
cca = create_cost_center_allocation("_Test Company", "Main Cost Center 1 - _TC",
|
||||
{
|
||||
"Sub Cost Center 1 - _TC": 60,
|
||||
"Sub Cost Center 2 - _TC": 40
|
||||
}
|
||||
cca = create_cost_center_allocation(
|
||||
"_Test Company",
|
||||
"Main Cost Center 1 - _TC",
|
||||
{"Sub Cost Center 1 - _TC": 60, "Sub Cost Center 2 - _TC": 40},
|
||||
)
|
||||
|
||||
jv = make_journal_entry("_Test Cash - _TC", "Sales - _TC", 100,
|
||||
cost_center = "Main Cost Center 1 - _TC", submit=True)
|
||||
jv = make_journal_entry(
|
||||
"_Test Cash - _TC", "Sales - _TC", 100, cost_center="Main Cost Center 1 - _TC", submit=True
|
||||
)
|
||||
|
||||
expected_values = [
|
||||
["Sub Cost Center 1 - _TC", 0.0, 60],
|
||||
["Sub Cost Center 2 - _TC", 0.0, 40]
|
||||
]
|
||||
expected_values = [["Sub Cost Center 1 - _TC", 0.0, 60], ["Sub Cost Center 2 - _TC", 0.0, 40]]
|
||||
|
||||
gle = frappe.qb.DocType("GL Entry")
|
||||
gl_entries = (
|
||||
frappe.qb.from_(gle)
|
||||
.select(gle.cost_center, gle.debit, gle.credit)
|
||||
.where(gle.voucher_type == 'Journal Entry')
|
||||
.where(gle.voucher_type == "Journal Entry")
|
||||
.where(gle.voucher_no == jv.name)
|
||||
.where(gle.account == 'Sales - _TC')
|
||||
.where(gle.account == "Sales - _TC")
|
||||
.orderby(gle.cost_center)
|
||||
).run(as_dict=1)
|
||||
|
||||
@ -61,11 +63,11 @@ class TestCostCenterAllocation(unittest.TestCase):
|
||||
|
||||
def test_main_cost_center_cant_be_child(self):
|
||||
# Main cost center itself cannot be entered in child table
|
||||
cca = create_cost_center_allocation("_Test Company", "Main Cost Center 1 - _TC",
|
||||
{
|
||||
"Sub Cost Center 1 - _TC": 60,
|
||||
"Main Cost Center 1 - _TC": 40
|
||||
}, save=False
|
||||
cca = create_cost_center_allocation(
|
||||
"_Test Company",
|
||||
"Main Cost Center 1 - _TC",
|
||||
{"Sub Cost Center 1 - _TC": 60, "Main Cost Center 1 - _TC": 40},
|
||||
save=False,
|
||||
)
|
||||
|
||||
self.assertRaises(MainCostCenterCantBeChild, cca.save)
|
||||
@ -73,17 +75,14 @@ class TestCostCenterAllocation(unittest.TestCase):
|
||||
def test_invalid_main_cost_center(self):
|
||||
# If main cost center is used for allocation under any other cost center,
|
||||
# allocation cannot be done against it
|
||||
cca1 = create_cost_center_allocation("_Test Company", "Main Cost Center 1 - _TC",
|
||||
{
|
||||
"Sub Cost Center 1 - _TC": 60,
|
||||
"Sub Cost Center 2 - _TC": 40
|
||||
}
|
||||
cca1 = create_cost_center_allocation(
|
||||
"_Test Company",
|
||||
"Main Cost Center 1 - _TC",
|
||||
{"Sub Cost Center 1 - _TC": 60, "Sub Cost Center 2 - _TC": 40},
|
||||
)
|
||||
|
||||
cca2 = create_cost_center_allocation("_Test Company", "Sub Cost Center 1 - _TC",
|
||||
{
|
||||
"Sub Cost Center 2 - _TC": 100
|
||||
}, save=False
|
||||
cca2 = create_cost_center_allocation(
|
||||
"_Test Company", "Sub Cost Center 1 - _TC", {"Sub Cost Center 2 - _TC": 100}, save=False
|
||||
)
|
||||
|
||||
self.assertRaises(InvalidMainCostCenter, cca2.save)
|
||||
@ -92,18 +91,17 @@ class TestCostCenterAllocation(unittest.TestCase):
|
||||
|
||||
def test_if_child_cost_center_has_any_allocation_record(self):
|
||||
# Check if any child cost center is used as main cost center in any other existing allocation
|
||||
cca1 = create_cost_center_allocation("_Test Company", "Main Cost Center 1 - _TC",
|
||||
{
|
||||
"Sub Cost Center 1 - _TC": 60,
|
||||
"Sub Cost Center 2 - _TC": 40
|
||||
}
|
||||
cca1 = create_cost_center_allocation(
|
||||
"_Test Company",
|
||||
"Main Cost Center 1 - _TC",
|
||||
{"Sub Cost Center 1 - _TC": 60, "Sub Cost Center 2 - _TC": 40},
|
||||
)
|
||||
|
||||
cca2 = create_cost_center_allocation("_Test Company", "Main Cost Center 2 - _TC",
|
||||
{
|
||||
"Main Cost Center 1 - _TC": 60,
|
||||
"Sub Cost Center 1 - _TC": 40
|
||||
}, save=False
|
||||
cca2 = create_cost_center_allocation(
|
||||
"_Test Company",
|
||||
"Main Cost Center 2 - _TC",
|
||||
{"Main Cost Center 1 - _TC": 60, "Sub Cost Center 1 - _TC": 40},
|
||||
save=False,
|
||||
)
|
||||
|
||||
self.assertRaises(InvalidChildCostCenter, cca2.save)
|
||||
@ -111,46 +109,58 @@ class TestCostCenterAllocation(unittest.TestCase):
|
||||
cca1.cancel()
|
||||
|
||||
def test_total_percentage(self):
|
||||
cca = create_cost_center_allocation("_Test Company", "Main Cost Center 1 - _TC",
|
||||
{
|
||||
"Sub Cost Center 1 - _TC": 40,
|
||||
"Sub Cost Center 2 - _TC": 40
|
||||
}, save=False
|
||||
cca = create_cost_center_allocation(
|
||||
"_Test Company",
|
||||
"Main Cost Center 1 - _TC",
|
||||
{"Sub Cost Center 1 - _TC": 40, "Sub Cost Center 2 - _TC": 40},
|
||||
save=False,
|
||||
)
|
||||
self.assertRaises(WrongPercentageAllocation, cca.save)
|
||||
|
||||
def test_valid_from_based_on_existing_gle(self):
|
||||
# GLE posted against Sub Cost Center 1 on today
|
||||
jv = make_journal_entry("_Test Cash - _TC", "Sales - _TC", 100,
|
||||
cost_center = "Main Cost Center 1 - _TC", posting_date=today(), submit=True)
|
||||
jv = make_journal_entry(
|
||||
"_Test Cash - _TC",
|
||||
"Sales - _TC",
|
||||
100,
|
||||
cost_center="Main Cost Center 1 - _TC",
|
||||
posting_date=today(),
|
||||
submit=True,
|
||||
)
|
||||
|
||||
# try to set valid from as yesterday
|
||||
cca = create_cost_center_allocation("_Test Company", "Main Cost Center 1 - _TC",
|
||||
{
|
||||
"Sub Cost Center 1 - _TC": 60,
|
||||
"Sub Cost Center 2 - _TC": 40
|
||||
}, valid_from=add_days(today(), -1), save=False
|
||||
cca = create_cost_center_allocation(
|
||||
"_Test Company",
|
||||
"Main Cost Center 1 - _TC",
|
||||
{"Sub Cost Center 1 - _TC": 60, "Sub Cost Center 2 - _TC": 40},
|
||||
valid_from=add_days(today(), -1),
|
||||
save=False,
|
||||
)
|
||||
|
||||
self.assertRaises(InvalidDateError, cca.save)
|
||||
|
||||
jv.cancel()
|
||||
|
||||
def create_cost_center_allocation(company, main_cost_center, allocation_percentages,
|
||||
valid_from=None, valid_upto=None, save=True, submit=True):
|
||||
|
||||
def create_cost_center_allocation(
|
||||
company,
|
||||
main_cost_center,
|
||||
allocation_percentages,
|
||||
valid_from=None,
|
||||
valid_upto=None,
|
||||
save=True,
|
||||
submit=True,
|
||||
):
|
||||
doc = frappe.new_doc("Cost Center Allocation")
|
||||
doc.main_cost_center = main_cost_center
|
||||
doc.company = company
|
||||
doc.valid_from = valid_from or today()
|
||||
doc.valid_upto = valid_upto
|
||||
for cc, percentage in allocation_percentages.items():
|
||||
doc.append("allocation_percentages", {
|
||||
"cost_center": cc,
|
||||
"percentage": percentage
|
||||
})
|
||||
doc.append("allocation_percentages", {"cost_center": cc, "percentage": percentage})
|
||||
if save:
|
||||
doc.save()
|
||||
if submit:
|
||||
doc.submit()
|
||||
|
||||
return doc
|
||||
return doc
|
||||
|
||||
@ -15,7 +15,7 @@ class CouponCode(Document):
|
||||
|
||||
if not self.coupon_code:
|
||||
if self.coupon_type == "Promotional":
|
||||
self.coupon_code =''.join(i for i in self.coupon_name if not i.isdigit())[0:8].upper()
|
||||
self.coupon_code = "".join(i for i in self.coupon_name if not i.isdigit())[0:8].upper()
|
||||
elif self.coupon_type == "Gift Card":
|
||||
self.coupon_code = frappe.generate_hash()[:10].upper()
|
||||
|
||||
|
||||
@ -7,91 +7,109 @@ import frappe
|
||||
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
|
||||
test_dependencies = ['Item']
|
||||
test_dependencies = ["Item"]
|
||||
|
||||
|
||||
def test_create_test_data():
|
||||
frappe.set_user("Administrator")
|
||||
# create test item
|
||||
if not frappe.db.exists("Item","_Test Tesla Car"):
|
||||
item = frappe.get_doc({
|
||||
"description": "_Test Tesla Car",
|
||||
"doctype": "Item",
|
||||
"has_batch_no": 0,
|
||||
"has_serial_no": 0,
|
||||
"inspection_required": 0,
|
||||
"is_stock_item": 1,
|
||||
"opening_stock":100,
|
||||
"is_sub_contracted_item": 0,
|
||||
"item_code": "_Test Tesla Car",
|
||||
"item_group": "_Test Item Group",
|
||||
"item_name": "_Test Tesla Car",
|
||||
"apply_warehouse_wise_reorder_level": 0,
|
||||
"warehouse":"Stores - _TC",
|
||||
"valuation_rate": 5000,
|
||||
"standard_rate":5000,
|
||||
"item_defaults": [{
|
||||
"company": "_Test Company",
|
||||
"default_warehouse": "Stores - _TC",
|
||||
"default_price_list":"_Test Price List",
|
||||
"expense_account": "Cost of Goods Sold - _TC",
|
||||
"buying_cost_center": "Main - _TC",
|
||||
"selling_cost_center": "Main - _TC",
|
||||
"income_account": "Sales - _TC"
|
||||
}],
|
||||
})
|
||||
if not frappe.db.exists("Item", "_Test Tesla Car"):
|
||||
item = frappe.get_doc(
|
||||
{
|
||||
"description": "_Test Tesla Car",
|
||||
"doctype": "Item",
|
||||
"has_batch_no": 0,
|
||||
"has_serial_no": 0,
|
||||
"inspection_required": 0,
|
||||
"is_stock_item": 1,
|
||||
"opening_stock": 100,
|
||||
"is_sub_contracted_item": 0,
|
||||
"item_code": "_Test Tesla Car",
|
||||
"item_group": "_Test Item Group",
|
||||
"item_name": "_Test Tesla Car",
|
||||
"apply_warehouse_wise_reorder_level": 0,
|
||||
"warehouse": "Stores - _TC",
|
||||
"valuation_rate": 5000,
|
||||
"standard_rate": 5000,
|
||||
"item_defaults": [
|
||||
{
|
||||
"company": "_Test Company",
|
||||
"default_warehouse": "Stores - _TC",
|
||||
"default_price_list": "_Test Price List",
|
||||
"expense_account": "Cost of Goods Sold - _TC",
|
||||
"buying_cost_center": "Main - _TC",
|
||||
"selling_cost_center": "Main - _TC",
|
||||
"income_account": "Sales - _TC",
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
item.insert()
|
||||
# create test item price
|
||||
item_price = frappe.get_list('Item Price', filters={'item_code': '_Test Tesla Car', 'price_list': '_Test Price List'}, fields=['name'])
|
||||
if len(item_price)==0:
|
||||
item_price = frappe.get_doc({
|
||||
"doctype": "Item Price",
|
||||
"item_code": "_Test Tesla Car",
|
||||
"price_list": "_Test Price List",
|
||||
"price_list_rate": 5000
|
||||
})
|
||||
item_price = frappe.get_list(
|
||||
"Item Price",
|
||||
filters={"item_code": "_Test Tesla Car", "price_list": "_Test Price List"},
|
||||
fields=["name"],
|
||||
)
|
||||
if len(item_price) == 0:
|
||||
item_price = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Item Price",
|
||||
"item_code": "_Test Tesla Car",
|
||||
"price_list": "_Test Price List",
|
||||
"price_list_rate": 5000,
|
||||
}
|
||||
)
|
||||
item_price.insert()
|
||||
# create test item pricing rule
|
||||
if not frappe.db.exists("Pricing Rule", {"title": "_Test Pricing Rule for _Test Item"}):
|
||||
item_pricing_rule = frappe.get_doc({
|
||||
"doctype": "Pricing Rule",
|
||||
"title": "_Test Pricing Rule for _Test Item",
|
||||
"apply_on": "Item Code",
|
||||
"items": [{
|
||||
"item_code": "_Test Tesla Car"
|
||||
}],
|
||||
"warehouse":"Stores - _TC",
|
||||
"coupon_code_based":1,
|
||||
"selling": 1,
|
||||
"rate_or_discount": "Discount Percentage",
|
||||
"discount_percentage": 30,
|
||||
"company": "_Test Company",
|
||||
"currency":"INR",
|
||||
"for_price_list":"_Test Price List"
|
||||
})
|
||||
item_pricing_rule = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Pricing Rule",
|
||||
"title": "_Test Pricing Rule for _Test Item",
|
||||
"apply_on": "Item Code",
|
||||
"items": [{"item_code": "_Test Tesla Car"}],
|
||||
"warehouse": "Stores - _TC",
|
||||
"coupon_code_based": 1,
|
||||
"selling": 1,
|
||||
"rate_or_discount": "Discount Percentage",
|
||||
"discount_percentage": 30,
|
||||
"company": "_Test Company",
|
||||
"currency": "INR",
|
||||
"for_price_list": "_Test Price List",
|
||||
}
|
||||
)
|
||||
item_pricing_rule.insert()
|
||||
# create test item sales partner
|
||||
if not frappe.db.exists("Sales Partner","_Test Coupon Partner"):
|
||||
sales_partner = frappe.get_doc({
|
||||
"doctype": "Sales Partner",
|
||||
"partner_name":"_Test Coupon Partner",
|
||||
"commission_rate":2,
|
||||
"referral_code": "COPART"
|
||||
})
|
||||
if not frappe.db.exists("Sales Partner", "_Test Coupon Partner"):
|
||||
sales_partner = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Sales Partner",
|
||||
"partner_name": "_Test Coupon Partner",
|
||||
"commission_rate": 2,
|
||||
"referral_code": "COPART",
|
||||
}
|
||||
)
|
||||
sales_partner.insert()
|
||||
# create test item coupon code
|
||||
if not frappe.db.exists("Coupon Code", "SAVE30"):
|
||||
pricing_rule = frappe.db.get_value("Pricing Rule", {"title": "_Test Pricing Rule for _Test Item"}, ['name'])
|
||||
coupon_code = frappe.get_doc({
|
||||
"doctype": "Coupon Code",
|
||||
"coupon_name":"SAVE30",
|
||||
"coupon_code":"SAVE30",
|
||||
"pricing_rule": pricing_rule,
|
||||
"valid_from": "2014-01-01",
|
||||
"maximum_use":1,
|
||||
"used":0
|
||||
})
|
||||
pricing_rule = frappe.db.get_value(
|
||||
"Pricing Rule", {"title": "_Test Pricing Rule for _Test Item"}, ["name"]
|
||||
)
|
||||
coupon_code = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Coupon Code",
|
||||
"coupon_name": "SAVE30",
|
||||
"coupon_code": "SAVE30",
|
||||
"pricing_rule": pricing_rule,
|
||||
"valid_from": "2014-01-01",
|
||||
"maximum_use": 1,
|
||||
"used": 0,
|
||||
}
|
||||
)
|
||||
coupon_code.insert()
|
||||
|
||||
|
||||
class TestCouponCode(unittest.TestCase):
|
||||
def setUp(self):
|
||||
test_create_test_data()
|
||||
@ -102,15 +120,21 @@ class TestCouponCode(unittest.TestCase):
|
||||
def test_sales_order_with_coupon_code(self):
|
||||
frappe.db.set_value("Coupon Code", "SAVE30", "used", 0)
|
||||
|
||||
so = make_sales_order(company='_Test Company', warehouse='Stores - _TC',
|
||||
customer="_Test Customer", selling_price_list="_Test Price List",
|
||||
item_code="_Test Tesla Car", rate=5000, qty=1,
|
||||
do_not_submit=True)
|
||||
so = make_sales_order(
|
||||
company="_Test Company",
|
||||
warehouse="Stores - _TC",
|
||||
customer="_Test Customer",
|
||||
selling_price_list="_Test Price List",
|
||||
item_code="_Test Tesla Car",
|
||||
rate=5000,
|
||||
qty=1,
|
||||
do_not_submit=True,
|
||||
)
|
||||
|
||||
self.assertEqual(so.items[0].rate, 5000)
|
||||
|
||||
so.coupon_code='SAVE30'
|
||||
so.sales_partner='_Test Coupon Partner'
|
||||
so.coupon_code = "SAVE30"
|
||||
so.sales_partner = "_Test Coupon Partner"
|
||||
so.save()
|
||||
|
||||
# check item price after coupon code is applied
|
||||
|
||||
@ -11,45 +11,40 @@ from frappe.utils import nowdate
|
||||
class CurrencyExchangeSettings(Document):
|
||||
def validate(self):
|
||||
self.set_parameters_and_result()
|
||||
if frappe.flags.in_test or frappe.flags.in_install or frappe.flags.in_setup_wizard:
|
||||
return
|
||||
response, value = self.validate_parameters()
|
||||
self.validate_result(response, value)
|
||||
|
||||
def set_parameters_and_result(self):
|
||||
if self.service_provider == 'exchangerate.host':
|
||||
self.set('result_key', [])
|
||||
self.set('req_params', [])
|
||||
if self.service_provider == "exchangerate.host":
|
||||
self.set("result_key", [])
|
||||
self.set("req_params", [])
|
||||
|
||||
self.api_endpoint = "https://api.exchangerate.host/convert"
|
||||
self.append('result_key', {'key': 'result'})
|
||||
self.append('req_params', {'key': 'date', 'value': '{transaction_date}'})
|
||||
self.append('req_params', {'key': 'from', 'value': '{from_currency}'})
|
||||
self.append('req_params', {'key': 'to', 'value': '{to_currency}'})
|
||||
elif self.service_provider == 'frankfurter.app':
|
||||
self.set('result_key', [])
|
||||
self.set('req_params', [])
|
||||
self.append("result_key", {"key": "result"})
|
||||
self.append("req_params", {"key": "date", "value": "{transaction_date}"})
|
||||
self.append("req_params", {"key": "from", "value": "{from_currency}"})
|
||||
self.append("req_params", {"key": "to", "value": "{to_currency}"})
|
||||
elif self.service_provider == "frankfurter.app":
|
||||
self.set("result_key", [])
|
||||
self.set("req_params", [])
|
||||
|
||||
self.api_endpoint = "https://frankfurter.app/{transaction_date}"
|
||||
self.append('result_key', {'key': 'rates'})
|
||||
self.append('result_key', {'key': '{to_currency}'})
|
||||
self.append('req_params', {'key': 'base', 'value': '{from_currency}'})
|
||||
self.append('req_params', {'key': 'symbols', 'value': '{to_currency}'})
|
||||
self.append("result_key", {"key": "rates"})
|
||||
self.append("result_key", {"key": "{to_currency}"})
|
||||
self.append("req_params", {"key": "base", "value": "{from_currency}"})
|
||||
self.append("req_params", {"key": "symbols", "value": "{to_currency}"})
|
||||
|
||||
def validate_parameters(self):
|
||||
if frappe.flags.in_test:
|
||||
return None, None
|
||||
|
||||
params = {}
|
||||
for row in self.req_params:
|
||||
params[row.key] = row.value.format(
|
||||
transaction_date=nowdate(),
|
||||
to_currency='INR',
|
||||
from_currency='USD'
|
||||
transaction_date=nowdate(), to_currency="INR", from_currency="USD"
|
||||
)
|
||||
|
||||
api_url = self.api_endpoint.format(
|
||||
transaction_date=nowdate(),
|
||||
to_currency='INR',
|
||||
from_currency='USD'
|
||||
transaction_date=nowdate(), to_currency="INR", from_currency="USD"
|
||||
)
|
||||
|
||||
try:
|
||||
@ -63,20 +58,14 @@ class CurrencyExchangeSettings(Document):
|
||||
return response, value
|
||||
|
||||
def validate_result(self, response, value):
|
||||
if frappe.flags.in_test:
|
||||
return
|
||||
|
||||
try:
|
||||
for key in self.result_key:
|
||||
value = value[str(key.key).format(
|
||||
transaction_date=nowdate(),
|
||||
to_currency='INR',
|
||||
from_currency='USD'
|
||||
)]
|
||||
value = value[
|
||||
str(key.key).format(transaction_date=nowdate(), to_currency="INR", from_currency="USD")
|
||||
]
|
||||
except Exception:
|
||||
frappe.throw("Invalid result key. Response: " + response.text)
|
||||
frappe.throw(_("Invalid result key. Response:") + " " + response.text)
|
||||
if not isinstance(value, (int, float)):
|
||||
frappe.throw(_("Returned exchange rate is neither integer not float."))
|
||||
|
||||
self.url = response.url
|
||||
frappe.msgprint("Exchange rate of USD to INR is " + str(value))
|
||||
|
||||
@ -19,78 +19,99 @@ 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.db.get_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
|
||||
|
||||
def validate_amount(self):
|
||||
amounts = calculate_interest_and_amount(
|
||||
self.outstanding_amount, self.rate_of_interest, self.dunning_fee, self.overdue_days)
|
||||
if self.interest_amount != amounts.get('interest_amount'):
|
||||
self.interest_amount = flt(amounts.get('interest_amount'), self.precision('interest_amount'))
|
||||
if self.dunning_amount != amounts.get('dunning_amount'):
|
||||
self.dunning_amount = flt(amounts.get('dunning_amount'), self.precision('dunning_amount'))
|
||||
if self.grand_total != amounts.get('grand_total'):
|
||||
self.grand_total = flt(amounts.get('grand_total'), self.precision('grand_total'))
|
||||
self.outstanding_amount, self.rate_of_interest, self.dunning_fee, self.overdue_days
|
||||
)
|
||||
if self.interest_amount != amounts.get("interest_amount"):
|
||||
self.interest_amount = flt(amounts.get("interest_amount"), self.precision("interest_amount"))
|
||||
if self.dunning_amount != amounts.get("dunning_amount"):
|
||||
self.dunning_amount = flt(amounts.get("dunning_amount"), self.precision("dunning_amount"))
|
||||
if self.grand_total != amounts.get("grand_total"):
|
||||
self.grand_total = flt(amounts.get("grand_total"), self.precision("grand_total"))
|
||||
|
||||
def on_submit(self):
|
||||
self.make_gl_entries()
|
||||
|
||||
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")
|
||||
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
|
||||
|
||||
def make_gl_entries(self):
|
||||
if not self.dunning_amount:
|
||||
return
|
||||
gl_entries = []
|
||||
invoice_fields = ["project", "cost_center", "debit_to", "party_account_currency", "conversion_rate", "cost_center"]
|
||||
invoice_fields = [
|
||||
"project",
|
||||
"cost_center",
|
||||
"debit_to",
|
||||
"party_account_currency",
|
||||
"conversion_rate",
|
||||
"cost_center",
|
||||
]
|
||||
inv = frappe.db.get_value("Sales Invoice", self.sales_invoice, invoice_fields, as_dict=1)
|
||||
|
||||
accounting_dimensions = get_accounting_dimensions()
|
||||
invoice_fields.extend(accounting_dimensions)
|
||||
|
||||
dunning_in_company_currency = flt(self.dunning_amount * inv.conversion_rate)
|
||||
default_cost_center = frappe.get_cached_value('Company', self.company, 'cost_center')
|
||||
default_cost_center = frappe.get_cached_value("Company", self.company, "cost_center")
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict({
|
||||
"account": inv.debit_to,
|
||||
"party_type": "Customer",
|
||||
"party": self.customer,
|
||||
"due_date": self.due_date,
|
||||
"against": self.income_account,
|
||||
"debit": dunning_in_company_currency,
|
||||
"debit_in_account_currency": self.dunning_amount,
|
||||
"against_voucher": self.name,
|
||||
"against_voucher_type": "Dunning",
|
||||
"cost_center": inv.cost_center or default_cost_center,
|
||||
"project": inv.project
|
||||
}, inv.party_account_currency, item=inv)
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": inv.debit_to,
|
||||
"party_type": "Customer",
|
||||
"party": self.customer,
|
||||
"due_date": self.due_date,
|
||||
"against": self.income_account,
|
||||
"debit": dunning_in_company_currency,
|
||||
"debit_in_account_currency": self.dunning_amount,
|
||||
"against_voucher": self.name,
|
||||
"against_voucher_type": "Dunning",
|
||||
"cost_center": inv.cost_center or default_cost_center,
|
||||
"project": inv.project,
|
||||
},
|
||||
inv.party_account_currency,
|
||||
item=inv,
|
||||
)
|
||||
)
|
||||
gl_entries.append(
|
||||
self.get_gl_dict({
|
||||
"account": self.income_account,
|
||||
"against": self.customer,
|
||||
"credit": dunning_in_company_currency,
|
||||
"cost_center": inv.cost_center or default_cost_center,
|
||||
"credit_in_account_currency": self.dunning_amount,
|
||||
"project": inv.project
|
||||
}, item=inv)
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": self.income_account,
|
||||
"against": self.customer,
|
||||
"credit": dunning_in_company_currency,
|
||||
"cost_center": inv.cost_center or default_cost_center,
|
||||
"credit_in_account_currency": self.dunning_amount,
|
||||
"project": inv.project,
|
||||
},
|
||||
item=inv,
|
||||
)
|
||||
)
|
||||
make_gl_entries(
|
||||
gl_entries, cancel=(self.docstatus == 2), update_outstanding="No", merge_entries=False
|
||||
)
|
||||
make_gl_entries(gl_entries, cancel=(self.docstatus == 2), update_outstanding="No", merge_entries=False)
|
||||
|
||||
|
||||
def resolve_dunning(doc, state):
|
||||
for reference in doc.references:
|
||||
if reference.reference_doctype == 'Sales Invoice' and reference.outstanding_amount <= 0:
|
||||
dunnings = frappe.get_list('Dunning', filters={
|
||||
'sales_invoice': reference.reference_name, 'status': ('!=', 'Resolved')}, ignore_permissions=True)
|
||||
if reference.reference_doctype == "Sales Invoice" and reference.outstanding_amount <= 0:
|
||||
dunnings = frappe.get_list(
|
||||
"Dunning",
|
||||
filters={"sales_invoice": reference.reference_name, "status": ("!=", "Resolved")},
|
||||
ignore_permissions=True,
|
||||
)
|
||||
|
||||
for dunning in dunnings:
|
||||
frappe.db.set_value("Dunning", dunning.name, "status", 'Resolved')
|
||||
frappe.db.set_value("Dunning", dunning.name, "status", "Resolved")
|
||||
|
||||
|
||||
def calculate_interest_and_amount(outstanding_amount, rate_of_interest, dunning_fee, overdue_days):
|
||||
interest_amount = 0
|
||||
@ -101,23 +122,26 @@ def calculate_interest_and_amount(outstanding_amount, rate_of_interest, dunning_
|
||||
grand_total += flt(interest_amount)
|
||||
dunning_amount = flt(interest_amount) + flt(dunning_fee)
|
||||
return {
|
||||
'interest_amount': interest_amount,
|
||||
'grand_total': grand_total,
|
||||
'dunning_amount': dunning_amount}
|
||||
"interest_amount": interest_amount,
|
||||
"grand_total": grand_total,
|
||||
"dunning_amount": dunning_amount,
|
||||
}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_dunning_letter_text(dunning_type, doc, language=None):
|
||||
if isinstance(doc, str):
|
||||
doc = json.loads(doc)
|
||||
if language:
|
||||
filters = {'parent': dunning_type, 'language': language}
|
||||
filters = {"parent": dunning_type, "language": language}
|
||||
else:
|
||||
filters = {'parent': dunning_type, 'is_default_language': 1}
|
||||
letter_text = frappe.db.get_value('Dunning Letter Text', filters,
|
||||
['body_text', 'closing_text', 'language'], as_dict=1)
|
||||
filters = {"parent": dunning_type, "is_default_language": 1}
|
||||
letter_text = frappe.db.get_value(
|
||||
"Dunning Letter Text", filters, ["body_text", "closing_text", "language"], as_dict=1
|
||||
)
|
||||
if letter_text:
|
||||
return {
|
||||
'body_text': frappe.render_template(letter_text.body_text, doc),
|
||||
'closing_text': frappe.render_template(letter_text.closing_text, doc),
|
||||
'language': letter_text.language
|
||||
"body_text": frappe.render_template(letter_text.body_text, doc),
|
||||
"closing_text": frappe.render_template(letter_text.closing_text, doc),
|
||||
"language": letter_text.language,
|
||||
}
|
||||
|
||||
@ -3,15 +3,10 @@ from frappe import _
|
||||
|
||||
def get_data():
|
||||
return {
|
||||
'fieldname': 'dunning',
|
||||
'non_standard_fieldnames': {
|
||||
'Journal Entry': 'reference_name',
|
||||
'Payment Entry': 'reference_name'
|
||||
"fieldname": "dunning",
|
||||
"non_standard_fieldnames": {
|
||||
"Journal Entry": "reference_name",
|
||||
"Payment Entry": "reference_name",
|
||||
},
|
||||
'transactions': [
|
||||
{
|
||||
'label': _('Payment'),
|
||||
'items': ['Payment Entry', 'Journal Entry']
|
||||
}
|
||||
]
|
||||
"transactions": [{"label": _("Payment"), "items": ["Payment Entry", "Journal Entry"]}],
|
||||
}
|
||||
|
||||
@ -30,30 +30,35 @@ class TestDunning(unittest.TestCase):
|
||||
def test_dunning(self):
|
||||
dunning = create_dunning()
|
||||
amounts = calculate_interest_and_amount(
|
||||
dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days)
|
||||
self.assertEqual(round(amounts.get('interest_amount'), 2), 0.44)
|
||||
self.assertEqual(round(amounts.get('dunning_amount'), 2), 20.44)
|
||||
self.assertEqual(round(amounts.get('grand_total'), 2), 120.44)
|
||||
dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days
|
||||
)
|
||||
self.assertEqual(round(amounts.get("interest_amount"), 2), 0.44)
|
||||
self.assertEqual(round(amounts.get("dunning_amount"), 2), 20.44)
|
||||
self.assertEqual(round(amounts.get("grand_total"), 2), 120.44)
|
||||
|
||||
def test_dunning_with_zero_interest_rate(self):
|
||||
dunning = create_dunning_with_zero_interest_rate()
|
||||
amounts = calculate_interest_and_amount(
|
||||
dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days)
|
||||
self.assertEqual(round(amounts.get('interest_amount'), 2), 0)
|
||||
self.assertEqual(round(amounts.get('dunning_amount'), 2), 20)
|
||||
self.assertEqual(round(amounts.get('grand_total'), 2), 120)
|
||||
dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days
|
||||
)
|
||||
self.assertEqual(round(amounts.get("interest_amount"), 2), 0)
|
||||
self.assertEqual(round(amounts.get("dunning_amount"), 2), 20)
|
||||
self.assertEqual(round(amounts.get("grand_total"), 2), 120)
|
||||
|
||||
def test_gl_entries(self):
|
||||
dunning = create_dunning()
|
||||
dunning.submit()
|
||||
gl_entries = frappe.db.sql("""select account, debit, credit
|
||||
gl_entries = frappe.db.sql(
|
||||
"""select account, debit, credit
|
||||
from `tabGL Entry` where voucher_type='Dunning' and voucher_no=%s
|
||||
order by account asc""", dunning.name, as_dict=1)
|
||||
order by account asc""",
|
||||
dunning.name,
|
||||
as_dict=1,
|
||||
)
|
||||
self.assertTrue(gl_entries)
|
||||
expected_values = dict((d[0], d) for d in [
|
||||
['Debtors - _TC', 20.44, 0.0],
|
||||
['Sales - _TC', 0.0, 20.44]
|
||||
])
|
||||
expected_values = dict(
|
||||
(d[0], d) for d in [["Debtors - _TC", 20.44, 0.0], ["Sales - _TC", 0.0, 20.44]]
|
||||
)
|
||||
for gle in gl_entries:
|
||||
self.assertEqual(expected_values[gle.account][0], gle.account)
|
||||
self.assertEqual(expected_values[gle.account][1], gle.debit)
|
||||
@ -71,7 +76,7 @@ class TestDunning(unittest.TestCase):
|
||||
pe.target_exchange_rate = 1
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
si_doc = frappe.get_doc('Sales Invoice', dunning.sales_invoice)
|
||||
si_doc = frappe.get_doc("Sales Invoice", dunning.sales_invoice)
|
||||
self.assertEqual(si_doc.outstanding_amount, 0)
|
||||
|
||||
|
||||
@ -79,8 +84,9 @@ def create_dunning():
|
||||
posting_date = add_days(today(), -20)
|
||||
due_date = add_days(today(), -15)
|
||||
sales_invoice = create_sales_invoice_against_cost_center(
|
||||
posting_date=posting_date, due_date=due_date, status='Overdue')
|
||||
dunning_type = frappe.get_doc("Dunning Type", 'First Notice')
|
||||
posting_date=posting_date, due_date=due_date, status="Overdue"
|
||||
)
|
||||
dunning_type = frappe.get_doc("Dunning Type", "First Notice")
|
||||
dunning = frappe.new_doc("Dunning")
|
||||
dunning.sales_invoice = sales_invoice.name
|
||||
dunning.customer_name = sales_invoice.customer_name
|
||||
@ -90,18 +96,20 @@ def create_dunning():
|
||||
dunning.company = sales_invoice.company
|
||||
dunning.posting_date = nowdate()
|
||||
dunning.due_date = sales_invoice.due_date
|
||||
dunning.dunning_type = 'First Notice'
|
||||
dunning.dunning_type = "First Notice"
|
||||
dunning.rate_of_interest = dunning_type.rate_of_interest
|
||||
dunning.dunning_fee = dunning_type.dunning_fee
|
||||
dunning.save()
|
||||
return dunning
|
||||
|
||||
|
||||
def create_dunning_with_zero_interest_rate():
|
||||
posting_date = add_days(today(), -20)
|
||||
due_date = add_days(today(), -15)
|
||||
sales_invoice = create_sales_invoice_against_cost_center(
|
||||
posting_date=posting_date, due_date=due_date, status='Overdue')
|
||||
dunning_type = frappe.get_doc("Dunning Type", 'First Notice with 0% Rate of Interest')
|
||||
posting_date=posting_date, due_date=due_date, status="Overdue"
|
||||
)
|
||||
dunning_type = frappe.get_doc("Dunning Type", "First Notice with 0% Rate of Interest")
|
||||
dunning = frappe.new_doc("Dunning")
|
||||
dunning.sales_invoice = sales_invoice.name
|
||||
dunning.customer_name = sales_invoice.customer_name
|
||||
@ -111,40 +119,44 @@ def create_dunning_with_zero_interest_rate():
|
||||
dunning.company = sales_invoice.company
|
||||
dunning.posting_date = nowdate()
|
||||
dunning.due_date = sales_invoice.due_date
|
||||
dunning.dunning_type = 'First Notice with 0% Rate of Interest'
|
||||
dunning.dunning_type = "First Notice with 0% Rate of Interest"
|
||||
dunning.rate_of_interest = dunning_type.rate_of_interest
|
||||
dunning.dunning_fee = dunning_type.dunning_fee
|
||||
dunning.save()
|
||||
return dunning
|
||||
|
||||
|
||||
def create_dunning_type():
|
||||
dunning_type = frappe.new_doc("Dunning Type")
|
||||
dunning_type.dunning_type = 'First Notice'
|
||||
dunning_type.dunning_type = "First Notice"
|
||||
dunning_type.start_day = 10
|
||||
dunning_type.end_day = 20
|
||||
dunning_type.dunning_fee = 20
|
||||
dunning_type.rate_of_interest = 8
|
||||
dunning_type.append(
|
||||
"dunning_letter_text", {
|
||||
'language': 'en',
|
||||
'body_text': 'We have still not received payment for our invoice ',
|
||||
'closing_text': 'We kindly request that you pay the outstanding amount immediately, including interest and late fees.'
|
||||
}
|
||||
"dunning_letter_text",
|
||||
{
|
||||
"language": "en",
|
||||
"body_text": "We have still not received payment for our invoice ",
|
||||
"closing_text": "We kindly request that you pay the outstanding amount immediately, including interest and late fees.",
|
||||
},
|
||||
)
|
||||
dunning_type.save()
|
||||
|
||||
|
||||
def create_dunning_type_with_zero_interest_rate():
|
||||
dunning_type = frappe.new_doc("Dunning Type")
|
||||
dunning_type.dunning_type = 'First Notice with 0% Rate of Interest'
|
||||
dunning_type.dunning_type = "First Notice with 0% Rate of Interest"
|
||||
dunning_type.start_day = 10
|
||||
dunning_type.end_day = 20
|
||||
dunning_type.dunning_fee = 20
|
||||
dunning_type.rate_of_interest = 0
|
||||
dunning_type.append(
|
||||
"dunning_letter_text", {
|
||||
'language': 'en',
|
||||
'body_text': 'We have still not received payment for our invoice ',
|
||||
'closing_text': 'We kindly request that you pay the outstanding amount immediately, and late fees.'
|
||||
}
|
||||
"dunning_letter_text",
|
||||
{
|
||||
"language": "en",
|
||||
"body_text": "We have still not received payment for our invoice ",
|
||||
"closing_text": "We kindly request that you pay the outstanding amount immediately, and late fees.",
|
||||
},
|
||||
)
|
||||
dunning_type.save()
|
||||
|
||||
@ -20,8 +20,9 @@ class ExchangeRateRevaluation(Document):
|
||||
def set_total_gain_loss(self):
|
||||
total_gain_loss = 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"))
|
||||
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"))
|
||||
total_gain_loss += flt(d.gain_loss, d.precision("gain_loss"))
|
||||
self.total_gain_loss = flt(total_gain_loss, self.precision("total_gain_loss"))
|
||||
|
||||
@ -30,15 +31,15 @@ class ExchangeRateRevaluation(Document):
|
||||
frappe.throw(_("Please select Company and Posting Date to getting entries"))
|
||||
|
||||
def on_cancel(self):
|
||||
self.ignore_linked_doctypes = ('GL Entry')
|
||||
self.ignore_linked_doctypes = "GL Entry"
|
||||
|
||||
@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")
|
||||
total_debit = frappe.db.get_value(
|
||||
"Journal Entry Account",
|
||||
{"reference_type": "Exchange Rate Revaluation", "reference_name": self.name, "docstatus": 1},
|
||||
"sum(debit) as sum",
|
||||
)
|
||||
|
||||
total_amt = 0
|
||||
for d in self.accounts:
|
||||
@ -54,28 +55,33 @@ class ExchangeRateRevaluation(Document):
|
||||
accounts = []
|
||||
self.validate_mandatory()
|
||||
company_currency = erpnext.get_company_currency(self.company)
|
||||
precision = get_field_precision(frappe.get_meta("Exchange Rate Revaluation Account")
|
||||
.get_field("new_balance_in_base_currency"), company_currency)
|
||||
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
|
||||
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
|
||||
})
|
||||
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 not accounts:
|
||||
self.throw_invalid_response_message(account_details)
|
||||
@ -84,7 +90,8 @@ class ExchangeRateRevaluation(Document):
|
||||
|
||||
def get_accounts_from_gle(self):
|
||||
company_currency = erpnext.get_company_currency(self.company)
|
||||
accounts = frappe.db.sql_list("""
|
||||
accounts = frappe.db.sql_list(
|
||||
"""
|
||||
select name
|
||||
from tabAccount
|
||||
where is_group = 0
|
||||
@ -93,11 +100,14 @@ class ExchangeRateRevaluation(Document):
|
||||
and account_type != 'Stock'
|
||||
and company=%s
|
||||
and account_currency != %s
|
||||
order by name""",(self.company, company_currency))
|
||||
order by name""",
|
||||
(self.company, company_currency),
|
||||
)
|
||||
|
||||
account_details = []
|
||||
if accounts:
|
||||
account_details = frappe.db.sql("""
|
||||
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,
|
||||
@ -109,7 +119,11 @@ class ExchangeRateRevaluation(Document):
|
||||
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)
|
||||
"""
|
||||
% (", ".join(["%s"] * len(accounts)), "%s"),
|
||||
tuple(accounts + [self.posting_date]),
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
return account_details
|
||||
|
||||
@ -125,77 +139,107 @@ class ExchangeRateRevaluation(Document):
|
||||
if self.total_gain_loss == 0:
|
||||
return
|
||||
|
||||
unrealized_exchange_gain_loss_account = frappe.get_cached_value('Company', self.company,
|
||||
"unrealized_exchange_gain_loss_account")
|
||||
unrealized_exchange_gain_loss_account = frappe.get_cached_value(
|
||||
"Company", self.company, "unrealized_exchange_gain_loss_account"
|
||||
)
|
||||
if not unrealized_exchange_gain_loss_account:
|
||||
frappe.throw(_("Please set Unrealized Exchange Gain/Loss Account in Company {0}")
|
||||
.format(self.company))
|
||||
frappe.throw(
|
||||
_("Please set Unrealized Exchange Gain/Loss Account in Company {0}").format(self.company)
|
||||
)
|
||||
|
||||
journal_entry = frappe.new_doc('Journal Entry')
|
||||
journal_entry.voucher_type = 'Exchange Rate Revaluation'
|
||||
journal_entry = frappe.new_doc("Journal Entry")
|
||||
journal_entry.voucher_type = "Exchange Rate Revaluation"
|
||||
journal_entry.company = self.company
|
||||
journal_entry.posting_date = self.posting_date
|
||||
journal_entry.multi_currency = 1
|
||||
|
||||
journal_entry_accounts = []
|
||||
for d in self.accounts:
|
||||
dr_or_cr = "debit_in_account_currency" \
|
||||
if d.get("balance_in_account_currency") > 0 else "credit_in_account_currency"
|
||||
dr_or_cr = (
|
||||
"debit_in_account_currency"
|
||||
if d.get("balance_in_account_currency") > 0
|
||||
else "credit_in_account_currency"
|
||||
)
|
||||
|
||||
reverse_dr_or_cr = "debit_in_account_currency" \
|
||||
if dr_or_cr=="credit_in_account_currency" else "credit_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_entry_accounts.append({
|
||||
"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")),
|
||||
dr_or_cr: flt(abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency")),
|
||||
"exchange_rate": flt(d.get("new_exchange_rate"), d.precision("new_exchange_rate")),
|
||||
journal_entry_accounts.append(
|
||||
{
|
||||
"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")
|
||||
),
|
||||
dr_or_cr: flt(
|
||||
abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency")
|
||||
),
|
||||
"exchange_rate": flt(d.get("new_exchange_rate"), d.precision("new_exchange_rate")),
|
||||
"reference_type": "Exchange Rate Revaluation",
|
||||
"reference_name": self.name,
|
||||
}
|
||||
)
|
||||
journal_entry_accounts.append(
|
||||
{
|
||||
"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")
|
||||
),
|
||||
reverse_dr_or_cr: flt(
|
||||
abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency")
|
||||
),
|
||||
"exchange_rate": flt(d.get("current_exchange_rate"), d.precision("current_exchange_rate")),
|
||||
"reference_type": "Exchange Rate Revaluation",
|
||||
"reference_name": self.name,
|
||||
}
|
||||
)
|
||||
|
||||
journal_entry_accounts.append(
|
||||
{
|
||||
"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,
|
||||
"exchange_rate": 1,
|
||||
"reference_type": "Exchange Rate Revaluation",
|
||||
"reference_name": self.name,
|
||||
})
|
||||
journal_entry_accounts.append({
|
||||
"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")),
|
||||
reverse_dr_or_cr: flt(abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency")),
|
||||
"exchange_rate": flt(d.get("current_exchange_rate"), d.precision("current_exchange_rate")),
|
||||
"reference_type": "Exchange Rate Revaluation",
|
||||
"reference_name": self.name
|
||||
})
|
||||
|
||||
journal_entry_accounts.append({
|
||||
"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,
|
||||
"exchange_rate": 1,
|
||||
"reference_type": "Exchange Rate Revaluation",
|
||||
"reference_name": self.name,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
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()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_account_details(account, company, posting_date, party_type=None, party=None):
|
||||
account_currency, account_type = frappe.db.get_value("Account", account,
|
||||
["account_currency", "account_type"])
|
||||
account_currency, account_type = frappe.db.get_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)
|
||||
balance = get_balance_on(
|
||||
account, date=posting_date, party_type=party_type, party=party, in_account_currency=False
|
||||
)
|
||||
if balance:
|
||||
balance_in_account_currency = get_balance_on(account, date=posting_date, party_type=party_type, party=party)
|
||||
current_exchange_rate = balance / balance_in_account_currency if balance_in_account_currency else 0
|
||||
balance_in_account_currency = get_balance_on(
|
||||
account, date=posting_date, party_type=party_type, party=party
|
||||
)
|
||||
current_exchange_rate = (
|
||||
balance / balance_in_account_currency if balance_in_account_currency else 0
|
||||
)
|
||||
new_exchange_rate = get_exchange_rate(account_currency, company_currency, posting_date)
|
||||
new_balance_in_base_currency = balance_in_account_currency * new_exchange_rate
|
||||
account_details = {
|
||||
@ -204,7 +248,7 @@ def get_account_details(account, company, posting_date, party_type=None, party=N
|
||||
"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
|
||||
"new_balance_in_base_currency": new_balance_in_base_currency,
|
||||
}
|
||||
|
||||
return account_details
|
||||
|
||||
@ -1,9 +1,2 @@
|
||||
def get_data():
|
||||
return {
|
||||
'fieldname': 'reference_name',
|
||||
'transactions': [
|
||||
{
|
||||
'items': ['Journal Entry']
|
||||
}
|
||||
]
|
||||
}
|
||||
return {"fieldname": "reference_name", "transactions": [{"items": ["Journal Entry"]}]}
|
||||
|
||||
@ -3,21 +3,11 @@ from frappe import _
|
||||
|
||||
def get_data():
|
||||
return {
|
||||
'fieldname': 'finance_book',
|
||||
'non_standard_fieldnames': {
|
||||
'Asset': 'default_finance_book',
|
||||
'Company': 'default_finance_book'
|
||||
},
|
||||
'transactions': [
|
||||
{
|
||||
'label': _('Assets'),
|
||||
'items': ['Asset', 'Asset Value Adjustment']
|
||||
},
|
||||
{
|
||||
'items': ['Company']
|
||||
},
|
||||
{
|
||||
'items': ['Journal Entry']
|
||||
}
|
||||
]
|
||||
"fieldname": "finance_book",
|
||||
"non_standard_fieldnames": {"Asset": "default_finance_book", "Company": "default_finance_book"},
|
||||
"transactions": [
|
||||
{"label": _("Assets"), "items": ["Asset", "Asset Value Adjustment"]},
|
||||
{"items": ["Company"]},
|
||||
{"items": ["Journal Entry"]},
|
||||
],
|
||||
}
|
||||
|
||||
@ -13,30 +13,29 @@ class TestFinanceBook(unittest.TestCase):
|
||||
finance_book = create_finance_book()
|
||||
|
||||
# create jv entry
|
||||
jv = make_journal_entry("_Test Bank - _TC",
|
||||
"_Test Receivable - _TC", 100, save=False)
|
||||
jv = make_journal_entry("_Test Bank - _TC", "_Test Receivable - _TC", 100, save=False)
|
||||
|
||||
jv.accounts[1].update({
|
||||
"party_type": "Customer",
|
||||
"party": "_Test Customer"
|
||||
})
|
||||
jv.accounts[1].update({"party_type": "Customer", "party": "_Test Customer"})
|
||||
|
||||
jv.finance_book = finance_book.finance_book_name
|
||||
jv.submit()
|
||||
|
||||
# check the Finance Book in the GL Entry
|
||||
gl_entries = frappe.get_all("GL Entry", fields=["name", "finance_book"],
|
||||
filters={"voucher_type": "Journal Entry", "voucher_no": jv.name})
|
||||
gl_entries = frappe.get_all(
|
||||
"GL Entry",
|
||||
fields=["name", "finance_book"],
|
||||
filters={"voucher_type": "Journal Entry", "voucher_no": jv.name},
|
||||
)
|
||||
|
||||
for gl_entry in gl_entries:
|
||||
self.assertEqual(gl_entry.finance_book, finance_book.name)
|
||||
|
||||
|
||||
def create_finance_book():
|
||||
if not frappe.db.exists("Finance Book", "_Test Finance Book"):
|
||||
finance_book = frappe.get_doc({
|
||||
"doctype": "Finance Book",
|
||||
"finance_book_name": "_Test Finance Book"
|
||||
}).insert()
|
||||
finance_book = frappe.get_doc(
|
||||
{"doctype": "Finance Book", "finance_book_name": "_Test Finance Book"}
|
||||
).insert()
|
||||
else:
|
||||
finance_book = frappe.get_doc("Finance Book", "_Test Finance Book")
|
||||
|
||||
|
||||
@ -9,7 +9,9 @@ from frappe.model.document import Document
|
||||
from frappe.utils import add_days, add_years, cstr, getdate
|
||||
|
||||
|
||||
class FiscalYearIncorrectDate(frappe.ValidationError): pass
|
||||
class FiscalYearIncorrectDate(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class FiscalYear(Document):
|
||||
@frappe.whitelist()
|
||||
@ -22,19 +24,33 @@ class FiscalYear(Document):
|
||||
# clear cache
|
||||
frappe.clear_cache()
|
||||
|
||||
msgprint(_("{0} is now the default Fiscal Year. Please refresh your browser for the change to take effect.").format(self.name))
|
||||
msgprint(
|
||||
_(
|
||||
"{0} is now the default Fiscal Year. Please refresh your browser for the change to take effect."
|
||||
).format(self.name)
|
||||
)
|
||||
|
||||
def validate(self):
|
||||
self.validate_dates()
|
||||
self.validate_overlap()
|
||||
|
||||
if not self.is_new():
|
||||
year_start_end_dates = frappe.db.sql("""select year_start_date, year_end_date
|
||||
from `tabFiscal Year` where name=%s""", (self.name))
|
||||
year_start_end_dates = frappe.db.sql(
|
||||
"""select year_start_date, year_end_date
|
||||
from `tabFiscal Year` where name=%s""",
|
||||
(self.name),
|
||||
)
|
||||
|
||||
if year_start_end_dates:
|
||||
if getdate(self.year_start_date) != year_start_end_dates[0][0] or getdate(self.year_end_date) != year_start_end_dates[0][1]:
|
||||
frappe.throw(_("Cannot change Fiscal Year Start Date and Fiscal Year End Date once the Fiscal Year is saved."))
|
||||
if (
|
||||
getdate(self.year_start_date) != year_start_end_dates[0][0]
|
||||
or getdate(self.year_end_date) != year_start_end_dates[0][1]
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Cannot change Fiscal Year Start Date and Fiscal Year End Date once the Fiscal Year is saved."
|
||||
)
|
||||
)
|
||||
|
||||
def validate_dates(self):
|
||||
if self.is_short_year:
|
||||
@ -43,14 +59,18 @@ class FiscalYear(Document):
|
||||
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)
|
||||
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.throw(
|
||||
_("Fiscal Year End Date should be one year after Fiscal Year Start Date"),
|
||||
FiscalYearIncorrectDate,
|
||||
)
|
||||
|
||||
def on_update(self):
|
||||
check_duplicate_fiscal_year(self)
|
||||
@ -59,11 +79,16 @@ class FiscalYear(Document):
|
||||
def on_trash(self):
|
||||
global_defaults = frappe.get_doc("Global Defaults")
|
||||
if global_defaults.current_fiscal_year == self.name:
|
||||
frappe.throw(_("You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global Settings").format(self.name))
|
||||
frappe.throw(
|
||||
_(
|
||||
"You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global Settings"
|
||||
).format(self.name)
|
||||
)
|
||||
frappe.cache().delete_value("fiscal_years")
|
||||
|
||||
def validate_overlap(self):
|
||||
existing_fiscal_years = frappe.db.sql("""select name from `tabFiscal Year`
|
||||
existing_fiscal_years = frappe.db.sql(
|
||||
"""select name from `tabFiscal Year`
|
||||
where (
|
||||
(%(year_start_date)s between year_start_date and year_end_date)
|
||||
or (%(year_end_date)s between year_start_date and year_end_date)
|
||||
@ -73,13 +98,18 @@ class FiscalYear(Document):
|
||||
{
|
||||
"year_start_date": self.year_start_date,
|
||||
"year_end_date": self.year_end_date,
|
||||
"name": self.name or "No Name"
|
||||
}, as_dict=True)
|
||||
"name": self.name or "No Name",
|
||||
},
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
if existing_fiscal_years:
|
||||
for existing in existing_fiscal_years:
|
||||
company_for_existing = frappe.db.sql_list("""select company from `tabFiscal Year Company`
|
||||
where parent=%s""", existing.name)
|
||||
company_for_existing = frappe.db.sql_list(
|
||||
"""select company from `tabFiscal Year Company`
|
||||
where parent=%s""",
|
||||
existing.name,
|
||||
)
|
||||
|
||||
overlap = False
|
||||
if not self.get("companies") or not company_for_existing:
|
||||
@ -90,20 +120,36 @@ class FiscalYear(Document):
|
||||
overlap = True
|
||||
|
||||
if overlap:
|
||||
frappe.throw(_("Year start date or end date is overlapping with {0}. To avoid please set company")
|
||||
.format(existing.name), frappe.NameError)
|
||||
frappe.throw(
|
||||
_("Year start date or end date is overlapping with {0}. To avoid please set company").format(
|
||||
existing.name
|
||||
),
|
||||
frappe.NameError,
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def check_duplicate_fiscal_year(doc):
|
||||
year_start_end_dates = frappe.db.sql("""select name, year_start_date, year_end_date from `tabFiscal Year` where name!=%s""", (doc.name))
|
||||
year_start_end_dates = frappe.db.sql(
|
||||
"""select name, year_start_date, year_end_date from `tabFiscal Year` where name!=%s""",
|
||||
(doc.name),
|
||||
)
|
||||
for fiscal_year, ysd, yed in year_start_end_dates:
|
||||
if (getdate(doc.year_start_date) == ysd and getdate(doc.year_end_date) == yed) and (not frappe.flags.in_test):
|
||||
frappe.throw(_("Fiscal Year Start Date and Fiscal Year End Date are already set in Fiscal Year {0}").format(fiscal_year))
|
||||
if (getdate(doc.year_start_date) == ysd and getdate(doc.year_end_date) == yed) and (
|
||||
not frappe.flags.in_test
|
||||
):
|
||||
frappe.throw(
|
||||
_("Fiscal Year Start Date and Fiscal Year End Date are already set in Fiscal Year {0}").format(
|
||||
fiscal_year
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def auto_create_fiscal_year():
|
||||
for d in frappe.db.sql("""select name from `tabFiscal Year` where year_end_date = date_add(current_date, interval 3 day)"""):
|
||||
for d in frappe.db.sql(
|
||||
"""select name from `tabFiscal Year` where year_end_date = date_add(current_date, interval 3 day)"""
|
||||
):
|
||||
try:
|
||||
current_fy = frappe.get_doc("Fiscal Year", d[0])
|
||||
|
||||
@ -114,16 +160,14 @@ def auto_create_fiscal_year():
|
||||
|
||||
start_year = cstr(new_fy.year_start_date.year)
|
||||
end_year = cstr(new_fy.year_end_date.year)
|
||||
new_fy.year = start_year if start_year==end_year else (start_year + "-" + end_year)
|
||||
new_fy.year = start_year if start_year == end_year else (start_year + "-" + end_year)
|
||||
new_fy.auto_created = 1
|
||||
|
||||
new_fy.insert(ignore_permissions=True)
|
||||
except frappe.NameError:
|
||||
pass
|
||||
|
||||
|
||||
def get_from_and_to_date(fiscal_year):
|
||||
fields = [
|
||||
"year_start_date as from_date",
|
||||
"year_end_date as to_date"
|
||||
]
|
||||
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)
|
||||
|
||||
@ -3,19 +3,13 @@ from frappe import _
|
||||
|
||||
def get_data():
|
||||
return {
|
||||
'fieldname': 'fiscal_year',
|
||||
'transactions': [
|
||||
"fieldname": "fiscal_year",
|
||||
"transactions": [
|
||||
{"label": _("Budgets"), "items": ["Budget"]},
|
||||
{"label": _("References"), "items": ["Period Closing Voucher"]},
|
||||
{
|
||||
'label': _('Budgets'),
|
||||
'items': ['Budget']
|
||||
"label": _("Target Details"),
|
||||
"items": ["Sales Person", "Sales Partner", "Territory", "Monthly Distribution"],
|
||||
},
|
||||
{
|
||||
'label': _('References'),
|
||||
'items': ['Period Closing Voucher']
|
||||
},
|
||||
{
|
||||
'label': _('Target Details'),
|
||||
'items': ['Sales Person', 'Sales Partner', 'Territory', 'Monthly Distribution']
|
||||
}
|
||||
]
|
||||
],
|
||||
}
|
||||
|
||||
@ -11,43 +11,48 @@ from erpnext.accounts.doctype.fiscal_year.fiscal_year import FiscalYearIncorrect
|
||||
|
||||
test_ignore = ["Company"]
|
||||
|
||||
class TestFiscalYear(unittest.TestCase):
|
||||
|
||||
class TestFiscalYear(unittest.TestCase):
|
||||
def test_extra_year(self):
|
||||
if frappe.db.exists("Fiscal Year", "_Test Fiscal Year 2000"):
|
||||
frappe.delete_doc("Fiscal Year", "_Test Fiscal Year 2000")
|
||||
|
||||
fy = frappe.get_doc({
|
||||
"doctype": "Fiscal Year",
|
||||
"year": "_Test Fiscal Year 2000",
|
||||
"year_end_date": "2002-12-31",
|
||||
"year_start_date": "2000-04-01"
|
||||
})
|
||||
fy = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Fiscal Year",
|
||||
"year": "_Test Fiscal Year 2000",
|
||||
"year_end_date": "2002-12-31",
|
||||
"year_start_date": "2000-04-01",
|
||||
}
|
||||
)
|
||||
|
||||
self.assertRaises(FiscalYearIncorrectDate, fy.insert)
|
||||
|
||||
|
||||
def test_record_generator():
|
||||
test_records = [
|
||||
{
|
||||
"doctype": "Fiscal Year",
|
||||
"year": "_Test Short Fiscal Year 2011",
|
||||
"is_short_year": 1,
|
||||
"year_end_date": "2011-04-01",
|
||||
"year_start_date": "2011-12-31"
|
||||
}
|
||||
{
|
||||
"doctype": "Fiscal Year",
|
||||
"year": "_Test Short Fiscal Year 2011",
|
||||
"is_short_year": 1,
|
||||
"year_end_date": "2011-04-01",
|
||||
"year_start_date": "2011-12-31",
|
||||
}
|
||||
]
|
||||
|
||||
start = 2012
|
||||
end = now_datetime().year + 5
|
||||
for year in range(start, end):
|
||||
test_records.append({
|
||||
"doctype": "Fiscal Year",
|
||||
"year": f"_Test Fiscal Year {year}",
|
||||
"year_start_date": f"{year}-01-01",
|
||||
"year_end_date": f"{year}-12-31"
|
||||
})
|
||||
test_records.append(
|
||||
{
|
||||
"doctype": "Fiscal Year",
|
||||
"year": f"_Test Fiscal Year {year}",
|
||||
"year_start_date": f"{year}-01-01",
|
||||
"year_end_date": f"{year}-12-31",
|
||||
}
|
||||
)
|
||||
|
||||
return test_records
|
||||
|
||||
|
||||
test_records = test_record_generator()
|
||||
|
||||
@ -3,6 +3,6 @@
|
||||
|
||||
frappe.ui.form.on('GL Entry', {
|
||||
refresh: function(frm) {
|
||||
|
||||
frm.page.btn_secondary.hide()
|
||||
}
|
||||
});
|
||||
|
||||
@ -25,6 +25,8 @@ from erpnext.exceptions import (
|
||||
)
|
||||
|
||||
exclude_from_linked_with = True
|
||||
|
||||
|
||||
class GLEntry(Document):
|
||||
def autoname(self):
|
||||
"""
|
||||
@ -32,6 +34,8 @@ class GLEntry(Document):
|
||||
name will be changed using autoname options (in a scheduled job)
|
||||
"""
|
||||
self.name = frappe.generate_hash(txt="", length=10)
|
||||
if self.meta.autoname == "hash":
|
||||
self.to_rename = 0
|
||||
|
||||
def validate(self):
|
||||
self.flags.ignore_submit_comment = True
|
||||
@ -54,15 +58,23 @@ class GLEntry(Document):
|
||||
validate_balance_type(self.account, adv_adj)
|
||||
validate_frozen_account(self.account, adv_adj)
|
||||
|
||||
# Update outstanding amt on against voucher
|
||||
if (self.against_voucher_type in ['Journal Entry', 'Sales Invoice', 'Purchase Invoice', 'Fees']
|
||||
and self.against_voucher and self.flags.update_outstanding == 'Yes'
|
||||
and not frappe.flags.is_reverse_depr_entry):
|
||||
update_outstanding_amt(self.account, self.party_type, self.party, self.against_voucher_type,
|
||||
self.against_voucher)
|
||||
if frappe.db.get_value("Account", self.account, "account_type") not in [
|
||||
"Receivable",
|
||||
"Payable",
|
||||
]:
|
||||
# Update outstanding amt on against voucher
|
||||
if (
|
||||
self.against_voucher_type in ["Journal Entry", "Sales Invoice", "Purchase Invoice", "Fees"]
|
||||
and self.against_voucher
|
||||
and self.flags.update_outstanding == "Yes"
|
||||
and not frappe.flags.is_reverse_depr_entry
|
||||
):
|
||||
update_outstanding_amt(
|
||||
self.account, self.party_type, self.party, self.against_voucher_type, self.against_voucher
|
||||
)
|
||||
|
||||
def check_mandatory(self):
|
||||
mandatory = ['account','voucher_type','voucher_no','company']
|
||||
mandatory = ["account", "voucher_type", "voucher_no", "company"]
|
||||
for k in mandatory:
|
||||
if not self.get(k):
|
||||
frappe.throw(_("{0} is required").format(_(self.meta.get_label(k))))
|
||||
@ -70,29 +82,40 @@ class GLEntry(Document):
|
||||
if not (self.party_type and self.party):
|
||||
account_type = frappe.get_cached_value("Account", self.account, "account_type")
|
||||
if account_type == "Receivable":
|
||||
frappe.throw(_("{0} {1}: Customer is required against Receivable account {2}")
|
||||
.format(self.voucher_type, self.voucher_no, self.account))
|
||||
frappe.throw(
|
||||
_("{0} {1}: Customer is required against Receivable account {2}").format(
|
||||
self.voucher_type, self.voucher_no, self.account
|
||||
)
|
||||
)
|
||||
elif account_type == "Payable":
|
||||
frappe.throw(_("{0} {1}: Supplier is required against Payable account {2}")
|
||||
.format(self.voucher_type, self.voucher_no, self.account))
|
||||
frappe.throw(
|
||||
_("{0} {1}: Supplier is required against Payable account {2}").format(
|
||||
self.voucher_type, self.voucher_no, self.account
|
||||
)
|
||||
)
|
||||
|
||||
# Zero value transaction is not allowed
|
||||
if not (flt(self.debit, self.precision("debit")) or flt(self.credit, self.precision("credit"))):
|
||||
frappe.throw(_("{0} {1}: Either debit or credit amount is required for {2}")
|
||||
.format(self.voucher_type, self.voucher_no, self.account))
|
||||
frappe.throw(
|
||||
_("{0} {1}: Either debit or credit amount is required for {2}").format(
|
||||
self.voucher_type, self.voucher_no, self.account
|
||||
)
|
||||
)
|
||||
|
||||
def pl_must_have_cost_center(self):
|
||||
"""Validate that profit and loss type account GL entries have a cost center."""
|
||||
|
||||
if self.cost_center or self.voucher_type == 'Period Closing Voucher':
|
||||
if self.cost_center or self.voucher_type == "Period Closing Voucher":
|
||||
return
|
||||
|
||||
if frappe.get_cached_value("Account", self.account, "report_type") == "Profit and Loss":
|
||||
msg = _("{0} {1}: Cost Center is required for 'Profit and Loss' account {2}.").format(
|
||||
self.voucher_type, self.voucher_no, self.account)
|
||||
self.voucher_type, self.voucher_no, self.account
|
||||
)
|
||||
msg += " "
|
||||
msg += _("Please set the cost center field in {0} or setup a default Cost Center for the Company.").format(
|
||||
self.voucher_type)
|
||||
msg += _(
|
||||
"Please set the cost center field in {0} or setup a default Cost Center for the Company."
|
||||
).format(self.voucher_type)
|
||||
|
||||
frappe.throw(msg, title=_("Missing Cost Center"))
|
||||
|
||||
@ -100,17 +123,31 @@ class GLEntry(Document):
|
||||
account_type = frappe.db.get_value("Account", self.account, "report_type")
|
||||
|
||||
for dimension in get_checks_for_pl_and_bs_accounts():
|
||||
if account_type == "Profit and Loss" \
|
||||
and self.company == dimension.company and dimension.mandatory_for_pl and not dimension.disabled:
|
||||
if (
|
||||
account_type == "Profit and Loss"
|
||||
and self.company == dimension.company
|
||||
and dimension.mandatory_for_pl
|
||||
and not dimension.disabled
|
||||
):
|
||||
if not self.get(dimension.fieldname):
|
||||
frappe.throw(_("Accounting Dimension <b>{0}</b> is required for 'Profit and Loss' account {1}.")
|
||||
.format(dimension.label, self.account))
|
||||
frappe.throw(
|
||||
_("Accounting Dimension <b>{0}</b> is required for 'Profit and Loss' account {1}.").format(
|
||||
dimension.label, self.account
|
||||
)
|
||||
)
|
||||
|
||||
if account_type == "Balance Sheet" \
|
||||
and self.company == dimension.company and dimension.mandatory_for_bs and not dimension.disabled:
|
||||
if (
|
||||
account_type == "Balance Sheet"
|
||||
and self.company == dimension.company
|
||||
and dimension.mandatory_for_bs
|
||||
and not dimension.disabled
|
||||
):
|
||||
if not self.get(dimension.fieldname):
|
||||
frappe.throw(_("Accounting Dimension <b>{0}</b> is required for 'Balance Sheet' account {1}.")
|
||||
.format(dimension.label, self.account))
|
||||
frappe.throw(
|
||||
_("Accounting Dimension <b>{0}</b> is required for 'Balance Sheet' account {1}.").format(
|
||||
dimension.label, self.account
|
||||
)
|
||||
)
|
||||
|
||||
def validate_allowed_dimensions(self):
|
||||
dimension_filter_map = get_dimension_filter_map()
|
||||
@ -119,56 +156,97 @@ class GLEntry(Document):
|
||||
account = key[1]
|
||||
|
||||
if self.account == account:
|
||||
if value['is_mandatory'] and not self.get(dimension):
|
||||
frappe.throw(_("{0} is mandatory for account {1}").format(
|
||||
frappe.bold(frappe.unscrub(dimension)), frappe.bold(self.account)), MandatoryAccountDimensionError)
|
||||
if value["is_mandatory"] and not self.get(dimension):
|
||||
frappe.throw(
|
||||
_("{0} is mandatory for account {1}").format(
|
||||
frappe.bold(frappe.unscrub(dimension)), frappe.bold(self.account)
|
||||
),
|
||||
MandatoryAccountDimensionError,
|
||||
)
|
||||
|
||||
if value['allow_or_restrict'] == 'Allow':
|
||||
if self.get(dimension) and self.get(dimension) not in value['allowed_dimensions']:
|
||||
frappe.throw(_("Invalid value {0} for {1} against account {2}").format(
|
||||
frappe.bold(self.get(dimension)), frappe.bold(frappe.unscrub(dimension)), frappe.bold(self.account)), InvalidAccountDimensionError)
|
||||
if value["allow_or_restrict"] == "Allow":
|
||||
if self.get(dimension) and self.get(dimension) not in value["allowed_dimensions"]:
|
||||
frappe.throw(
|
||||
_("Invalid value {0} for {1} against account {2}").format(
|
||||
frappe.bold(self.get(dimension)),
|
||||
frappe.bold(frappe.unscrub(dimension)),
|
||||
frappe.bold(self.account),
|
||||
),
|
||||
InvalidAccountDimensionError,
|
||||
)
|
||||
else:
|
||||
if self.get(dimension) and self.get(dimension) in value['allowed_dimensions']:
|
||||
frappe.throw(_("Invalid value {0} for {1} against account {2}").format(
|
||||
frappe.bold(self.get(dimension)), frappe.bold(frappe.unscrub(dimension)), frappe.bold(self.account)), InvalidAccountDimensionError)
|
||||
if self.get(dimension) and self.get(dimension) in value["allowed_dimensions"]:
|
||||
frappe.throw(
|
||||
_("Invalid value {0} for {1} against account {2}").format(
|
||||
frappe.bold(self.get(dimension)),
|
||||
frappe.bold(frappe.unscrub(dimension)),
|
||||
frappe.bold(self.account),
|
||||
),
|
||||
InvalidAccountDimensionError,
|
||||
)
|
||||
|
||||
def check_pl_account(self):
|
||||
if self.is_opening=='Yes' and \
|
||||
frappe.db.get_value("Account", self.account, "report_type")=="Profit and Loss":
|
||||
frappe.throw(_("{0} {1}: 'Profit and Loss' type account {2} not allowed in Opening Entry")
|
||||
.format(self.voucher_type, self.voucher_no, self.account))
|
||||
if (
|
||||
self.is_opening == "Yes"
|
||||
and frappe.db.get_value("Account", self.account, "report_type") == "Profit and Loss"
|
||||
and not self.is_cancelled
|
||||
):
|
||||
frappe.throw(
|
||||
_("{0} {1}: 'Profit and Loss' type account {2} not allowed in Opening Entry").format(
|
||||
self.voucher_type, self.voucher_no, self.account
|
||||
)
|
||||
)
|
||||
|
||||
def validate_account_details(self, adv_adj):
|
||||
"""Account must be ledger, active and not freezed"""
|
||||
|
||||
ret = frappe.db.sql("""select is_group, docstatus, company
|
||||
from tabAccount where name=%s""", self.account, as_dict=1)[0]
|
||||
ret = frappe.db.sql(
|
||||
"""select is_group, docstatus, company
|
||||
from tabAccount where name=%s""",
|
||||
self.account,
|
||||
as_dict=1,
|
||||
)[0]
|
||||
|
||||
if ret.is_group==1:
|
||||
frappe.throw(_('''{0} {1}: Account {2} is a Group Account and group accounts cannot be used in transactions''')
|
||||
.format(self.voucher_type, self.voucher_no, self.account))
|
||||
if ret.is_group == 1:
|
||||
frappe.throw(
|
||||
_(
|
||||
"""{0} {1}: Account {2} is a Group Account and group accounts cannot be used in transactions"""
|
||||
).format(self.voucher_type, self.voucher_no, self.account)
|
||||
)
|
||||
|
||||
if ret.docstatus==2:
|
||||
frappe.throw(_("{0} {1}: Account {2} is inactive")
|
||||
.format(self.voucher_type, self.voucher_no, self.account))
|
||||
if ret.docstatus == 2:
|
||||
frappe.throw(
|
||||
_("{0} {1}: Account {2} is inactive").format(self.voucher_type, self.voucher_no, self.account)
|
||||
)
|
||||
|
||||
if ret.company != self.company:
|
||||
frappe.throw(_("{0} {1}: Account {2} does not belong to Company {3}")
|
||||
.format(self.voucher_type, self.voucher_no, self.account, self.company))
|
||||
frappe.throw(
|
||||
_("{0} {1}: Account {2} does not belong to Company {3}").format(
|
||||
self.voucher_type, self.voucher_no, self.account, self.company
|
||||
)
|
||||
)
|
||||
|
||||
def validate_cost_center(self):
|
||||
if not self.cost_center: return
|
||||
if not self.cost_center:
|
||||
return
|
||||
|
||||
is_group, company = frappe.get_cached_value('Cost Center',
|
||||
self.cost_center, ['is_group', 'company'])
|
||||
is_group, company = frappe.get_cached_value(
|
||||
"Cost Center", self.cost_center, ["is_group", "company"]
|
||||
)
|
||||
|
||||
if company != self.company:
|
||||
frappe.throw(_("{0} {1}: Cost Center {2} does not belong to Company {3}")
|
||||
.format(self.voucher_type, self.voucher_no, self.cost_center, self.company))
|
||||
frappe.throw(
|
||||
_("{0} {1}: Cost Center {2} does not belong to Company {3}").format(
|
||||
self.voucher_type, self.voucher_no, self.cost_center, self.company
|
||||
)
|
||||
)
|
||||
|
||||
if (self.voucher_type != 'Period Closing Voucher' and is_group):
|
||||
frappe.throw(_("""{0} {1}: Cost Center {2} is a group cost center and group cost centers cannot be used in transactions""").format(
|
||||
self.voucher_type, self.voucher_no, frappe.bold(self.cost_center)))
|
||||
if self.voucher_type != "Period Closing Voucher" and is_group:
|
||||
frappe.throw(
|
||||
_(
|
||||
"""{0} {1}: Cost Center {2} is a group cost center and group cost centers cannot be used in transactions"""
|
||||
).format(self.voucher_type, self.voucher_no, frappe.bold(self.cost_center))
|
||||
)
|
||||
|
||||
def validate_party(self):
|
||||
validate_party_frozen_disabled(self.party_type, self.party)
|
||||
@ -181,9 +259,12 @@ class GLEntry(Document):
|
||||
self.account_currency = account_currency or company_currency
|
||||
|
||||
if account_currency != self.account_currency:
|
||||
frappe.throw(_("{0} {1}: Accounting Entry for {2} can only be made in currency: {3}")
|
||||
.format(self.voucher_type, self.voucher_no, self.account,
|
||||
(account_currency or company_currency)), InvalidAccountCurrency)
|
||||
frappe.throw(
|
||||
_("{0} {1}: Accounting Entry for {2} can only be made in currency: {3}").format(
|
||||
self.voucher_type, self.voucher_no, self.account, (account_currency or company_currency)
|
||||
),
|
||||
InvalidAccountCurrency,
|
||||
)
|
||||
|
||||
if self.party_type and self.party:
|
||||
validate_party_gle_currency(self.party_type, self.party, self.company, self.account_currency)
|
||||
@ -192,51 +273,85 @@ class GLEntry(Document):
|
||||
if not self.fiscal_year:
|
||||
self.fiscal_year = get_fiscal_year(self.posting_date, company=self.company)[0]
|
||||
|
||||
def on_cancel(self):
|
||||
msg = _("Individual GL Entry cannot be cancelled.")
|
||||
msg += "<br>" + _("Please cancel related transaction.")
|
||||
frappe.throw(msg)
|
||||
|
||||
|
||||
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")
|
||||
if balance_must_be:
|
||||
balance = frappe.db.sql("""select sum(debit) - sum(credit)
|
||||
from `tabGL Entry` where account = %s""", account)[0][0]
|
||||
balance = frappe.db.sql(
|
||||
"""select sum(debit) - sum(credit)
|
||||
from `tabGL Entry` where account = %s""",
|
||||
account,
|
||||
)[0][0]
|
||||
|
||||
if (balance_must_be=="Debit" and flt(balance) < 0) or \
|
||||
(balance_must_be=="Credit" and flt(balance) > 0):
|
||||
frappe.throw(_("Balance for Account {0} must always be {1}").format(account, _(balance_must_be)))
|
||||
if (balance_must_be == "Debit" and flt(balance) < 0) or (
|
||||
balance_must_be == "Credit" and flt(balance) > 0
|
||||
):
|
||||
frappe.throw(
|
||||
_("Balance for Account {0} must always be {1}").format(account, _(balance_must_be))
|
||||
)
|
||||
|
||||
def update_outstanding_amt(account, party_type, party, against_voucher_type, against_voucher, on_cancel=False):
|
||||
|
||||
def update_outstanding_amt(
|
||||
account, party_type, party, against_voucher_type, against_voucher, on_cancel=False
|
||||
):
|
||||
if party_type and party:
|
||||
party_condition = " and party_type={0} and party={1}"\
|
||||
.format(frappe.db.escape(party_type), frappe.db.escape(party))
|
||||
party_condition = " and party_type={0} and party={1}".format(
|
||||
frappe.db.escape(party_type), frappe.db.escape(party)
|
||||
)
|
||||
else:
|
||||
party_condition = ""
|
||||
|
||||
if against_voucher_type == "Sales Invoice":
|
||||
party_account = frappe.db.get_value(against_voucher_type, against_voucher, "debit_to")
|
||||
account_condition = "and account in ({0}, {1})".format(frappe.db.escape(account), frappe.db.escape(party_account))
|
||||
account_condition = "and account in ({0}, {1})".format(
|
||||
frappe.db.escape(account), frappe.db.escape(party_account)
|
||||
)
|
||||
else:
|
||||
account_condition = " and account = {0}".format(frappe.db.escape(account))
|
||||
|
||||
# get final outstanding amt
|
||||
bal = flt(frappe.db.sql("""
|
||||
bal = flt(
|
||||
frappe.db.sql(
|
||||
"""
|
||||
select sum(debit_in_account_currency) - sum(credit_in_account_currency)
|
||||
from `tabGL Entry`
|
||||
where against_voucher_type=%s and against_voucher=%s
|
||||
and voucher_type != 'Invoice Discounting'
|
||||
{0} {1}""".format(party_condition, account_condition),
|
||||
(against_voucher_type, against_voucher))[0][0] or 0.0)
|
||||
{0} {1}""".format(
|
||||
party_condition, account_condition
|
||||
),
|
||||
(against_voucher_type, against_voucher),
|
||||
)[0][0]
|
||||
or 0.0
|
||||
)
|
||||
|
||||
if against_voucher_type == 'Purchase Invoice':
|
||||
if against_voucher_type == "Purchase Invoice":
|
||||
bal = -bal
|
||||
elif against_voucher_type == "Journal Entry":
|
||||
against_voucher_amount = flt(frappe.db.sql("""
|
||||
against_voucher_amount = flt(
|
||||
frappe.db.sql(
|
||||
"""
|
||||
select sum(debit_in_account_currency) - sum(credit_in_account_currency)
|
||||
from `tabGL Entry` where voucher_type = 'Journal Entry' and voucher_no = %s
|
||||
and account = %s and (against_voucher is null or against_voucher='') {0}"""
|
||||
.format(party_condition), (against_voucher, account))[0][0])
|
||||
and account = %s and (against_voucher is null or against_voucher='') {0}""".format(
|
||||
party_condition
|
||||
),
|
||||
(against_voucher, account),
|
||||
)[0][0]
|
||||
)
|
||||
|
||||
if not against_voucher_amount:
|
||||
frappe.throw(_("Against Journal Entry {0} is already adjusted against some other voucher")
|
||||
.format(against_voucher))
|
||||
frappe.throw(
|
||||
_("Against Journal Entry {0} is already adjusted against some other voucher").format(
|
||||
against_voucher
|
||||
)
|
||||
)
|
||||
|
||||
bal = against_voucher_amount + bal
|
||||
if against_voucher_amount < 0:
|
||||
@ -244,44 +359,51 @@ def update_outstanding_amt(account, party_type, party, against_voucher_type, aga
|
||||
|
||||
# Validation : Outstanding can not be negative for JV
|
||||
if bal < 0 and not on_cancel:
|
||||
frappe.throw(_("Outstanding for {0} cannot be less than zero ({1})").format(against_voucher, fmt_money(bal)))
|
||||
frappe.throw(
|
||||
_("Outstanding for {0} cannot be less than zero ({1})").format(against_voucher, fmt_money(bal))
|
||||
)
|
||||
|
||||
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
|
||||
ref_doc.outstanding_amount = bal
|
||||
frappe.db.set_value(against_voucher_type, against_voucher, 'outstanding_amount', bal)
|
||||
frappe.db.set_value(against_voucher_type, against_voucher, "outstanding_amount", bal)
|
||||
|
||||
ref_doc.set_status(update=True)
|
||||
|
||||
|
||||
def validate_frozen_account(account, adv_adj=None):
|
||||
frozen_account = frappe.get_cached_value("Account", account, "freeze_account")
|
||||
if frozen_account == 'Yes' and not adv_adj:
|
||||
frozen_accounts_modifier = frappe.db.get_value( 'Accounts Settings', None,
|
||||
'frozen_accounts_modifier')
|
||||
if frozen_account == "Yes" and not adv_adj:
|
||||
frozen_accounts_modifier = frappe.db.get_value(
|
||||
"Accounts Settings", None, "frozen_accounts_modifier"
|
||||
)
|
||||
|
||||
if not frozen_accounts_modifier:
|
||||
frappe.throw(_("Account {0} is frozen").format(account))
|
||||
elif frozen_accounts_modifier not in frappe.get_roles():
|
||||
frappe.throw(_("Not authorized to edit frozen Account {0}").format(account))
|
||||
|
||||
|
||||
def update_against_account(voucher_type, voucher_no):
|
||||
entries = frappe.db.get_all("GL Entry",
|
||||
entries = frappe.db.get_all(
|
||||
"GL Entry",
|
||||
filters={"voucher_type": voucher_type, "voucher_no": voucher_no},
|
||||
fields=["name", "party", "against", "debit", "credit", "account", "company"])
|
||||
fields=["name", "party", "against", "debit", "credit", "account", "company"],
|
||||
)
|
||||
|
||||
if not entries:
|
||||
return
|
||||
company_currency = erpnext.get_company_currency(entries[0].company)
|
||||
precision = get_field_precision(frappe.get_meta("GL Entry")
|
||||
.get_field("debit"), company_currency)
|
||||
precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), company_currency)
|
||||
|
||||
accounts_debited, accounts_credited = [], []
|
||||
for d in entries:
|
||||
if flt(d.debit, precision) > 0: accounts_debited.append(d.party or d.account)
|
||||
if flt(d.credit, precision) > 0: accounts_credited.append(d.party or d.account)
|
||||
if flt(d.debit, precision) > 0:
|
||||
accounts_debited.append(d.party or d.account)
|
||||
if flt(d.credit, precision) > 0:
|
||||
accounts_credited.append(d.party or d.account)
|
||||
|
||||
for d in entries:
|
||||
if flt(d.debit, precision) > 0:
|
||||
@ -292,14 +414,17 @@ def update_against_account(voucher_type, voucher_no):
|
||||
if d.against != new_against:
|
||||
frappe.db.set_value("GL Entry", d.name, "against", new_against)
|
||||
|
||||
|
||||
def on_doctype_update():
|
||||
frappe.db.add_index("GL Entry", ["against_voucher_type", "against_voucher"])
|
||||
frappe.db.add_index("GL Entry", ["voucher_type", "voucher_no"])
|
||||
|
||||
|
||||
def rename_gle_sle_docs():
|
||||
for doctype in ["GL Entry", "Stock Ledger Entry"]:
|
||||
rename_temporarily_named_docs(doctype)
|
||||
|
||||
|
||||
def rename_temporarily_named_docs(doctype):
|
||||
"""Rename temporarily named docs using autoname options"""
|
||||
docs_to_rename = frappe.get_all(doctype, {"to_rename": "1"}, order_by="creation", limit=50000)
|
||||
@ -310,5 +435,5 @@ def rename_temporarily_named_docs(doctype):
|
||||
frappe.db.sql(
|
||||
"UPDATE `tab{}` SET name = %s, to_rename = 0 where name = %s".format(doctype),
|
||||
(newname, oldname),
|
||||
auto_commit=True
|
||||
auto_commit=True,
|
||||
)
|
||||
|
||||
@ -14,48 +14,68 @@ from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journ
|
||||
class TestGLEntry(unittest.TestCase):
|
||||
def test_round_off_entry(self):
|
||||
frappe.db.set_value("Company", "_Test Company", "round_off_account", "_Test Write Off - _TC")
|
||||
frappe.db.set_value("Company", "_Test Company", "round_off_cost_center", "_Test Cost Center - _TC")
|
||||
frappe.db.set_value(
|
||||
"Company", "_Test Company", "round_off_cost_center", "_Test Cost Center - _TC"
|
||||
)
|
||||
|
||||
jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
|
||||
"_Test Bank - _TC", 100, "_Test Cost Center - _TC", submit=False)
|
||||
jv = make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC",
|
||||
"_Test Bank - _TC",
|
||||
100,
|
||||
"_Test Cost Center - _TC",
|
||||
submit=False,
|
||||
)
|
||||
|
||||
jv.get("accounts")[0].debit = 100.01
|
||||
jv.flags.ignore_validate = True
|
||||
jv.submit()
|
||||
|
||||
round_off_entry = frappe.db.sql("""select name from `tabGL Entry`
|
||||
round_off_entry = frappe.db.sql(
|
||||
"""select name from `tabGL Entry`
|
||||
where voucher_type='Journal Entry' and voucher_no = %s
|
||||
and account='_Test Write Off - _TC' and cost_center='_Test Cost Center - _TC'
|
||||
and debit = 0 and credit = '.01'""", jv.name)
|
||||
and debit = 0 and credit = '.01'""",
|
||||
jv.name,
|
||||
)
|
||||
|
||||
self.assertTrue(round_off_entry)
|
||||
|
||||
def test_rename_entries(self):
|
||||
je = make_journal_entry("_Test Account Cost for Goods Sold - _TC", "_Test Bank - _TC", 100, submit=True)
|
||||
je = make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC", "_Test Bank - _TC", 100, submit=True
|
||||
)
|
||||
rename_gle_sle_docs()
|
||||
naming_series = parse_naming_series(parts=frappe.get_meta("GL Entry").autoname.split(".")[:-1])
|
||||
|
||||
je = make_journal_entry("_Test Account Cost for Goods Sold - _TC", "_Test Bank - _TC", 100, submit=True)
|
||||
je = make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC", "_Test Bank - _TC", 100, submit=True
|
||||
)
|
||||
|
||||
gl_entries = frappe.get_all("GL Entry",
|
||||
gl_entries = frappe.get_all(
|
||||
"GL Entry",
|
||||
fields=["name", "to_rename"],
|
||||
filters={"voucher_type": "Journal Entry", "voucher_no": je.name},
|
||||
order_by="creation"
|
||||
order_by="creation",
|
||||
)
|
||||
|
||||
self.assertTrue(all(entry.to_rename == 1 for entry in gl_entries))
|
||||
old_naming_series_current_value = frappe.db.sql("SELECT current from tabSeries where name = %s", naming_series)[0][0]
|
||||
old_naming_series_current_value = frappe.db.sql(
|
||||
"SELECT current from tabSeries where name = %s", naming_series
|
||||
)[0][0]
|
||||
|
||||
rename_gle_sle_docs()
|
||||
|
||||
new_gl_entries = frappe.get_all("GL Entry",
|
||||
new_gl_entries = frappe.get_all(
|
||||
"GL Entry",
|
||||
fields=["name", "to_rename"],
|
||||
filters={"voucher_type": "Journal Entry", "voucher_no": je.name},
|
||||
order_by="creation"
|
||||
order_by="creation",
|
||||
)
|
||||
self.assertTrue(all(entry.to_rename == 0 for entry in new_gl_entries))
|
||||
|
||||
self.assertTrue(all(new.name != old.name for new, old in zip(gl_entries, new_gl_entries)))
|
||||
|
||||
new_naming_series_current_value = frappe.db.sql("SELECT current from tabSeries where name = %s", naming_series)[0][0]
|
||||
new_naming_series_current_value = frappe.db.sql(
|
||||
"SELECT current from tabSeries where name = %s", naming_series
|
||||
)[0][0]
|
||||
self.assertEqual(old_naming_series_current_value + 2, new_naming_series_current_value)
|
||||
|
||||
@ -33,19 +33,32 @@ class InvoiceDiscounting(AccountsController):
|
||||
frappe.throw(_("Loan Start Date and Loan Period are mandatory to save the Invoice Discounting"))
|
||||
|
||||
def validate_invoices(self):
|
||||
discounted_invoices = [record.sales_invoice for record in
|
||||
frappe.get_all("Discounted Invoice",fields=["sales_invoice"], filters={"docstatus":1})]
|
||||
discounted_invoices = [
|
||||
record.sales_invoice
|
||||
for record in frappe.get_all(
|
||||
"Discounted Invoice", fields=["sales_invoice"], filters={"docstatus": 1}
|
||||
)
|
||||
]
|
||||
|
||||
for record in self.invoices:
|
||||
if record.sales_invoice in discounted_invoices:
|
||||
frappe.throw(_("Row({0}): {1} is already discounted in {2}")
|
||||
.format(record.idx, frappe.bold(record.sales_invoice), frappe.bold(record.parent)))
|
||||
frappe.throw(
|
||||
_("Row({0}): {1} is already discounted in {2}").format(
|
||||
record.idx, frappe.bold(record.sales_invoice), frappe.bold(record.parent)
|
||||
)
|
||||
)
|
||||
|
||||
actual_outstanding = frappe.db.get_value("Sales Invoice", record.sales_invoice,"outstanding_amount")
|
||||
if record.outstanding_amount > actual_outstanding :
|
||||
frappe.throw(_
|
||||
("Row({0}): Outstanding Amount cannot be greater than actual Outstanding Amount {1} in {2}").format(
|
||||
record.idx, frappe.bold(actual_outstanding), frappe.bold(record.sales_invoice)))
|
||||
actual_outstanding = frappe.db.get_value(
|
||||
"Sales Invoice", record.sales_invoice, "outstanding_amount"
|
||||
)
|
||||
if record.outstanding_amount > actual_outstanding:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row({0}): Outstanding Amount cannot be greater than actual Outstanding Amount {1} in {2}"
|
||||
).format(
|
||||
record.idx, frappe.bold(actual_outstanding), frappe.bold(record.sales_invoice)
|
||||
)
|
||||
)
|
||||
|
||||
def calculate_total_amount(self):
|
||||
self.total_amount = sum(flt(d.outstanding_amount) for d in self.invoices)
|
||||
@ -73,24 +86,21 @@ class InvoiceDiscounting(AccountsController):
|
||||
self.status = "Cancelled"
|
||||
|
||||
if cancel:
|
||||
self.db_set('status', self.status, update_modified = True)
|
||||
self.db_set("status", self.status, update_modified=True)
|
||||
|
||||
def update_sales_invoice(self):
|
||||
for d in self.invoices:
|
||||
if self.docstatus == 1:
|
||||
is_discounted = 1
|
||||
else:
|
||||
discounted_invoice = frappe.db.exists({
|
||||
"doctype": "Discounted Invoice",
|
||||
"sales_invoice": d.sales_invoice,
|
||||
"docstatus": 1
|
||||
})
|
||||
discounted_invoice = frappe.db.exists(
|
||||
{"doctype": "Discounted Invoice", "sales_invoice": d.sales_invoice, "docstatus": 1}
|
||||
)
|
||||
is_discounted = 1 if discounted_invoice else 0
|
||||
frappe.db.set_value("Sales Invoice", d.sales_invoice, "is_discounted", is_discounted)
|
||||
|
||||
def make_gl_entries(self):
|
||||
company_currency = frappe.get_cached_value('Company', self.company, "default_currency")
|
||||
|
||||
company_currency = frappe.get_cached_value("Company", self.company, "default_currency")
|
||||
|
||||
gl_entries = []
|
||||
invoice_fields = ["debit_to", "party_account_currency", "conversion_rate", "cost_center"]
|
||||
@ -102,135 +112,182 @@ class InvoiceDiscounting(AccountsController):
|
||||
inv = frappe.db.get_value("Sales Invoice", d.sales_invoice, invoice_fields, as_dict=1)
|
||||
|
||||
if d.outstanding_amount:
|
||||
outstanding_in_company_currency = flt(d.outstanding_amount * inv.conversion_rate,
|
||||
d.precision("outstanding_amount"))
|
||||
ar_credit_account_currency = frappe.get_cached_value("Account", self.accounts_receivable_credit, "currency")
|
||||
outstanding_in_company_currency = flt(
|
||||
d.outstanding_amount * inv.conversion_rate, d.precision("outstanding_amount")
|
||||
)
|
||||
ar_credit_account_currency = frappe.get_cached_value(
|
||||
"Account", self.accounts_receivable_credit, "currency"
|
||||
)
|
||||
|
||||
gl_entries.append(self.get_gl_dict({
|
||||
"account": inv.debit_to,
|
||||
"party_type": "Customer",
|
||||
"party": d.customer,
|
||||
"against": self.accounts_receivable_credit,
|
||||
"credit": outstanding_in_company_currency,
|
||||
"credit_in_account_currency": outstanding_in_company_currency \
|
||||
if inv.party_account_currency==company_currency else d.outstanding_amount,
|
||||
"cost_center": inv.cost_center,
|
||||
"against_voucher": d.sales_invoice,
|
||||
"against_voucher_type": "Sales Invoice"
|
||||
}, inv.party_account_currency, item=inv))
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": inv.debit_to,
|
||||
"party_type": "Customer",
|
||||
"party": d.customer,
|
||||
"against": self.accounts_receivable_credit,
|
||||
"credit": outstanding_in_company_currency,
|
||||
"credit_in_account_currency": outstanding_in_company_currency
|
||||
if inv.party_account_currency == company_currency
|
||||
else d.outstanding_amount,
|
||||
"cost_center": inv.cost_center,
|
||||
"against_voucher": d.sales_invoice,
|
||||
"against_voucher_type": "Sales Invoice",
|
||||
},
|
||||
inv.party_account_currency,
|
||||
item=inv,
|
||||
)
|
||||
)
|
||||
|
||||
gl_entries.append(self.get_gl_dict({
|
||||
"account": self.accounts_receivable_credit,
|
||||
"party_type": "Customer",
|
||||
"party": d.customer,
|
||||
"against": inv.debit_to,
|
||||
"debit": outstanding_in_company_currency,
|
||||
"debit_in_account_currency": outstanding_in_company_currency \
|
||||
if ar_credit_account_currency==company_currency else d.outstanding_amount,
|
||||
"cost_center": inv.cost_center,
|
||||
"against_voucher": d.sales_invoice,
|
||||
"against_voucher_type": "Sales Invoice"
|
||||
}, ar_credit_account_currency, item=inv))
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": self.accounts_receivable_credit,
|
||||
"party_type": "Customer",
|
||||
"party": d.customer,
|
||||
"against": inv.debit_to,
|
||||
"debit": outstanding_in_company_currency,
|
||||
"debit_in_account_currency": outstanding_in_company_currency
|
||||
if ar_credit_account_currency == company_currency
|
||||
else d.outstanding_amount,
|
||||
"cost_center": inv.cost_center,
|
||||
"against_voucher": d.sales_invoice,
|
||||
"against_voucher_type": "Sales Invoice",
|
||||
},
|
||||
ar_credit_account_currency,
|
||||
item=inv,
|
||||
)
|
||||
)
|
||||
|
||||
make_gl_entries(gl_entries, cancel=(self.docstatus == 2), update_outstanding='No')
|
||||
make_gl_entries(gl_entries, cancel=(self.docstatus == 2), update_outstanding="No")
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_disbursement_entry(self):
|
||||
je = frappe.new_doc("Journal Entry")
|
||||
je.voucher_type = 'Journal Entry'
|
||||
je.voucher_type = "Journal Entry"
|
||||
je.company = self.company
|
||||
je.remark = 'Loan Disbursement entry against Invoice Discounting: ' + self.name
|
||||
je.remark = "Loan Disbursement entry against Invoice Discounting: " + self.name
|
||||
|
||||
je.append("accounts", {
|
||||
"account": self.bank_account,
|
||||
"debit_in_account_currency": flt(self.total_amount) - flt(self.bank_charges),
|
||||
"cost_center": erpnext.get_default_cost_center(self.company)
|
||||
})
|
||||
je.append(
|
||||
"accounts",
|
||||
{
|
||||
"account": self.bank_account,
|
||||
"debit_in_account_currency": flt(self.total_amount) - flt(self.bank_charges),
|
||||
"cost_center": erpnext.get_default_cost_center(self.company),
|
||||
},
|
||||
)
|
||||
|
||||
if self.bank_charges:
|
||||
je.append("accounts", {
|
||||
"account": self.bank_charges_account,
|
||||
"debit_in_account_currency": flt(self.bank_charges),
|
||||
"cost_center": erpnext.get_default_cost_center(self.company)
|
||||
})
|
||||
je.append(
|
||||
"accounts",
|
||||
{
|
||||
"account": self.bank_charges_account,
|
||||
"debit_in_account_currency": flt(self.bank_charges),
|
||||
"cost_center": erpnext.get_default_cost_center(self.company),
|
||||
},
|
||||
)
|
||||
|
||||
je.append("accounts", {
|
||||
"account": self.short_term_loan,
|
||||
"credit_in_account_currency": flt(self.total_amount),
|
||||
"cost_center": erpnext.get_default_cost_center(self.company),
|
||||
"reference_type": "Invoice Discounting",
|
||||
"reference_name": self.name
|
||||
})
|
||||
je.append(
|
||||
"accounts",
|
||||
{
|
||||
"account": self.short_term_loan,
|
||||
"credit_in_account_currency": flt(self.total_amount),
|
||||
"cost_center": erpnext.get_default_cost_center(self.company),
|
||||
"reference_type": "Invoice Discounting",
|
||||
"reference_name": self.name,
|
||||
},
|
||||
)
|
||||
for d in self.invoices:
|
||||
je.append("accounts", {
|
||||
"account": self.accounts_receivable_discounted,
|
||||
"debit_in_account_currency": flt(d.outstanding_amount),
|
||||
"cost_center": erpnext.get_default_cost_center(self.company),
|
||||
"reference_type": "Invoice Discounting",
|
||||
"reference_name": self.name,
|
||||
"party_type": "Customer",
|
||||
"party": d.customer
|
||||
})
|
||||
je.append(
|
||||
"accounts",
|
||||
{
|
||||
"account": self.accounts_receivable_discounted,
|
||||
"debit_in_account_currency": flt(d.outstanding_amount),
|
||||
"cost_center": erpnext.get_default_cost_center(self.company),
|
||||
"reference_type": "Invoice Discounting",
|
||||
"reference_name": self.name,
|
||||
"party_type": "Customer",
|
||||
"party": d.customer,
|
||||
},
|
||||
)
|
||||
|
||||
je.append("accounts", {
|
||||
"account": self.accounts_receivable_credit,
|
||||
"credit_in_account_currency": flt(d.outstanding_amount),
|
||||
"cost_center": erpnext.get_default_cost_center(self.company),
|
||||
"reference_type": "Invoice Discounting",
|
||||
"reference_name": self.name,
|
||||
"party_type": "Customer",
|
||||
"party": d.customer
|
||||
})
|
||||
je.append(
|
||||
"accounts",
|
||||
{
|
||||
"account": self.accounts_receivable_credit,
|
||||
"credit_in_account_currency": flt(d.outstanding_amount),
|
||||
"cost_center": erpnext.get_default_cost_center(self.company),
|
||||
"reference_type": "Invoice Discounting",
|
||||
"reference_name": self.name,
|
||||
"party_type": "Customer",
|
||||
"party": d.customer,
|
||||
},
|
||||
)
|
||||
|
||||
return je
|
||||
|
||||
@frappe.whitelist()
|
||||
def close_loan(self):
|
||||
je = frappe.new_doc("Journal Entry")
|
||||
je.voucher_type = 'Journal Entry'
|
||||
je.voucher_type = "Journal Entry"
|
||||
je.company = self.company
|
||||
je.remark = 'Loan Settlement entry against Invoice Discounting: ' + self.name
|
||||
je.remark = "Loan Settlement entry against Invoice Discounting: " + self.name
|
||||
|
||||
je.append("accounts", {
|
||||
"account": self.short_term_loan,
|
||||
"debit_in_account_currency": flt(self.total_amount),
|
||||
"cost_center": erpnext.get_default_cost_center(self.company),
|
||||
"reference_type": "Invoice Discounting",
|
||||
"reference_name": self.name,
|
||||
})
|
||||
je.append(
|
||||
"accounts",
|
||||
{
|
||||
"account": self.short_term_loan,
|
||||
"debit_in_account_currency": flt(self.total_amount),
|
||||
"cost_center": erpnext.get_default_cost_center(self.company),
|
||||
"reference_type": "Invoice Discounting",
|
||||
"reference_name": self.name,
|
||||
},
|
||||
)
|
||||
|
||||
je.append("accounts", {
|
||||
"account": self.bank_account,
|
||||
"credit_in_account_currency": flt(self.total_amount),
|
||||
"cost_center": erpnext.get_default_cost_center(self.company)
|
||||
})
|
||||
je.append(
|
||||
"accounts",
|
||||
{
|
||||
"account": self.bank_account,
|
||||
"credit_in_account_currency": flt(self.total_amount),
|
||||
"cost_center": erpnext.get_default_cost_center(self.company),
|
||||
},
|
||||
)
|
||||
|
||||
if getdate(self.loan_end_date) > getdate(nowdate()):
|
||||
for d in self.invoices:
|
||||
outstanding_amount = frappe.db.get_value("Sales Invoice", d.sales_invoice, "outstanding_amount")
|
||||
outstanding_amount = frappe.db.get_value(
|
||||
"Sales Invoice", d.sales_invoice, "outstanding_amount"
|
||||
)
|
||||
if flt(outstanding_amount) > 0:
|
||||
je.append("accounts", {
|
||||
"account": self.accounts_receivable_discounted,
|
||||
"credit_in_account_currency": flt(outstanding_amount),
|
||||
"cost_center": erpnext.get_default_cost_center(self.company),
|
||||
"reference_type": "Invoice Discounting",
|
||||
"reference_name": self.name,
|
||||
"party_type": "Customer",
|
||||
"party": d.customer
|
||||
})
|
||||
je.append(
|
||||
"accounts",
|
||||
{
|
||||
"account": self.accounts_receivable_discounted,
|
||||
"credit_in_account_currency": flt(outstanding_amount),
|
||||
"cost_center": erpnext.get_default_cost_center(self.company),
|
||||
"reference_type": "Invoice Discounting",
|
||||
"reference_name": self.name,
|
||||
"party_type": "Customer",
|
||||
"party": d.customer,
|
||||
},
|
||||
)
|
||||
|
||||
je.append("accounts", {
|
||||
"account": self.accounts_receivable_unpaid,
|
||||
"debit_in_account_currency": flt(outstanding_amount),
|
||||
"cost_center": erpnext.get_default_cost_center(self.company),
|
||||
"reference_type": "Invoice Discounting",
|
||||
"reference_name": self.name,
|
||||
"party_type": "Customer",
|
||||
"party": d.customer
|
||||
})
|
||||
je.append(
|
||||
"accounts",
|
||||
{
|
||||
"account": self.accounts_receivable_unpaid,
|
||||
"debit_in_account_currency": flt(outstanding_amount),
|
||||
"cost_center": erpnext.get_default_cost_center(self.company),
|
||||
"reference_type": "Invoice Discounting",
|
||||
"reference_name": self.name,
|
||||
"party_type": "Customer",
|
||||
"party": d.customer,
|
||||
},
|
||||
)
|
||||
|
||||
return je
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_invoices(filters):
|
||||
filters = frappe._dict(json.loads(filters))
|
||||
@ -250,7 +307,8 @@ def get_invoices(filters):
|
||||
if cond:
|
||||
where_condition += " and " + " and ".join(cond)
|
||||
|
||||
return frappe.db.sql("""
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
name as sales_invoice,
|
||||
customer,
|
||||
@ -264,17 +322,26 @@ def get_invoices(filters):
|
||||
%s
|
||||
and not exists(select di.name from `tabDiscounted Invoice` di
|
||||
where di.docstatus=1 and di.sales_invoice=si.name)
|
||||
""" % where_condition, filters, as_dict=1)
|
||||
"""
|
||||
% where_condition,
|
||||
filters,
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
|
||||
def get_party_account_based_on_invoice_discounting(sales_invoice):
|
||||
party_account = None
|
||||
invoice_discounting = frappe.db.sql("""
|
||||
invoice_discounting = frappe.db.sql(
|
||||
"""
|
||||
select par.accounts_receivable_discounted, par.accounts_receivable_unpaid, par.status
|
||||
from `tabInvoice Discounting` par, `tabDiscounted Invoice` ch
|
||||
where par.name=ch.parent
|
||||
and par.docstatus=1
|
||||
and ch.sales_invoice = %s
|
||||
""", (sales_invoice), as_dict=1)
|
||||
""",
|
||||
(sales_invoice),
|
||||
as_dict=1,
|
||||
)
|
||||
if invoice_discounting:
|
||||
if invoice_discounting[0].status == "Disbursed":
|
||||
party_account = invoice_discounting[0].accounts_receivable_discounted
|
||||
|
||||
@ -3,18 +3,10 @@ from frappe import _
|
||||
|
||||
def get_data():
|
||||
return {
|
||||
'fieldname': 'reference_name',
|
||||
'internal_links': {
|
||||
'Sales Invoice': ['invoices', 'sales_invoice']
|
||||
},
|
||||
'transactions': [
|
||||
{
|
||||
'label': _('Reference'),
|
||||
'items': ['Sales Invoice']
|
||||
},
|
||||
{
|
||||
'label': _('Payment'),
|
||||
'items': ['Payment Entry', 'Journal Entry']
|
||||
}
|
||||
]
|
||||
"fieldname": "reference_name",
|
||||
"internal_links": {"Sales Invoice": ["invoices", "sales_invoice"]},
|
||||
"transactions": [
|
||||
{"label": _("Reference"), "items": ["Sales Invoice"]},
|
||||
{"label": _("Payment"), "items": ["Payment Entry", "Journal Entry"]},
|
||||
],
|
||||
}
|
||||
|
||||
@ -14,52 +14,74 @@ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_
|
||||
|
||||
class TestInvoiceDiscounting(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.ar_credit = create_account(account_name="_Test Accounts Receivable Credit", parent_account = "Accounts Receivable - _TC", company="_Test Company")
|
||||
self.ar_discounted = create_account(account_name="_Test Accounts Receivable Discounted", parent_account = "Accounts Receivable - _TC", company="_Test Company")
|
||||
self.ar_unpaid = create_account(account_name="_Test Accounts Receivable Unpaid", parent_account = "Accounts Receivable - _TC", company="_Test Company")
|
||||
self.short_term_loan = create_account(account_name="_Test Short Term Loan", parent_account = "Source of Funds (Liabilities) - _TC", company="_Test Company")
|
||||
self.bank_account = create_account(account_name="_Test Bank 2", parent_account = "Bank Accounts - _TC", company="_Test Company")
|
||||
self.bank_charges_account = create_account(account_name="_Test Bank Charges Account", parent_account = "Expenses - _TC", company="_Test Company")
|
||||
self.ar_credit = create_account(
|
||||
account_name="_Test Accounts Receivable Credit",
|
||||
parent_account="Accounts Receivable - _TC",
|
||||
company="_Test Company",
|
||||
)
|
||||
self.ar_discounted = create_account(
|
||||
account_name="_Test Accounts Receivable Discounted",
|
||||
parent_account="Accounts Receivable - _TC",
|
||||
company="_Test Company",
|
||||
)
|
||||
self.ar_unpaid = create_account(
|
||||
account_name="_Test Accounts Receivable Unpaid",
|
||||
parent_account="Accounts Receivable - _TC",
|
||||
company="_Test Company",
|
||||
)
|
||||
self.short_term_loan = create_account(
|
||||
account_name="_Test Short Term Loan",
|
||||
parent_account="Source of Funds (Liabilities) - _TC",
|
||||
company="_Test Company",
|
||||
)
|
||||
self.bank_account = create_account(
|
||||
account_name="_Test Bank 2", parent_account="Bank Accounts - _TC", company="_Test Company"
|
||||
)
|
||||
self.bank_charges_account = create_account(
|
||||
account_name="_Test Bank Charges Account",
|
||||
parent_account="Expenses - _TC",
|
||||
company="_Test Company",
|
||||
)
|
||||
frappe.db.set_value("Company", "_Test Company", "default_bank_account", self.bank_account)
|
||||
|
||||
def test_total_amount(self):
|
||||
inv1 = create_sales_invoice(rate=200)
|
||||
inv2 = create_sales_invoice(rate=500)
|
||||
|
||||
inv_disc = create_invoice_discounting([inv1.name, inv2.name],
|
||||
inv_disc = create_invoice_discounting(
|
||||
[inv1.name, inv2.name],
|
||||
do_not_submit=True,
|
||||
accounts_receivable_credit=self.ar_credit,
|
||||
accounts_receivable_discounted=self.ar_discounted,
|
||||
accounts_receivable_unpaid=self.ar_unpaid,
|
||||
short_term_loan=self.short_term_loan,
|
||||
bank_charges_account=self.bank_charges_account,
|
||||
bank_account=self.bank_account
|
||||
)
|
||||
bank_account=self.bank_account,
|
||||
)
|
||||
self.assertEqual(inv_disc.total_amount, 700)
|
||||
|
||||
def test_gl_entries_in_base_currency(self):
|
||||
inv = create_sales_invoice(rate=200)
|
||||
inv_disc = create_invoice_discounting([inv.name],
|
||||
inv_disc = create_invoice_discounting(
|
||||
[inv.name],
|
||||
accounts_receivable_credit=self.ar_credit,
|
||||
accounts_receivable_discounted=self.ar_discounted,
|
||||
accounts_receivable_unpaid=self.ar_unpaid,
|
||||
short_term_loan=self.short_term_loan,
|
||||
bank_charges_account=self.bank_charges_account,
|
||||
bank_account=self.bank_account
|
||||
)
|
||||
bank_account=self.bank_account,
|
||||
)
|
||||
|
||||
gle = get_gl_entries("Invoice Discounting", inv_disc.name)
|
||||
|
||||
expected_gle = {
|
||||
inv.debit_to: [0.0, 200],
|
||||
self.ar_credit: [200, 0.0]
|
||||
}
|
||||
expected_gle = {inv.debit_to: [0.0, 200], self.ar_credit: [200, 0.0]}
|
||||
for i, gle in enumerate(gle):
|
||||
self.assertEqual([gle.debit, gle.credit], expected_gle.get(gle.account))
|
||||
|
||||
def test_loan_on_submit(self):
|
||||
inv = create_sales_invoice(rate=300)
|
||||
inv_disc = create_invoice_discounting([inv.name],
|
||||
inv_disc = create_invoice_discounting(
|
||||
[inv.name],
|
||||
accounts_receivable_credit=self.ar_credit,
|
||||
accounts_receivable_discounted=self.ar_discounted,
|
||||
accounts_receivable_unpaid=self.ar_unpaid,
|
||||
@ -67,28 +89,33 @@ class TestInvoiceDiscounting(unittest.TestCase):
|
||||
bank_charges_account=self.bank_charges_account,
|
||||
bank_account=self.bank_account,
|
||||
start=nowdate(),
|
||||
period=60
|
||||
)
|
||||
period=60,
|
||||
)
|
||||
self.assertEqual(inv_disc.status, "Sanctioned")
|
||||
self.assertEqual(inv_disc.loan_end_date, add_days(inv_disc.loan_start_date, inv_disc.loan_period))
|
||||
|
||||
self.assertEqual(
|
||||
inv_disc.loan_end_date, add_days(inv_disc.loan_start_date, inv_disc.loan_period)
|
||||
)
|
||||
|
||||
def test_on_disbursed(self):
|
||||
inv = create_sales_invoice(rate=500)
|
||||
inv_disc = create_invoice_discounting([inv.name],
|
||||
inv_disc = create_invoice_discounting(
|
||||
[inv.name],
|
||||
accounts_receivable_credit=self.ar_credit,
|
||||
accounts_receivable_discounted=self.ar_discounted,
|
||||
accounts_receivable_unpaid=self.ar_unpaid,
|
||||
short_term_loan=self.short_term_loan,
|
||||
bank_charges_account=self.bank_charges_account,
|
||||
bank_account=self.bank_account,
|
||||
bank_charges=100
|
||||
)
|
||||
bank_charges=100,
|
||||
)
|
||||
|
||||
je = inv_disc.create_disbursement_entry()
|
||||
|
||||
self.assertEqual(je.accounts[0].account, self.bank_account)
|
||||
self.assertEqual(je.accounts[0].debit_in_account_currency, flt(inv_disc.total_amount) - flt(inv_disc.bank_charges))
|
||||
self.assertEqual(
|
||||
je.accounts[0].debit_in_account_currency,
|
||||
flt(inv_disc.total_amount) - flt(inv_disc.bank_charges),
|
||||
)
|
||||
|
||||
self.assertEqual(je.accounts[1].account, self.bank_charges_account)
|
||||
self.assertEqual(je.accounts[1].debit_in_account_currency, flt(inv_disc.bank_charges))
|
||||
@ -102,7 +129,6 @@ class TestInvoiceDiscounting(unittest.TestCase):
|
||||
self.assertEqual(je.accounts[4].account, self.ar_credit)
|
||||
self.assertEqual(je.accounts[4].credit_in_account_currency, flt(inv.outstanding_amount))
|
||||
|
||||
|
||||
je.posting_date = nowdate()
|
||||
je.submit()
|
||||
|
||||
@ -114,7 +140,8 @@ class TestInvoiceDiscounting(unittest.TestCase):
|
||||
|
||||
def test_on_close_after_loan_period(self):
|
||||
inv = create_sales_invoice(rate=600)
|
||||
inv_disc = create_invoice_discounting([inv.name],
|
||||
inv_disc = create_invoice_discounting(
|
||||
[inv.name],
|
||||
accounts_receivable_credit=self.ar_credit,
|
||||
accounts_receivable_discounted=self.ar_discounted,
|
||||
accounts_receivable_unpaid=self.ar_unpaid,
|
||||
@ -122,8 +149,8 @@ class TestInvoiceDiscounting(unittest.TestCase):
|
||||
bank_charges_account=self.bank_charges_account,
|
||||
bank_account=self.bank_account,
|
||||
start=nowdate(),
|
||||
period=60
|
||||
)
|
||||
period=60,
|
||||
)
|
||||
|
||||
je1 = inv_disc.create_disbursement_entry()
|
||||
je1.posting_date = nowdate()
|
||||
@ -151,7 +178,8 @@ class TestInvoiceDiscounting(unittest.TestCase):
|
||||
|
||||
def test_on_close_after_loan_period_after_inv_payment(self):
|
||||
inv = create_sales_invoice(rate=600)
|
||||
inv_disc = create_invoice_discounting([inv.name],
|
||||
inv_disc = create_invoice_discounting(
|
||||
[inv.name],
|
||||
accounts_receivable_credit=self.ar_credit,
|
||||
accounts_receivable_discounted=self.ar_discounted,
|
||||
accounts_receivable_unpaid=self.ar_unpaid,
|
||||
@ -159,8 +187,8 @@ class TestInvoiceDiscounting(unittest.TestCase):
|
||||
bank_charges_account=self.bank_charges_account,
|
||||
bank_account=self.bank_account,
|
||||
start=nowdate(),
|
||||
period=60
|
||||
)
|
||||
period=60,
|
||||
)
|
||||
|
||||
je1 = inv_disc.create_disbursement_entry()
|
||||
je1.posting_date = nowdate()
|
||||
@ -183,7 +211,8 @@ class TestInvoiceDiscounting(unittest.TestCase):
|
||||
|
||||
def test_on_close_before_loan_period(self):
|
||||
inv = create_sales_invoice(rate=700)
|
||||
inv_disc = create_invoice_discounting([inv.name],
|
||||
inv_disc = create_invoice_discounting(
|
||||
[inv.name],
|
||||
accounts_receivable_credit=self.ar_credit,
|
||||
accounts_receivable_discounted=self.ar_discounted,
|
||||
accounts_receivable_unpaid=self.ar_unpaid,
|
||||
@ -191,7 +220,7 @@ class TestInvoiceDiscounting(unittest.TestCase):
|
||||
bank_charges_account=self.bank_charges_account,
|
||||
bank_account=self.bank_account,
|
||||
start=add_days(nowdate(), -80),
|
||||
period=60
|
||||
period=60,
|
||||
)
|
||||
|
||||
je1 = inv_disc.create_disbursement_entry()
|
||||
@ -209,16 +238,17 @@ class TestInvoiceDiscounting(unittest.TestCase):
|
||||
self.assertEqual(je2.accounts[1].credit_in_account_currency, flt(inv_disc.total_amount))
|
||||
|
||||
def test_make_payment_before_loan_period(self):
|
||||
#it has problem
|
||||
# it has problem
|
||||
inv = create_sales_invoice(rate=700)
|
||||
inv_disc = create_invoice_discounting([inv.name],
|
||||
accounts_receivable_credit=self.ar_credit,
|
||||
accounts_receivable_discounted=self.ar_discounted,
|
||||
accounts_receivable_unpaid=self.ar_unpaid,
|
||||
short_term_loan=self.short_term_loan,
|
||||
bank_charges_account=self.bank_charges_account,
|
||||
bank_account=self.bank_account
|
||||
)
|
||||
inv_disc = create_invoice_discounting(
|
||||
[inv.name],
|
||||
accounts_receivable_credit=self.ar_credit,
|
||||
accounts_receivable_discounted=self.ar_discounted,
|
||||
accounts_receivable_unpaid=self.ar_unpaid,
|
||||
short_term_loan=self.short_term_loan,
|
||||
bank_charges_account=self.bank_charges_account,
|
||||
bank_account=self.bank_account,
|
||||
)
|
||||
je = inv_disc.create_disbursement_entry()
|
||||
inv_disc.reload()
|
||||
je.posting_date = nowdate()
|
||||
@ -232,26 +262,31 @@ class TestInvoiceDiscounting(unittest.TestCase):
|
||||
je_on_payment.submit()
|
||||
|
||||
self.assertEqual(je_on_payment.accounts[0].account, self.ar_discounted)
|
||||
self.assertEqual(je_on_payment.accounts[0].credit_in_account_currency, flt(inv.outstanding_amount))
|
||||
self.assertEqual(
|
||||
je_on_payment.accounts[0].credit_in_account_currency, flt(inv.outstanding_amount)
|
||||
)
|
||||
self.assertEqual(je_on_payment.accounts[1].account, self.bank_account)
|
||||
self.assertEqual(je_on_payment.accounts[1].debit_in_account_currency, flt(inv.outstanding_amount))
|
||||
self.assertEqual(
|
||||
je_on_payment.accounts[1].debit_in_account_currency, flt(inv.outstanding_amount)
|
||||
)
|
||||
|
||||
inv.reload()
|
||||
self.assertEqual(inv.outstanding_amount, 0)
|
||||
|
||||
def test_make_payment_before_after_period(self):
|
||||
#it has problem
|
||||
# it has problem
|
||||
inv = create_sales_invoice(rate=700)
|
||||
inv_disc = create_invoice_discounting([inv.name],
|
||||
accounts_receivable_credit=self.ar_credit,
|
||||
accounts_receivable_discounted=self.ar_discounted,
|
||||
accounts_receivable_unpaid=self.ar_unpaid,
|
||||
short_term_loan=self.short_term_loan,
|
||||
bank_charges_account=self.bank_charges_account,
|
||||
bank_account=self.bank_account,
|
||||
loan_start_date=add_days(nowdate(), -10),
|
||||
period=5
|
||||
)
|
||||
inv_disc = create_invoice_discounting(
|
||||
[inv.name],
|
||||
accounts_receivable_credit=self.ar_credit,
|
||||
accounts_receivable_discounted=self.ar_discounted,
|
||||
accounts_receivable_unpaid=self.ar_unpaid,
|
||||
short_term_loan=self.short_term_loan,
|
||||
bank_charges_account=self.bank_charges_account,
|
||||
bank_account=self.bank_account,
|
||||
loan_start_date=add_days(nowdate(), -10),
|
||||
period=5,
|
||||
)
|
||||
je = inv_disc.create_disbursement_entry()
|
||||
inv_disc.reload()
|
||||
je.posting_date = nowdate()
|
||||
@ -269,9 +304,13 @@ class TestInvoiceDiscounting(unittest.TestCase):
|
||||
je_on_payment.submit()
|
||||
|
||||
self.assertEqual(je_on_payment.accounts[0].account, self.ar_unpaid)
|
||||
self.assertEqual(je_on_payment.accounts[0].credit_in_account_currency, flt(inv.outstanding_amount))
|
||||
self.assertEqual(
|
||||
je_on_payment.accounts[0].credit_in_account_currency, flt(inv.outstanding_amount)
|
||||
)
|
||||
self.assertEqual(je_on_payment.accounts[1].account, self.bank_account)
|
||||
self.assertEqual(je_on_payment.accounts[1].debit_in_account_currency, flt(inv.outstanding_amount))
|
||||
self.assertEqual(
|
||||
je_on_payment.accounts[1].debit_in_account_currency, flt(inv.outstanding_amount)
|
||||
)
|
||||
|
||||
inv.reload()
|
||||
self.assertEqual(inv.outstanding_amount, 0)
|
||||
@ -287,17 +326,15 @@ def create_invoice_discounting(invoices, **args):
|
||||
inv_disc.accounts_receivable_credit = args.accounts_receivable_credit
|
||||
inv_disc.accounts_receivable_discounted = args.accounts_receivable_discounted
|
||||
inv_disc.accounts_receivable_unpaid = args.accounts_receivable_unpaid
|
||||
inv_disc.short_term_loan=args.short_term_loan
|
||||
inv_disc.bank_charges_account=args.bank_charges_account
|
||||
inv_disc.bank_account=args.bank_account
|
||||
inv_disc.short_term_loan = args.short_term_loan
|
||||
inv_disc.bank_charges_account = args.bank_charges_account
|
||||
inv_disc.bank_account = args.bank_account
|
||||
inv_disc.loan_start_date = args.start or nowdate()
|
||||
inv_disc.loan_period = args.period or 30
|
||||
inv_disc.bank_charges = flt(args.bank_charges)
|
||||
|
||||
for d in invoices:
|
||||
inv_disc.append("invoices", {
|
||||
"sales_invoice": d
|
||||
})
|
||||
inv_disc.append("invoices", {"sales_invoice": d})
|
||||
inv_disc.insert()
|
||||
|
||||
if not args.do_not_submit:
|
||||
|
||||
@ -13,20 +13,28 @@ class ItemTaxTemplate(Document):
|
||||
|
||||
def autoname(self):
|
||||
if self.company and self.title:
|
||||
abbr = frappe.get_cached_value('Company', self.company, 'abbr')
|
||||
self.name = '{0} - {1}'.format(self.title, abbr)
|
||||
abbr = frappe.get_cached_value("Company", self.company, "abbr")
|
||||
self.name = "{0} - {1}".format(self.title, abbr)
|
||||
|
||||
def validate_tax_accounts(self):
|
||||
"""Check whether Tax Rate is not entered twice for same Tax Type"""
|
||||
check_list = []
|
||||
for d in self.get('taxes'):
|
||||
for d in self.get("taxes"):
|
||||
if d.tax_type:
|
||||
account_type = frappe.db.get_value("Account", d.tax_type, "account_type")
|
||||
|
||||
if account_type not in ['Tax', 'Chargeable', 'Income Account', 'Expense Account', 'Expenses Included In Valuation']:
|
||||
if account_type not in [
|
||||
"Tax",
|
||||
"Chargeable",
|
||||
"Income Account",
|
||||
"Expense Account",
|
||||
"Expenses Included In Valuation",
|
||||
]:
|
||||
frappe.throw(
|
||||
_("Item Tax Row {0} must have account of type Tax or Income or Expense or Chargeable").format(
|
||||
d.idx))
|
||||
_(
|
||||
"Item Tax Row {0} must have account of type Tax or Income or Expense or Chargeable"
|
||||
).format(d.idx)
|
||||
)
|
||||
else:
|
||||
if d.tax_type in check_list:
|
||||
frappe.throw(_("{0} entered twice in Item Tax").format(d.tax_type))
|
||||
|
||||
@ -3,23 +3,11 @@ from frappe import _
|
||||
|
||||
def get_data():
|
||||
return {
|
||||
'fieldname': 'item_tax_template',
|
||||
'transactions': [
|
||||
{
|
||||
'label': _('Pre Sales'),
|
||||
'items': ['Quotation', 'Supplier Quotation']
|
||||
},
|
||||
{
|
||||
'label': _('Sales'),
|
||||
'items': ['Sales Invoice', 'Sales Order', 'Delivery Note']
|
||||
},
|
||||
{
|
||||
'label': _('Purchase'),
|
||||
'items': ['Purchase Invoice', 'Purchase Order', 'Purchase Receipt']
|
||||
},
|
||||
{
|
||||
'label': _('Stock'),
|
||||
'items': ['Item Groups', 'Item']
|
||||
}
|
||||
]
|
||||
"fieldname": "item_tax_template",
|
||||
"transactions": [
|
||||
{"label": _("Pre Sales"), "items": ["Quotation", "Supplier Quotation"]},
|
||||
{"label": _("Sales"), "items": ["Sales Invoice", "Sales Order", "Delivery Note"]},
|
||||
{"label": _("Purchase"), "items": ["Purchase Invoice", "Purchase Order", "Purchase Receipt"]},
|
||||
{"label": _("Stock"), "items": ["Item Groups", "Item"]},
|
||||
],
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user