Compare commits

..

1 Commits

Author SHA1 Message Date
Rohit Waghchaure
b141be9b9b bumped to version 14.0.0-beta.1 2022-02-04 23:21:09 +05:30
3307 changed files with 248388 additions and 136789 deletions

View File

@@ -5,7 +5,7 @@
"es6": true "es6": true
}, },
"parserOptions": { "parserOptions": {
"ecmaVersion": 11, "ecmaVersion": 9,
"sourceType": "module" "sourceType": "module"
}, },
"extends": "eslint:recommended", "extends": "eslint:recommended",

View File

@@ -29,9 +29,6 @@ ignore =
B950, B950,
W191, W191,
E124, # closing bracket, irritating while writing QB code 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 max-line-length = 200
exclude=.github/helper/semgrep_rules exclude=.github/helper/semgrep_rules

View File

@@ -23,9 +23,3 @@ b147b85e6ac19a9220cd1e2958a6ebd99373283a
# removing six compatibility layer # removing six compatibility layer
8fe5feb6a4372bf5f2dfaf65fca41bbcc25c8ce7 8fe5feb6a4372bf5f2dfaf65fca41bbcc25c8ce7
# bulk format python code with black
494bd9ef78313436f0424b918f200dab8fc7c20b
# bulk format python code with black
baec607ff5905b1c67531096a9cf50ec7ff00a5d

View File

@@ -12,18 +12,10 @@ 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 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 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 2. Use the search function before creating a new issue. Duplicates will be closed and directed to
the original discussion. 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.** **Is your feature request related to a problem? Please describe.**

View File

@@ -66,7 +66,6 @@ ignore =
F841, F841,
E713, E713,
E712, E712,
B023
max-line-length = 200 max-line-length = 200

View File

@@ -4,7 +4,7 @@ set -e
cd ~ || exit cd ~ || exit
sudo apt update && sudo apt install redis-server libcups2-dev sudo apt-get install redis-server libcups2-dev
pip install frappe-bench pip install frappe-bench
@@ -24,14 +24,15 @@ fi
if [ "$DB" == "mariadb" ];then if [ "$DB" == "mariadb" ];then
mysql --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL character_set_server = 'utf8mb4'" mysql --host 127.0.0.1 --port 3306 -u root -e "SET GLOBAL character_set_server = 'utf8mb4'"
mysql --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'" mysql --host 127.0.0.1 --port 3306 -u root -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'"
mysql --host 127.0.0.1 --port 3306 -u root -proot -e "CREATE USER 'test_frappe'@'localhost' IDENTIFIED BY 'test_frappe'" mysql --host 127.0.0.1 --port 3306 -u root -e "CREATE USER 'test_frappe'@'localhost' IDENTIFIED BY 'test_frappe'"
mysql --host 127.0.0.1 --port 3306 -u root -proot -e "CREATE DATABASE test_frappe" mysql --host 127.0.0.1 --port 3306 -u root -e "CREATE DATABASE test_frappe"
mysql --host 127.0.0.1 --port 3306 -u root -proot -e "GRANT ALL PRIVILEGES ON \`test_frappe\`.* TO 'test_frappe'@'localhost'" mysql --host 127.0.0.1 --port 3306 -u root -e "GRANT ALL PRIVILEGES ON \`test_frappe\`.* TO 'test_frappe'@'localhost'"
mysql --host 127.0.0.1 --port 3306 -u root -proot -e "FLUSH PRIVILEGES" mysql --host 127.0.0.1 --port 3306 -u root -e "UPDATE mysql.user SET Password=PASSWORD('travis') WHERE User='root'"
mysql --host 127.0.0.1 --port 3306 -u root -e "FLUSH PRIVILEGES"
fi fi
if [ "$DB" == "postgres" ];then if [ "$DB" == "postgres" ];then
@@ -39,14 +40,10 @@ if [ "$DB" == "postgres" ];then
echo "travis" | psql -h 127.0.0.1 -p 5432 -c "CREATE USER test_frappe WITH PASSWORD 'test_frappe'" -U postgres; echo "travis" | psql -h 127.0.0.1 -p 5432 -c "CREATE USER test_frappe WITH PASSWORD 'test_frappe'" -U postgres;
fi fi
wget -O /tmp/wkhtmltox.tar.xz https://github.com/frappe/wkhtmltopdf/raw/master/wkhtmltox-0.12.3_linux-generic-amd64.tar.xz
install_whktml() { tar -xf /tmp/wkhtmltox.tar.xz -C /tmp
wget -O /tmp/wkhtmltox.tar.xz https://github.com/frappe/wkhtmltopdf/raw/master/wkhtmltox-0.12.3_linux-generic-amd64.tar.xz sudo mv /tmp/wkhtmltox/bin/wkhtmltopdf /usr/local/bin/wkhtmltopdf
tar -xf /tmp/wkhtmltox.tar.xz -C /tmp sudo chmod o+x /usr/local/bin/wkhtmltopdf
sudo mv /tmp/wkhtmltox/bin/wkhtmltopdf /usr/local/bin/wkhtmltopdf
sudo chmod o+x /usr/local/bin/wkhtmltopdf
}
install_whktml &
cd ~/frappe-bench || exit cd ~/frappe-bench || exit
@@ -55,11 +52,10 @@ sed -i 's/schedule:/# schedule:/g' Procfile
sed -i 's/socketio:/# socketio:/g' Procfile sed -i 's/socketio:/# socketio:/g' Procfile
sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile
bench get-app payments
bench get-app erpnext "${GITHUB_WORKSPACE}" bench get-app erpnext "${GITHUB_WORKSPACE}"
if [ "$TYPE" == "server" ]; then bench setup requirements --dev; fi if [ "$TYPE" == "server" ]; then bench setup requirements --dev; fi
bench start &> bench_run_logs.txt & bench start &> bench_run_logs.txt &
CI=Yes bench build --app frappe &
bench --site test_site reinstall --yes bench --site test_site reinstall --yes
bench build --app frappe

View File

@@ -9,7 +9,7 @@
"mail_password": "test", "mail_password": "test",
"admin_password": "admin", "admin_password": "admin",
"root_login": "root", "root_login": "root",
"root_password": "root", "root_password": "travis",
"host_name": "http://test_site:8000", "host_name": "http://test_site:8000",
"install_apps": ["erpnext"], "install_apps": ["erpnext"],
"throttle_user_limit": 100 "throttle_user_limit": 100

11
.github/stale.yml vendored
View File

@@ -24,4 +24,13 @@ pulls:
:) Also, even if it is closed, you can always reopen the PR when you're :) Also, even if it is closed, you can always reopen the PR when you're
ready. Thank you for contributing. ready. Thank you for contributing.
only: pulls issues:
daysUntilStale: 60
daysUntilClose: 7
exemptLabels:
- valid
- to-validate
markComment: >
This issue has been automatically marked as inactive because it has not had
recent activity and it wasn't validated by maintainer team. It will be
closed within a week if no further activity occurs.

View File

@@ -1,4 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="4 2 193 52"> <svg width="201" height="60" viewBox="0 0 201 60" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_dd)"> <g filter="url(#filter0_dd)">
<rect x="4" y="2" width="193" height="52" rx="6" fill="#2490EF"/> <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"/> <path d="M28 22.2891H32.8786V35.5H36.2088V22.2891H41.0874V19.5H28V22.2891Z" fill="white"/>

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@@ -12,7 +12,7 @@ jobs:
- name: 'Setup Environment' - name: 'Setup Environment'
uses: actions/setup-python@v2 uses: actions/setup-python@v2
with: with:
python-version: '3.10' python-version: 3.8
- name: 'Clone repo' - name: 'Clone repo'
uses: actions/checkout@v2 uses: actions/checkout@v2

View File

@@ -1,32 +0,0 @@
# This workflow is agnostic to branches. Only maintain on develop branch.
# To add/remove versions just modify the matrix.
name: Create weekly release pull requests
on:
schedule:
# 9:30 UTC => 3 PM IST Tuesday
- cron: "30 9 * * 2"
workflow_dispatch:
jobs:
release:
name: Release
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
version: ["13", "14"]
steps:
- uses: octokit/request-action@v2.x
with:
route: POST /repos/{owner}/{repo}/pulls
owner: frappe
repo: erpnext
title: |-
"chore: release v${{ matrix.version }}"
body: "Automated weekly release."
base: version-${{ matrix.version }}
head: version-${{ matrix.version }}-hotfix
env:
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}

View File

@@ -11,10 +11,10 @@ jobs:
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python 3.10 - name: Set up Python 3.8
uses: actions/setup-python@v2 uses: actions/setup-python@v2
with: with:
python-version: '3.10' python-version: 3.8
- name: Install and Run Pre-commit - name: Install and Run Pre-commit
uses: pre-commit/action@v2.0.3 uses: pre-commit/action@v2.0.3
@@ -22,8 +22,10 @@ jobs:
- name: Download Semgrep rules - name: Download Semgrep rules
run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules
- name: Download semgrep - uses: returntocorp/semgrep-action@v1
run: pip install semgrep==0.97.0 env:
SEMGREP_TIMEOUT: 120
- name: Run Semgrep rules with:
run: semgrep ci --config ./frappe-semgrep-rules/rules --config r/python.lang.correctness config: >-
r/python.lang.correctness
./frappe-semgrep-rules/rules

View File

@@ -4,14 +4,11 @@ on:
pull_request: pull_request:
paths-ignore: paths-ignore:
- '**.js' - '**.js'
- '**.css'
- '**.md' - '**.md'
- '**.html'
- '**.csv'
workflow_dispatch: workflow_dispatch:
concurrency: concurrency:
group: patch-develop-${{ github.event_name }}-${{ github.event.number || github.event_name == 'workflow_dispatch' && github.run_id || '' }} group: patch-develop-${{ github.event.number }}
cancel-in-progress: true cancel-in-progress: true
jobs: jobs:
@@ -25,7 +22,7 @@ jobs:
mysql: mysql:
image: mariadb:10.3 image: mariadb:10.3
env: env:
MARIADB_ROOT_PASSWORD: 'root' MYSQL_ALLOW_EMPTY_PASSWORD: YES
ports: ports:
- 3306:3306 - 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
@@ -34,18 +31,10 @@ jobs:
- name: Clone - name: Clone
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Check for valid Python & Merge Conflicts
run: |
python -m compileall -f "${GITHUB_WORKSPACE}"
if grep -lr --exclude-dir=node_modules "^<<<<<<< " "${GITHUB_WORKSPACE}"
then echo "Found merge conflicts"
exit 1
fi
- name: Setup Python - name: Setup Python
uses: "gabrielfalcao/pyenv-action@v9" uses: actions/setup-python@v2
with: with:
versions: 3.10:latest, 3.7:latest python-version: 3.8
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v2 uses: actions/setup-node@v2
@@ -60,7 +49,7 @@ jobs:
uses: actions/cache@v2 uses: actions/cache@v2
with: with:
path: ~/.cache/pip path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml') }} key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
restore-keys: | restore-keys: |
${{ runner.os }}-pip- ${{ runner.os }}-pip-
${{ runner.os }}- ${{ runner.os }}-
@@ -90,10 +79,7 @@ jobs:
${{ runner.os }}-yarn- ${{ runner.os }}-yarn-
- name: Install - name: Install
run: | run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
pip install frappe-bench
pyenv global $(pyenv versions | grep '3.10')
bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
env: env:
DB: mariadb DB: mariadb
TYPE: server TYPE: server
@@ -107,7 +93,6 @@ jobs:
git -C "apps/frappe" remote set-url upstream https://github.com/frappe/frappe.git git -C "apps/frappe" remote set-url upstream https://github.com/frappe/frappe.git
git -C "apps/erpnext" remote set-url upstream https://github.com/frappe/erpnext.git git -C "apps/erpnext" remote set-url upstream https://github.com/frappe/erpnext.git
pyenv global $(pyenv versions | grep '3.7')
for version in $(seq 12 13) for version in $(seq 12 13)
do do
echo "Updating to v$version" echo "Updating to v$version"
@@ -119,11 +104,7 @@ jobs:
git -C "apps/frappe" checkout -q -f $branch_name git -C "apps/frappe" checkout -q -f $branch_name
git -C "apps/erpnext" checkout -q -f $branch_name git -C "apps/erpnext" checkout -q -f $branch_name
rm -rf ~/frappe-bench/env bench setup requirements --python
bench setup env
bench pip install -e ./apps/payments
bench pip install -e ./apps/erpnext
bench --site test_site migrate bench --site test_site migrate
done done
@@ -131,12 +112,4 @@ jobs:
echo "Updating to latest version" echo "Updating to latest version"
git -C "apps/frappe" checkout -q -f "${GITHUB_BASE_REF:-${GITHUB_REF##*/}}" git -C "apps/frappe" checkout -q -f "${GITHUB_BASE_REF:-${GITHUB_REF##*/}}"
git -C "apps/erpnext" checkout -q -f "$GITHUB_SHA" git -C "apps/erpnext" checkout -q -f "$GITHUB_SHA"
pyenv global $(pyenv versions | grep '3.10')
rm -rf ~/frappe-bench/env
bench -v setup env
bench pip install -e ./apps/payments
bench pip install -e ./apps/erpnext
bench --site test_site migrate bench --site test_site migrate
bench --site test_site install-app payments

View File

@@ -1,31 +0,0 @@
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

View File

@@ -1,30 +0,0 @@
name: Semantic Commits
on:
pull_request: {}
permissions:
contents: read
concurrency:
group: commitcheck-erpnext-${{ github.event.number }}
cancel-in-progress: true
jobs:
commitlint:
name: Check Commit Titles
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 200
- uses: actions/setup-node@v3
with:
node-version: 14
check-latest: true
- name: Check commit titles
run: |
npm install @commitlint/cli @commitlint/config-conventional
npx commitlint --verbose --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }}

View File

@@ -4,10 +4,8 @@ on:
pull_request: pull_request:
paths-ignore: paths-ignore:
- '**.js' - '**.js'
- '**.css'
- '**.md' - '**.md'
- '**.html' - '**.html'
- '**.csv'
push: push:
branches: [ develop ] branches: [ develop ]
paths-ignore: paths-ignore:
@@ -27,7 +25,7 @@ on:
type: string type: string
concurrency: concurrency:
group: server-mariadb-develop-${{ github.event_name }}-${{ github.event.number || github.event_name == 'workflow_dispatch' && github.run_id || '' }} group: server-mariadb-develop-${{ github.event.number }}
cancel-in-progress: true cancel-in-progress: true
jobs: jobs:
@@ -39,15 +37,15 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
container: [1, 2, 3, 4] container: [1, 2, 3]
name: Python Unit Tests name: Python Unit Tests
services: services:
mysql: mysql:
image: mariadb:10.6 image: mariadb:10.3
env: env:
MARIADB_ROOT_PASSWORD: 'root' MYSQL_ALLOW_EMPTY_PASSWORD: YES
ports: ports:
- 3306:3306 - 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
@@ -59,15 +57,7 @@ jobs:
- name: Setup Python - name: Setup Python
uses: actions/setup-python@v2 uses: actions/setup-python@v2
with: with:
python-version: '3.11' python-version: 3.8
- name: Check for valid Python & Merge Conflicts
run: |
python -m compileall -f "${GITHUB_WORKSPACE}"
if grep -lr --exclude-dir=node_modules "^<<<<<<< " "${GITHUB_WORKSPACE}"
then echo "Found merge conflicts"
exit 1
fi
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v2 uses: actions/setup-node@v2
@@ -82,7 +72,7 @@ jobs:
uses: actions/cache@v2 uses: actions/cache@v2
with: with:
path: ~/.cache/pip path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml') }} key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
restore-keys: | restore-keys: |
${{ runner.os }}-pip- ${{ runner.os }}-pip-
${{ runner.os }}- ${{ runner.os }}-
@@ -120,32 +110,16 @@ jobs:
FRAPPE_BRANCH: ${{ github.event.inputs.branch }} FRAPPE_BRANCH: ${{ github.event.inputs.branch }}
- name: Run Tests - name: Run Tests
run: 'cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --app erpnext --with-coverage --total-builds 4 --build-number ${{ matrix.container }}' run: cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --app erpnext --use-orchestrator --with-coverage
env: env:
TYPE: server TYPE: server
CI_BUILD_ID: ${{ github.run_id }} CI_BUILD_ID: ${{ github.run_id }}
ORCHESTRATOR_URL: http://test-orchestrator.frappe.io 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 - name: Upload coverage data
uses: codecov/codecov-action@v2 uses: codecov/codecov-action@v2
with: with:
name: MariaDB name: MariaDB
fail_ci_if_error: true fail_ci_if_error: true
files: /home/runner/frappe-bench/sites/coverage.xml
verbose: true verbose: true

View File

@@ -9,7 +9,7 @@ on:
types: [opened, labelled, synchronize, reopened] types: [opened, labelled, synchronize, reopened]
concurrency: concurrency:
group: server-postgres-develop-${{ github.event_name }}-${{ github.event.number || github.event_name == 'workflow_dispatch' && github.run_id || '' }} group: server-postgres-develop-${{ github.event.number }}
cancel-in-progress: true cancel-in-progress: true
jobs: jobs:
@@ -21,7 +21,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
container: [1] container: [1, 2, 3]
name: Python Unit Tests name: Python Unit Tests
@@ -46,15 +46,7 @@ jobs:
- name: Setup Python - name: Setup Python
uses: actions/setup-python@v2 uses: actions/setup-python@v2
with: with:
python-version: '3.10' python-version: 3.8
- name: Check for valid Python & Merge Conflicts
run: |
python -m compileall -f "${GITHUB_WORKSPACE}"
if grep -lr --exclude-dir=node_modules "^<<<<<<< " "${GITHUB_WORKSPACE}"
then echo "Found merge conflicts"
exit 1
fi
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v2 uses: actions/setup-node@v2
@@ -69,7 +61,7 @@ jobs:
uses: actions/cache@v2 uses: actions/cache@v2
with: with:
path: ~/.cache/pip path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml') }} key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
restore-keys: | restore-keys: |
${{ runner.os }}-pip- ${{ runner.os }}-pip-
${{ runner.os }}- ${{ runner.os }}-
@@ -98,6 +90,7 @@ jobs:
restore-keys: | restore-keys: |
${{ runner.os }}-yarn- ${{ runner.os }}-yarn-
- name: Install - name: Install
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
env: env:

117
.github/workflows/ui-tests.yml vendored Normal file
View File

@@ -0,0 +1,117 @@
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

View File

@@ -7,11 +7,6 @@ pull_request_rules:
- author!=gavindsouza - author!=gavindsouza
- author!=rohitwaghchaure - author!=rohitwaghchaure
- author!=nabinhait - author!=nabinhait
- author!=ankush
- author!=deepeshgarg007
- author!=frappe-pr-bot
- author!=mergify[bot]
- or: - or:
- base=version-13 - base=version-13
- base=version-12 - base=version-12
@@ -22,46 +17,6 @@ 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. @{{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 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"
actions:
backport:
branches:
- develop
assignees:
- "{{ author }}"
- name: backport to version-14-hotfix
conditions:
- label="backport version-14-hotfix"
actions:
backport:
branches:
- version-14-hotfix
assignees:
- "{{ author }}"
- name: backport to version-14-pre-release
conditions:
- label="backport version-14-pre-release"
actions:
backport:
branches:
- version-14-pre-release
assignees:
- "{{ author }}"
- name: backport to version-13-hotfix - name: backport to version-13-hotfix
conditions: conditions:
- label="backport version-13-hotfix" - label="backport version-13-hotfix"
@@ -101,37 +56,3 @@ pull_request_rules:
- version-12-pre-release - version-12-pre-release
assignees: assignees:
- "{{ author }}" - "{{ 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 }}

View File

@@ -16,8 +16,8 @@ repos:
- id: check-merge-conflict - id: check-merge-conflict
- id: check-ast - id: check-ast
- repo: https://github.com/PyCQA/flake8 - repo: https://gitlab.com/pycqa/flake8
rev: 5.0.4 rev: 3.9.2
hooks: hooks:
- id: flake8 - id: flake8
additional_dependencies: [ additional_dependencies: [
@@ -26,19 +26,12 @@ repos:
args: ['--config', '.github/helper/.flake8_strict'] args: ['--config', '.github/helper/.flake8_strict']
exclude: ".*setup.py$" 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 - repo: https://github.com/timothycrosley/isort
rev: 5.9.1 rev: 5.9.1
hooks: hooks:
- id: isort - id: isort
exclude: ".*setup.py$" exclude: ".*setup.py$"
ci: ci:
autoupdate_schedule: weekly autoupdate_schedule: weekly
skip: [] skip: []

View File

@@ -1,24 +0,0 @@
{
"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"
]
}

View File

@@ -3,26 +3,33 @@
# These owners will be the default owners for everything in # These owners will be the default owners for everything in
# the repo. Unless a later match takes precedence, # the repo. Unless a later match takes precedence,
erpnext/accounts/ @nextchamp-saqib @deepeshgarg007 @ruthra-kumar erpnext/accounts/ @nextchamp-saqib @deepeshgarg007
erpnext/assets/ @nextchamp-saqib @deepeshgarg007 @ruthra-kumar erpnext/assets/ @nextchamp-saqib @deepeshgarg007
erpnext/erpnext_integrations/ @nextchamp-saqib
erpnext/loan_management/ @nextchamp-saqib @deepeshgarg007 erpnext/loan_management/ @nextchamp-saqib @deepeshgarg007
erpnext/regional @nextchamp-saqib @deepeshgarg007 @ruthra-kumar erpnext/regional @nextchamp-saqib @deepeshgarg007
erpnext/selling @nextchamp-saqib @deepeshgarg007 @ruthra-kumar erpnext/selling @nextchamp-saqib @deepeshgarg007
erpnext/support/ @nextchamp-saqib @deepeshgarg007 erpnext/support/ @nextchamp-saqib @deepeshgarg007
pos* @nextchamp-saqib pos* @nextchamp-saqib
erpnext/buying/ @rohitwaghchaure @s-aga-r erpnext/buying/ @marination @rohitwaghchaure @ankush
erpnext/maintenance/ @rohitwaghchaure @s-aga-r erpnext/e_commerce/ @marination
erpnext/manufacturing/ @rohitwaghchaure @s-aga-r erpnext/maintenance/ @marination @rohitwaghchaure
erpnext/quality_management/ @rohitwaghchaure @s-aga-r erpnext/manufacturing/ @marination @rohitwaghchaure @ankush
erpnext/stock/ @rohitwaghchaure @s-aga-r erpnext/portal/ @marination
erpnext/quality_management/ @marination @rohitwaghchaure
erpnext/shopping_cart/ @marination
erpnext/stock/ @marination @rohitwaghchaure @ankush
erpnext/crm/ @NagariaHussain erpnext/crm/ @ruchamahabal @pateljannat
erpnext/education/ @rutwikhdev erpnext/education/ @ruchamahabal @pateljannat
erpnext/projects/ @ruchamahabal erpnext/hr/ @ruchamahabal @pateljannat
erpnext/payroll @ruchamahabal @pateljannat
erpnext/projects/ @ruchamahabal @pateljannat
erpnext/controllers/ @deepeshgarg007 @nextchamp-saqib @rohitwaghchaure erpnext/controllers/ @deepeshgarg007 @nextchamp-saqib @rohitwaghchaure @marination @ankush
erpnext/patches/ @deepeshgarg007 @nextchamp-saqib erpnext/patches/ @deepeshgarg007 @nextchamp-saqib @marination @ankush
erpnext/public/ @nextchamp-saqib @marination
.github/ @ankush .github/ @ankush
pyproject.toml @ankush requirements.txt @gavindsouza

View File

@@ -1,14 +1,11 @@
<div align="center"> <div align="center">
<a href="https://erpnext.com">
<img src="https://raw.githubusercontent.com/frappe/erpnext/develop/erpnext/public/images/erpnext-logo.png" height="128"> <img src="https://raw.githubusercontent.com/frappe/erpnext/develop/erpnext/public/images/erpnext-logo.png" height="128">
</a>
<h2>ERPNext</h2> <h2>ERPNext</h2>
<p align="center"> <p align="center">
<p>ERP made simple</p> <p>ERP made simple</p>
</p> </p>
[![CI](https://github.com/frappe/erpnext/actions/workflows/server-tests.yml/badge.svg?branch=develop)](https://github.com/frappe/erpnext/actions/workflows/server-tests.yml) [![CI](https://github.com/frappe/erpnext/actions/workflows/server-tests.yml/badge.svg?branch=develop)](https://github.com/frappe/erpnext/actions/workflows/server-tests.yml)
[![UI](https://github.com/erpnext/erpnext_ui_tests/actions/workflows/ui-tests.yml/badge.svg?branch=develop&event=schedule)](https://github.com/erpnext/erpnext_ui_tests/actions/workflows/ui-tests.yml)
[![Open Source Helpers](https://www.codetriage.com/frappe/erpnext/badges/users.svg)](https://www.codetriage.com/frappe/erpnext) [![Open Source Helpers](https://www.codetriage.com/frappe/erpnext/badges/users.svg)](https://www.codetriage.com/frappe/erpnext)
[![codecov](https://codecov.io/gh/frappe/erpnext/branch/develop/graph/badge.svg?token=0TwvyUg3I5)](https://codecov.io/gh/frappe/erpnext) [![codecov](https://codecov.io/gh/frappe/erpnext/branch/develop/graph/badge.svg?token=0TwvyUg3I5)](https://codecov.io/gh/frappe/erpnext)
[![docker pulls](https://img.shields.io/docker/pulls/frappe/erpnext-worker.svg)](https://hub.docker.com/r/frappe/erpnext-worker) [![docker pulls](https://img.shields.io/docker/pulls/frappe/erpnext-worker.svg)](https://hub.docker.com/r/frappe/erpnext-worker)
@@ -34,47 +31,32 @@ ERPNext as a monolith includes the following areas for managing businesses:
1. [Customize ERPNext](https://erpnext.com/docs/user/manual/en/customize-erpnext) 1. [Customize ERPNext](https://erpnext.com/docs/user/manual/en/customize-erpnext)
1. [And More](https://erpnext.com/docs/user/manual/en/) 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. ERPNext is built on the [Frappe Framework](https://github.com/frappe/frappe), a full-stack web app framework built with Python & JavaScript.
## Installation - [User Guide](https://erpnext.com/docs/user)
- [Discussion Forum](https://discuss.erpnext.com/)
<div align="center" style="max-height: 40px;"> ---
<a href="https://frappecloud.com/erpnext/signup">
<div align="center">
<a href="https://frappecloud.com/deploy?apps=frappe,erpnext&source=erpnext_readme">
<img src=".github/try-on-f-cloud-button.svg" height="40"> <img src=".github/try-on-f-cloud-button.svg" height="40">
</a> </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> </div>
> Login for the PWD site: (username: Administrator, password: admin)
### Containerized Installation ### 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. 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.
### Manual Install ### Full 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. 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). 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).
---
## Learning and community
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.
## Contributing
1. [Issue Guidelines](https://github.com/frappe/erpnext/wiki/Issue-Guidelines)
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)
## License ## License
@@ -82,8 +64,57 @@ 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. The ERPNext code is licensed as GNU General Public License (v3) and the Documentation is licensed as Creative Commons (CC-BY-SA-3.0) and the copyright is owned by Frappe Technologies Pvt Ltd (Frappe) and Contributors.
By contributing to ERPNext, you agree that your contributions will be licensed under its GNU General Public License (v3). ---
## Logo and Trademark Policy ## Contributing
Please read our [Logo and Trademark Policy](TRADEMARK_POLICY.md). 1. [Issue Guidelines](https://github.com/frappe/erpnext/wiki/Issue-Guidelines)
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
1. [Frappe School](https://frappe.school) - Learn Frappe Framework and ERPNext from the various courses by the maintainers or from the community.
---
## Logo and Trademark
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:
- Wed like to make it easy for anyone to use the ERPNext name or logo for community-oriented efforts that help spread and improve ERPNext.
- Wed like to make it clear how ERPNext-related businesses and projects can (and cannot) use the ERPNext name and logo.
- Wed 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, its 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)

View File

@@ -1,36 +0,0 @@
## 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:
- Wed like to make it easy for anyone to use the ERPNext name or logo for community-oriented efforts that help spread and improve ERPNext.
- Wed like to make it clear how ERPNext-related businesses and projects can (and cannot) use the ERPNext name and logo.
- Wed 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, its 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)

View File

@@ -21,6 +21,7 @@ coverage:
comment: comment:
layout: "diff, files" layout: "diff, files"
require_changes: true require_changes: true
after_n_builds: 3
ignore: ignore:
- "erpnext/demo" - "erpnext/demo"

View File

@@ -1,25 +0,0 @@
module.exports = {
parserPreset: 'conventional-changelog-conventionalcommits',
rules: {
'subject-empty': [2, 'never'],
'type-case': [2, 'always', 'lower-case'],
'type-empty': [2, 'never'],
'type-enum': [
2,
'always',
[
'build',
'chore',
'ci',
'docs',
'feat',
'fix',
'perf',
'refactor',
'revert',
'style',
'test',
],
],
},
};

11
cypress.json Normal file
View File

@@ -0,0 +1,11 @@
{
"baseUrl": "http://test_site:8000/",
"projectId": "da59y9",
"adminPassword": "admin",
"defaultCommandTimeout": 20000,
"pageLoadTimeout": 15000,
"retries": {
"runMode": 2,
"openMode": 2
}
}

View File

@@ -0,0 +1,5 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}

View File

@@ -0,0 +1,13 @@
context('Customer', () => {
before(() => {
cy.login();
});
it('Check Customer Group', () => {
cy.visit(`app/customer/`);
cy.get('.primary-action').click();
cy.wait(500);
cy.get('.custom-actions > .btn').click();
cy.get_field('customer_group', 'Link').should('have.value', 'All Customer Groups');
});
});

View File

@@ -0,0 +1,44 @@
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");
});
});

View File

@@ -0,0 +1,116 @@
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]}`);
});
});
});

View File

@@ -0,0 +1,195 @@
context('Organizational Chart Mobile', () => {
before(() => {
cy.login();
cy.visit('/app/website');
});
it('navigates to org chart', () => {
cy.viewport(375, 667);
cy.visit('/app');
cy.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]}`);
});
});
});

17
cypress/plugins/index.js Normal file
View File

@@ -0,0 +1,17 @@
// ***********************************************************
// This example plugins/index.js can be used to load plugins
//
// You can change the location of this file or turn off loading
// the plugins file with the 'pluginsFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/plugins-guide
// ***********************************************************
// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)
module.exports = () => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
};

View File

@@ -0,0 +1,31 @@
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add("login", (email, password) => { ... });
//
//
// -- This is a child command --
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... });
//
//
// -- This is a dual command --
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... });
//
//
// -- This is will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... });
const slug = (name) => name.toLowerCase().replace(" ", "-");
Cypress.Commands.add("go_to_doc", (doctype, name) => {
cy.visit(`/app/${slug(doctype)}/${encodeURIComponent(name)}`);
});

26
cypress/support/index.js Normal file
View File

@@ -0,0 +1,26 @@
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands';
import '../../../frappe/cypress/support/commands' // eslint-disable-line
// Alternatively you can use CommonJS syntax:
// require('./commands')
Cypress.Cookies.defaults({
preserve: 'sid'
});

12
cypress/tsconfig.json Normal file
View File

@@ -0,0 +1,12 @@
{
"compilerOptions": {
"allowJs": true,
"baseUrl": "../node_modules",
"types": [
"cypress"
]
},
"include": [
"**/*.*"
]
}

1
dev-requirements.txt Normal file
View File

@@ -0,0 +1 @@
hypothesis~=6.31.0

View File

@@ -2,57 +2,51 @@ import inspect
import frappe import frappe
__version__ = "14.0.0-dev" from erpnext.hooks import regional_overrides
__version__ = '14.0.0-beta.1'
def get_default_company(user=None): 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 from frappe.defaults import get_user_default_as_list
if not user: if not user:
user = frappe.session.user user = frappe.session.user
companies = get_user_default_as_list(user, "company") companies = get_user_default_as_list(user, 'company')
if companies: if companies:
default_company = companies[0] default_company = companies[0]
else: 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 return default_company
def get_default_currency(): def get_default_currency():
"""Returns the currency of the default company""" '''Returns the currency of the default company'''
company = get_default_company() company = get_default_company()
if 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): 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: if not company:
return None return None
if not frappe.flags.company_cost_center: if not frappe.flags.company_cost_center:
frappe.flags.company_cost_center = {} frappe.flags.company_cost_center = {}
if not company in frappe.flags.company_cost_center: if not company in frappe.flags.company_cost_center:
frappe.flags.company_cost_center[company] = frappe.get_cached_value( frappe.flags.company_cost_center[company] = frappe.get_cached_value('Company', company, 'cost_center')
"Company", company, "cost_center"
)
return frappe.flags.company_cost_center[company] return frappe.flags.company_cost_center[company]
def get_company_currency(company): def get_company_currency(company):
"""Returns the default company currency""" '''Returns the default company currency'''
if not frappe.flags.company_currency: if not frappe.flags.company_currency:
frappe.flags.company_currency = {} frappe.flags.company_currency = {}
if not company in frappe.flags.company_currency: if not company in frappe.flags.company_currency:
frappe.flags.company_currency[company] = frappe.db.get_value( frappe.flags.company_currency[company] = frappe.db.get_value('Company', company, 'default_currency', cache=True)
"Company", company, "default_currency", cache=True
)
return frappe.flags.company_currency[company] return frappe.flags.company_currency[company]
def set_perpetual_inventory(enable=1, company=None): def set_perpetual_inventory(enable=1, company=None):
if not company: if not company:
company = "_Test Company" if frappe.flags.in_test else get_default_company() company = "_Test Company" if frappe.flags.in_test else get_default_company()
@@ -61,10 +55,9 @@ def set_perpetual_inventory(enable=1, company=None):
company.enable_perpetual_inventory = enable company.enable_perpetual_inventory = enable
company.save() company.save()
def encode_company_abbr(name, company=None, abbr=None): def encode_company_abbr(name, company=None, abbr=None):
"""Returns name encoded with company abbreviation""" '''Returns name encoded with company abbreviation'''
company_abbr = abbr or frappe.get_cached_value("Company", company, "abbr") company_abbr = abbr or frappe.get_cached_value('Company', company, "abbr")
parts = name.rsplit(" - ", 1) parts = name.rsplit(" - ", 1)
if parts[-1].lower() != company_abbr.lower(): if parts[-1].lower() != company_abbr.lower():
@@ -72,78 +65,76 @@ def encode_company_abbr(name, company=None, abbr=None):
return " - ".join(parts) return " - ".join(parts)
def is_perpetual_inventory_enabled(company): def is_perpetual_inventory_enabled(company):
if not company: if not company:
company = "_Test Company" if frappe.flags.in_test else get_default_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 = {} frappe.local.enable_perpetual_inventory = {}
if not company in frappe.local.enable_perpetual_inventory: if not company in frappe.local.enable_perpetual_inventory:
frappe.local.enable_perpetual_inventory[company] = ( frappe.local.enable_perpetual_inventory[company] = frappe.get_cached_value('Company',
frappe.get_cached_value("Company", company, "enable_perpetual_inventory") or 0 company, "enable_perpetual_inventory") or 0
)
return frappe.local.enable_perpetual_inventory[company] return frappe.local.enable_perpetual_inventory[company]
def get_default_finance_book(company=None): def get_default_finance_book(company=None):
if not company: if not company:
company = get_default_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 = {} frappe.local.default_finance_book = {}
if not company in frappe.local.default_finance_book: if not company in frappe.local.default_finance_book:
frappe.local.default_finance_book[company] = frappe.get_cached_value( frappe.local.default_finance_book[company] = frappe.get_cached_value('Company',
"Company", company, "default_finance_book" company, "default_finance_book")
)
return frappe.local.default_finance_book[company] return frappe.local.default_finance_book[company]
def get_party_account_type(party_type): 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 = {} frappe.local.party_account_types = {}
if not party_type in frappe.local.party_account_types: if not party_type in frappe.local.party_account_types:
frappe.local.party_account_types[party_type] = ( frappe.local.party_account_types[party_type] = frappe.db.get_value("Party Type",
frappe.db.get_value("Party Type", party_type, "account_type") or "" party_type, "account_type") or ''
)
return frappe.local.party_account_types[party_type] return frappe.local.party_account_types[party_type]
def get_region(company=None): 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` You can also set global company flag in `frappe.flags.company`
""" '''
if company or 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: elif frappe.flags.country:
return frappe.flags.country return frappe.flags.country
else: else:
return frappe.get_system_settings("country") return frappe.get_system_settings('country')
def allow_regional(fn): def allow_regional(fn):
"""Decorator to make a function regionally overridable '''Decorator to make a function regionally overridable
Example: Example:
@erpnext.allow_regional @erpnext.allow_regional
def myfunction(): def myfunction():
pass""" pass'''
def caller(*args, **kwargs): def caller(*args, **kwargs):
overrides = frappe.get_hooks("regional_overrides", {}).get(get_region()) region = get_region()
function_path = f"{inspect.getmodule(fn).__name__}.{fn.__name__}" fn_name = inspect.getmodule(fn).__name__ + '.' + fn.__name__
if region in regional_overrides and fn_name in regional_overrides[region]:
if not overrides or function_path not in overrides: return frappe.get_attr(regional_overrides[region][fn_name])(*args, **kwargs)
else:
return fn(*args, **kwargs) return fn(*args, **kwargs)
# Priority given to last installed app
return frappe.get_attr(overrides[function_path][-1])(*args, **kwargs)
return caller return caller
def get_last_membership(member):
'''Returns last membership if exists'''
last_membership = frappe.get_all('Membership', 'name,to_date,membership_type',
dict(member=member, paid=1), order_by='to_date desc', limit=1)
if last_membership:
return last_membership[0]

View File

@@ -11,41 +11,3 @@ Entries are:
- Purchase 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 |

View File

@@ -21,39 +21,37 @@ class ERPNextAddress(Address):
return super(ERPNextAddress, self).link_address() return super(ERPNextAddress, self).link_address()
def update_compnay_address(self): def update_compnay_address(self):
for link in self.get("links"): for link in self.get('links'):
if link.link_doctype == "Company": if link.link_doctype == 'Company':
self.is_your_company_address = 1 self.is_your_company_address = 1
def validate_reference(self): def validate_reference(self):
if self.is_your_company_address and not [ if self.is_your_company_address and not [
row for row in self.links if row.link_doctype == "Company" row for row in self.links if row.link_doctype == "Company"
]: ]:
frappe.throw( frappe.throw(_("Address needs to be linked to a Company. Please add a row for Company in the Links table."),
_("Address needs to be linked to a Company. Please add a row for Company in the Links table."), title=_("Company Not Linked"))
title=_("Company Not Linked"),
)
def on_update(self): def on_update(self):
""" """
After Address is updated, update the related 'Primary Address' on Customer. After Address is updated, update the related 'Primary Address' on Customer.
""" """
address_display = get_address_display(self.as_dict()) 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) customers = frappe.db.get_all("Customer", filters=filters, as_list=True)
for customer_name in customers: for customer_name in customers:
frappe.db.set_value("Customer", customer_name[0], "primary_address", address_display) frappe.db.set_value("Customer", customer_name[0], "primary_address", address_display)
@frappe.whitelist() @frappe.whitelist()
def get_shipping_address(company, address=None): def get_shipping_address(company, address = None):
filters = [ filters = [
["Dynamic Link", "link_doctype", "=", "Company"], ["Dynamic Link", "link_doctype", "=", "Company"],
["Dynamic Link", "link_name", "=", company], ["Dynamic Link", "link_name", "=", company],
["Address", "is_your_company_address", "=", 1], ["Address", "is_your_company_address", "=", 1]
] ]
fields = ["*"] 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]) filters.append(["Address", "name", "=", address])
if not address: if not address:
filters.append(["Address", "is_shipping_address", "=", 1]) filters.append(["Address", "is_shipping_address", "=", 1])

View File

@@ -12,24 +12,15 @@ from frappe.utils.nestedset import get_descendants_of
@frappe.whitelist() @frappe.whitelist()
@cache_source @cache_source
def get( def get(chart_name = None, chart = None, no_cache = None, filters = None, from_date = None,
chart_name=None, to_date = None, timespan = None, time_interval = None, heatmap_year = 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: if chart_name:
chart = frappe.get_doc("Dashboard Chart", chart_name) chart = frappe.get_doc('Dashboard Chart', chart_name)
else: else:
chart = frappe._dict(frappe.parse_json(chart)) chart = frappe._dict(frappe.parse_json(chart))
timespan = chart.timespan timespan = chart.timespan
if chart.timespan == "Select Date Range": if chart.timespan == 'Select Date Range':
from_date = chart.from_date from_date = chart.from_date
to_date = chart.to_date to_date = chart.to_date
@@ -40,23 +31,17 @@ def get(
company = filters.get("company") company = filters.get("company")
if not account and chart_name: if not account and chart_name:
frappe.throw( frappe.throw(_("Account is not set for the dashboard chart {0}")
_("Account is not set for the dashboard chart {0}").format( .format(get_link_to_form("Dashboard Chart", chart_name)))
get_link_to_form("Dashboard Chart", chart_name)
)
)
if not frappe.db.exists("Account", account) and chart_name: if not frappe.db.exists("Account", account) and chart_name:
frappe.throw( frappe.throw(_("Account {0} does not exists in the dashboard chart {1}")
_("Account {0} does not exists in the dashboard chart {1}").format( .format(account, get_link_to_form("Dashboard Chart", chart_name)))
account, get_link_to_form("Dashboard Chart", chart_name)
)
)
if not to_date: if not to_date:
to_date = nowdate() to_date = nowdate()
if not from_date: if not from_date:
if timegrain in ("Monthly", "Quarterly"): if timegrain in ('Monthly', 'Quarterly'):
from_date = get_from_date_from_timespan(to_date, timespan) from_date = get_from_date_from_timespan(to_date, timespan)
# fetch dates to plot # fetch dates to plot
@@ -69,14 +54,16 @@ def get(
result = build_result(account, dates, gl_entries) result = build_result(account, dates, gl_entries)
return { return {
"labels": [formatdate(r[0].strftime("%Y-%m-%d")) 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]}], "datasets": [{
"name": account,
"values": [r[1] for r in result]
}]
} }
def build_result(account, dates, gl_entries): def build_result(account, dates, gl_entries):
result = [[getdate(date), 0.0] for date in dates] result = [[getdate(date), 0.0] for date in dates]
root_type = frappe.get_cached_value("Account", account, "root_type") root_type = frappe.db.get_value('Account', account, 'root_type')
# start with the first date # start with the first date
date_index = 0 date_index = 0
@@ -91,34 +78,30 @@ def build_result(account, dates, gl_entries):
result[date_index][1] += entry.debit - entry.credit result[date_index][1] += entry.debit - entry.credit
# if account type is credit, switch balances # 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: for r in result:
r[1] = -1 * r[1] r[1] = -1 * r[1]
# for balance sheet accounts, the totals are cumulative # 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): for i, r in enumerate(result):
if i > 0: if i > 0:
r[1] = r[1] + result[i - 1][1] r[1] = r[1] + result[i-1][1]
return result return result
def get_gl_entries(account, to_date): 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) child_accounts.append(account)
return frappe.db.get_all( return frappe.db.get_all('GL Entry',
"GL Entry", fields = ['posting_date', 'debit', 'credit'],
fields=["posting_date", "debit", "credit"], filters = [
filters=[ dict(posting_date = ('<', to_date)),
dict(posting_date=("<", to_date)), dict(account = ('in', child_accounts)),
dict(account=("in", child_accounts)), dict(voucher_type = ('!=', 'Period Closing Voucher'))
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): def get_dates_from_timegrain(from_date, to_date, timegrain):
days = months = years = 0 days = months = years = 0
@@ -133,8 +116,6 @@ def get_dates_from_timegrain(from_date, to_date, timegrain):
dates = [get_period_ending(from_date, timegrain)] dates = [get_period_ending(from_date, timegrain)]
while getdate(dates[-1]) < getdate(to_date): while getdate(dates[-1]) < getdate(to_date):
date = get_period_ending( date = get_period_ending(add_to_date(dates[-1], years=years, months=months, days=days), timegrain)
add_to_date(dates[-1], years=years, months=months, days=days), timegrain
)
dates.append(date) dates.append(date)
return dates return dates

View File

@@ -22,23 +22,20 @@ from erpnext.accounts.utils import get_account_currency
def validate_service_stop_date(doc): 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_check = "enable_deferred_revenue" \
"enable_deferred_revenue" if doc.doctype == "Sales Invoice" else "enable_deferred_expense" if doc.doctype=="Sales Invoice" else "enable_deferred_expense"
)
old_stop_dates = {} old_stop_dates = {}
old_doc = frappe.db.get_all( old_doc = frappe.db.get_all("{0} Item".format(doc.doctype),
"{0} Item".format(doc.doctype), {"parent": doc.name}, ["name", "service_stop_date"] {"parent": doc.name}, ["name", "service_stop_date"])
)
for d in old_doc: for d in old_doc:
old_stop_dates[d.name] = d.service_stop_date or "" old_stop_dates[d.name] = d.service_stop_date or ""
for item in doc.items: for item in doc.items:
if not item.get(enable_check): if not item.get(enable_check): continue
continue
if item.service_stop_date: if item.service_stop_date:
if date_diff(item.service_stop_date, item.service_start_date) < 0: if date_diff(item.service_stop_date, item.service_start_date) < 0:
@@ -47,31 +44,21 @@ def validate_service_stop_date(doc):
if date_diff(item.service_stop_date, item.service_end_date) > 0: if date_diff(item.service_stop_date, item.service_end_date) > 0:
frappe.throw(_("Service Stop Date cannot be after Service End Date")) frappe.throw(_("Service Stop Date cannot be after Service End Date"))
if ( if old_stop_dates and old_stop_dates.get(item.name) and item.service_stop_date!=old_stop_dates.get(item.name):
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)) frappe.throw(_("Cannot change Service Stop Date for item in row {0}").format(item.idx))
def build_conditions(process_type, account, company): def build_conditions(process_type, account, company):
conditions = "" conditions=''
deferred_account = ( deferred_account = "item.deferred_revenue_account" if process_type=="Income" else "item.deferred_expense_account"
"item.deferred_revenue_account" if process_type == "Income" else "item.deferred_expense_account"
)
if account: if account:
conditions += "AND %s='%s'" % (deferred_account, account) conditions += "AND %s='%s'"%(deferred_account, account)
elif company: elif company:
conditions += f"AND p.company = {frappe.db.escape(company)}" conditions += f"AND p.company = {frappe.db.escape(company)}"
return conditions 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 # 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: if not start_date:
@@ -80,19 +67,14 @@ def convert_deferred_expense_to_expense(
end_date = add_days(today(), -1) end_date = add_days(today(), -1)
# check for the purchase invoice for which GL entries has to be done # 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 select distinct item.parent
from `tabPurchase Invoice Item` item, `tabPurchase Invoice` p from `tabPurchase Invoice Item` item, `tabPurchase Invoice` p
where item.service_start_date<=%s and item.service_end_date>=%s 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.enable_deferred_expense = 1 and item.parent=p.name
and item.docstatus = 1 and ifnull(item.amount, 0) > 0 and item.docstatus = 1 and ifnull(item.amount, 0) > 0
{0} {0}
""".format( '''.format(conditions), (end_date, start_date)) #nosec
conditions
),
(end_date, start_date),
) # nosec
# For each invoice, book deferred expense # For each invoice, book deferred expense
for invoice in invoices: for invoice in invoices:
@@ -102,10 +84,7 @@ def convert_deferred_expense_to_expense(
if frappe.flags.deferred_accounting_error: if frappe.flags.deferred_accounting_error:
send_mail(deferred_process) 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 # 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: if not start_date:
@@ -114,19 +93,14 @@ def convert_deferred_revenue_to_income(
end_date = add_days(today(), -1) end_date = add_days(today(), -1)
# check for the sales invoice for which GL entries has to be done # 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 select distinct item.parent
from `tabSales Invoice Item` item, `tabSales Invoice` p from `tabSales Invoice Item` item, `tabSales Invoice` p
where item.service_start_date<=%s and item.service_end_date>=%s 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.enable_deferred_revenue = 1 and item.parent=p.name
and item.docstatus = 1 and ifnull(item.amount, 0) > 0 and item.docstatus = 1 and ifnull(item.amount, 0) > 0
{0} {0}
""".format( '''.format(conditions), (end_date, start_date)) #nosec
conditions
),
(end_date, start_date),
) # nosec
for invoice in invoices: for invoice in invoices:
doc = frappe.get_doc("Sales Invoice", invoice) doc = frappe.get_doc("Sales Invoice", invoice)
@@ -135,43 +109,30 @@ def convert_deferred_revenue_to_income(
if frappe.flags.deferred_accounting_error: if frappe.flags.deferred_accounting_error:
send_mail(deferred_process) send_mail(deferred_process)
def get_booking_dates(doc, item, posting_date=None): def get_booking_dates(doc, item, posting_date=None):
if not posting_date: if not posting_date:
posting_date = add_days(today(), -1) posting_date = add_days(today(), -1)
last_gl_entry = False last_gl_entry = False
deferred_account = ( deferred_account = "deferred_revenue_account" if doc.doctype=="Sales Invoice" else "deferred_expense_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 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 voucher_type=%s and voucher_no=%s and voucher_detail_no=%s
and is_cancelled = 0
order by posting_date desc limit 1 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 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 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_type=%s and c.reference_name=%s
and c.reference_detail_no=%s and c.docstatus < 2 order by posting_date desc limit 1 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 prev_gl_via_je:
if (not prev_gl_entry) or ( if (not prev_gl_entry) or (prev_gl_entry and
prev_gl_entry and prev_gl_entry[0].posting_date < prev_gl_via_je[0].posting_date prev_gl_entry[0].posting_date < prev_gl_via_je[0].posting_date):
):
prev_gl_entry = prev_gl_via_je prev_gl_entry = prev_gl_via_je
if prev_gl_entry: if prev_gl_entry:
@@ -195,94 +156,66 @@ def get_booking_dates(doc, item, posting_date=None):
else: else:
return None, None, None 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 amount, base_amount = 0, 0
if not last_gl_entry: if not last_gl_entry:
total_months = ( total_months = (item.service_end_date.year - item.service_start_date.year) * 12 + \
(item.service_end_date.year - item.service_start_date.year) * 12 (item.service_end_date.month - item.service_start_date.month) + 1
+ (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( prorate_factor = flt(date_diff(item.service_end_date, item.service_start_date)) \
date_diff(get_last_day(item.service_end_date), get_first_day(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) actual_months = rounded(total_months * prorate_factor, 1)
already_booked_amount, already_booked_amount_in_account_currency = get_already_booked_amount( already_booked_amount, already_booked_amount_in_account_currency = get_already_booked_amount(doc, item)
doc, item
)
base_amount = flt(item.base_net_amount / actual_months, item.precision("base_net_amount")) base_amount = flt(item.base_net_amount / actual_months, item.precision("base_net_amount"))
if base_amount + already_booked_amount > item.base_net_amount: if base_amount + already_booked_amount > item.base_net_amount:
base_amount = item.base_net_amount - already_booked_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 amount = base_amount
else: 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: if amount + already_booked_amount_in_account_currency > item.net_amount:
amount = item.net_amount - already_booked_amount_in_account_currency 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): 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( partial_month = flt(date_diff(end_date, start_date)) \
date_diff(get_last_day(end_date), get_first_day(start_date)) / flt(date_diff(get_last_day(end_date), get_first_day(start_date)))
)
base_amount = rounded(partial_month, 1) * base_amount base_amount = rounded(partial_month, 1) * base_amount
amount = rounded(partial_month, 1) * amount amount = rounded(partial_month, 1) * amount
else: else:
already_booked_amount, already_booked_amount_in_account_currency = get_already_booked_amount( already_booked_amount, already_booked_amount_in_account_currency = get_already_booked_amount(doc, item)
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 amount = base_amount
else: else:
amount = flt( amount = flt(item.net_amount - already_booked_amount_in_account_currency, item.precision("net_amount"))
item.net_amount - already_booked_amount_in_account_currency, item.precision("net_amount")
)
return amount, base_amount return amount, base_amount
def calculate_amount(doc, item, last_gl_entry, total_days, total_booking_days, account_currency): def calculate_amount(doc, item, last_gl_entry, total_days, total_booking_days, account_currency):
amount, base_amount = 0, 0 amount, base_amount = 0, 0
if not last_gl_entry: if not last_gl_entry:
base_amount = flt( base_amount = flt(item.base_net_amount*total_booking_days/flt(total_days), item.precision("base_net_amount"))
item.base_net_amount * total_booking_days / flt(total_days), item.precision("base_net_amount") if account_currency==doc.company_currency:
)
if account_currency == doc.company_currency:
amount = base_amount amount = base_amount
else: else:
amount = flt( amount = flt(item.net_amount*total_booking_days/flt(total_days), item.precision("net_amount"))
item.net_amount * total_booking_days / flt(total_days), item.precision("net_amount")
)
else: else:
already_booked_amount, already_booked_amount_in_account_currency = get_already_booked_amount( already_booked_amount, already_booked_amount_in_account_currency = get_already_booked_amount(doc, item)
doc, item
)
base_amount = flt( base_amount = flt(item.base_net_amount - already_booked_amount, item.precision("base_net_amount"))
item.base_net_amount - already_booked_amount, item.precision("base_net_amount") if account_currency==doc.company_currency:
)
if account_currency == doc.company_currency:
amount = base_amount amount = base_amount
else: else:
amount = flt( amount = flt(item.net_amount - already_booked_amount_in_account_currency, item.precision("net_amount"))
item.net_amount - already_booked_amount_in_account_currency, item.precision("net_amount")
)
return amount, base_amount return amount, base_amount
def get_already_booked_amount(doc, item): def get_already_booked_amount(doc, item):
if doc.doctype == "Sales Invoice": if doc.doctype == "Sales Invoice":
total_credit_debit, total_credit_debit_currency = "debit", "debit_in_account_currency" total_credit_debit, total_credit_debit_currency = "debit", "debit_in_account_currency"
@@ -291,31 +224,20 @@ def get_already_booked_amount(doc, item):
total_credit_debit, total_credit_debit_currency = "credit", "credit_in_account_currency" total_credit_debit, total_credit_debit_currency = "credit", "credit_in_account_currency"
deferred_account = "deferred_expense_account" 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 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 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 group by voucher_detail_no
""".format( '''.format(total_credit_debit, total_credit_debit_currency),
total_credit_debit, total_credit_debit_currency (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,
)
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 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 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 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 and p.docstatus < 2 group by reference_detail_no
""".format( '''.format(total_credit_debit, total_credit_debit_currency),
total_credit_debit, total_credit_debit_currency (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,
)
already_booked_amount = gl_entries_details[0].total_credit if gl_entries_details else 0 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 already_booked_amount += journal_entry_details[0].total_credit if journal_entry_details else 0
@@ -323,29 +245,20 @@ def get_already_booked_amount(doc, item):
if doc.currency == doc.company_currency: if doc.currency == doc.company_currency:
already_booked_amount_in_account_currency = already_booked_amount already_booked_amount_in_account_currency = already_booked_amount
else: else:
already_booked_amount_in_account_currency = ( already_booked_amount_in_account_currency = gl_entries_details[0].total_credit_in_account_currency if gl_entries_details else 0
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 += (
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 return already_booked_amount, already_booked_amount_in_account_currency
def book_deferred_income_or_expense(doc, deferred_process, posting_date=None): def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
enable_check = ( enable_check = "enable_deferred_revenue" \
"enable_deferred_revenue" if doc.doctype == "Sales Invoice" else "enable_deferred_expense" 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( def _book_deferred_revenue_or_expense(item, via_journal_entry, submit_journal_entry, book_deferred_entries_based_on):
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) start_date, end_date, last_gl_entry = get_booking_dates(doc, item, posting_date=posting_date)
if not (start_date and end_date): if not (start_date and end_date): return
return
account_currency = get_account_currency(item.expense_account or item.income_account) account_currency = get_account_currency(item.expense_account or item.income_account)
if doc.doctype == "Sales Invoice": if doc.doctype == "Sales Invoice":
@@ -358,179 +271,107 @@ 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_days = date_diff(item.service_end_date, item.service_start_date) + 1
total_booking_days = date_diff(end_date, start_date) + 1 total_booking_days = date_diff(end_date, start_date) + 1
if book_deferred_entries_based_on == "Months": if book_deferred_entries_based_on == 'Months':
amount, base_amount = calculate_monthly_amount( amount, base_amount = calculate_monthly_amount(doc, item, last_gl_entry,
doc, start_date, end_date, total_days, total_booking_days, account_currency)
item,
last_gl_entry,
start_date,
end_date,
total_days,
total_booking_days,
account_currency,
)
else: else:
amount, base_amount = calculate_amount( amount, base_amount = calculate_amount(doc, item, last_gl_entry,
doc, item, last_gl_entry, total_days, total_booking_days, account_currency total_days, total_booking_days, account_currency)
)
if not amount: if not amount:
return return
# check if books nor frozen till endate: # check if books nor frozen till endate:
if accounts_frozen_upto and (end_date) <= getdate(accounts_frozen_upto): if getdate(end_date) >= getdate(accounts_frozen_upto):
end_date = get_last_day(add_days(accounts_frozen_upto, 1)) end_date = get_last_day(add_days(accounts_frozen_upto, 1))
if via_journal_entry: if via_journal_entry:
book_revenue_via_journal_entry( book_revenue_via_journal_entry(doc, credit_account, debit_account, against, amount,
doc, base_amount, end_date, project, account_currency, item.cost_center, item, deferred_process, submit_journal_entry)
credit_account,
debit_account,
amount,
base_amount,
end_date,
project,
account_currency,
item.cost_center,
item,
deferred_process,
submit_journal_entry,
)
else: else:
make_gl_entries( make_gl_entries(doc, credit_account, debit_account, against,
doc, amount, base_amount, end_date, project, account_currency, item.cost_center, item, deferred_process)
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 # 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: if frappe.flags.deferred_accounting_error:
return return
if getdate(end_date) < getdate(posting_date) and not last_gl_entry: if getdate(end_date) < getdate(posting_date) and not last_gl_entry:
_book_deferred_revenue_or_expense( _book_deferred_revenue_or_expense(item, via_journal_entry, submit_journal_entry, book_deferred_entries_based_on)
item, via_journal_entry, submit_journal_entry, book_deferred_entries_based_on
)
via_journal_entry = cint( via_journal_entry = cint(frappe.db.get_singles_value('Accounts Settings', 'book_deferred_entries_via_journal_entry'))
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')
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): if item.get(enable_check):
_book_deferred_revenue_or_expense( _book_deferred_revenue_or_expense(item, via_journal_entry, submit_journal_entry, book_deferred_entries_based_on)
item, via_journal_entry, submit_journal_entry, book_deferred_entries_based_on
)
def process_deferred_accounting(posting_date=None): def process_deferred_accounting(posting_date=None):
"""Converts deferred income/expense into income/expense ''' Converts deferred income/expense into income/expense
Executed via background jobs on every month end""" Executed via background jobs on every month end '''
if not posting_date: if not posting_date:
posting_date = today() posting_date = today()
if not cint( if not cint(frappe.db.get_singles_value('Accounts Settings', 'automatically_process_deferred_accounting_entry')):
frappe.db.get_singles_value(
"Accounts Settings", "automatically_process_deferred_accounting_entry"
)
):
return return
start_date = add_months(today(), -1) start_date = add_months(today(), -1)
end_date = add_days(today(), -1) end_date = add_days(today(), -1)
companies = frappe.get_all("Company") companies = frappe.get_all('Company')
for company in companies: for company in companies:
for record_type in ("Income", "Expense"): for record_type in ('Income', 'Expense'):
doc = frappe.get_doc( doc = frappe.get_doc(dict(
dict( doctype='Process Deferred Accounting',
doctype="Process Deferred Accounting",
company=company.name, company=company.name,
posting_date=posting_date, posting_date=posting_date,
start_date=start_date, start_date=start_date,
end_date=end_date, end_date=end_date,
type=record_type, type=record_type
) ))
)
doc.insert() doc.insert()
doc.submit() doc.submit()
def make_gl_entries(doc, credit_account, debit_account, against,
def make_gl_entries( amount, base_amount, posting_date, project, account_currency, cost_center, item, deferred_process=None):
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 # GL Entry for crediting the amount in the deferred expense
from erpnext.accounts.general_ledger import make_gl_entries from erpnext.accounts.general_ledger import make_gl_entries
if amount == 0: if amount == 0: return
return
gl_entries = [] gl_entries = []
gl_entries.append( gl_entries.append(
doc.get_gl_dict( doc.get_gl_dict({
{
"account": credit_account, "account": credit_account,
"against": against, "against": against,
"credit": base_amount, "credit": base_amount,
"credit_in_account_currency": amount, "credit_in_account_currency": amount,
"cost_center": cost_center, "cost_center": cost_center,
"voucher_detail_no": item.name, "voucher_detail_no": item.name,
"posting_date": posting_date, 'posting_date': posting_date,
"project": project, 'project': project,
"against_voucher_type": "Process Deferred Accounting", 'against_voucher_type': 'Process Deferred Accounting',
"against_voucher": deferred_process, 'against_voucher': deferred_process
}, }, account_currency, item=item)
account_currency,
item=item,
)
) )
# GL Entry to debit the amount from the expense # GL Entry to debit the amount from the expense
gl_entries.append( gl_entries.append(
doc.get_gl_dict( doc.get_gl_dict({
{
"account": debit_account, "account": debit_account,
"against": against, "against": against,
"debit": base_amount, "debit": base_amount,
"debit_in_account_currency": amount, "debit_in_account_currency": amount,
"cost_center": cost_center, "cost_center": cost_center,
"voucher_detail_no": item.name, "voucher_detail_no": item.name,
"posting_date": posting_date, 'posting_date': posting_date,
"project": project, 'project': project,
"against_voucher_type": "Process Deferred Accounting", 'against_voucher_type': 'Process Deferred Accounting',
"against_voucher": deferred_process, 'against_voucher': deferred_process
}, }, account_currency, item=item)
account_currency,
item=item,
)
) )
if gl_entries: if gl_entries:
@@ -539,81 +380,69 @@ def make_gl_entries(
frappe.db.commit() frappe.db.commit()
except Exception as e: except Exception as e:
if frappe.flags.in_test: if frappe.flags.in_test:
doc.log_error(f"Error while processing deferred accounting for Invoice {doc.name}") traceback = frappe.get_traceback()
frappe.log_error(title=_('Error while processing deferred accounting for Invoice {0}').format(doc.name), message=traceback)
raise e raise e
else: else:
frappe.db.rollback() frappe.db.rollback()
doc.log_error(f"Error while processing deferred accounting for Invoice {doc.name}") traceback = frappe.get_traceback()
frappe.log_error(title=_('Error while processing deferred accounting for Invoice {0}').format(doc.name), message=traceback)
frappe.flags.deferred_accounting_error = True frappe.flags.deferred_accounting_error = True
def send_mail(deferred_process): def send_mail(deferred_process):
title = _("Error while processing deferred accounting for {0}").format(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 = _("Deferred accounting failed for some invoices:") + "\n"
content += _( content += _("Please check Process Deferred Accounting {0} and submit manually after resolving errors.").format(link)
"Please check Process Deferred Accounting {0} and submit manually after resolving errors."
).format(link)
sendmail_to_system_managers(title, content) 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'):
def book_revenue_via_journal_entry( if amount == 0: return
doc,
credit_account,
debit_account,
amount,
base_amount,
posting_date,
project,
account_currency,
cost_center,
item,
deferred_process=None,
submit="No",
):
if amount == 0: journal_entry = frappe.new_doc('Journal Entry')
return
journal_entry = frappe.new_doc("Journal Entry")
journal_entry.posting_date = posting_date journal_entry.posting_date = posting_date
journal_entry.company = doc.company journal_entry.company = doc.company
journal_entry.voucher_type = ( journal_entry.voucher_type = 'Deferred Revenue' if doc.doctype == 'Sales Invoice' \
"Deferred Revenue" if doc.doctype == "Sales Invoice" else "Deferred Expense" else 'Deferred Expense'
)
journal_entry.process_deferred_accounting = deferred_process
debit_entry = { debit_entry = {
"account": credit_account, 'account': credit_account,
"credit": base_amount, 'credit': base_amount,
"credit_in_account_currency": amount, 'credit_in_account_currency': amount,
"account_currency": account_currency, 'account_currency': account_currency,
"reference_name": doc.name, 'reference_name': doc.name,
"reference_type": doc.doctype, 'reference_type': doc.doctype,
"reference_detail_no": item.name, 'reference_detail_no': item.name,
"cost_center": cost_center, 'cost_center': cost_center,
"project": project, 'project': project,
} }
credit_entry = { credit_entry = {
"account": debit_account, 'account': debit_account,
"debit": base_amount, 'debit': base_amount,
"debit_in_account_currency": amount, 'debit_in_account_currency': amount,
"account_currency": account_currency, 'account_currency': account_currency,
"reference_name": doc.name, 'reference_name': doc.name,
"reference_type": doc.doctype, 'reference_type': doc.doctype,
"reference_detail_no": item.name, 'reference_detail_no': item.name,
"cost_center": cost_center, 'cost_center': cost_center,
"project": project, 'project': project,
} }
for dimension in get_accounting_dimensions(): 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', debit_entry)
journal_entry.append("accounts", credit_entry) journal_entry.append('accounts', credit_entry)
try: try:
journal_entry.save() journal_entry.save()
@@ -624,26 +453,21 @@ def book_revenue_via_journal_entry(
frappe.db.commit() frappe.db.commit()
except Exception: except Exception:
frappe.db.rollback() frappe.db.rollback()
doc.log_error(f"Error while processing deferred accounting for Invoice {doc.name}") traceback = frappe.get_traceback()
frappe.flags.deferred_accounting_error = True frappe.log_error(title=_('Error while processing deferred accounting for Invoice {0}').format(doc.name), message=traceback)
frappe.flags.deferred_accounting_error = True
def get_deferred_booking_accounts(doctype, voucher_detail_no, dr_or_cr): def get_deferred_booking_accounts(doctype, voucher_detail_no, dr_or_cr):
if doctype == "Sales Invoice": if doctype == 'Sales Invoice':
credit_account, debit_account = frappe.db.get_value( credit_account, debit_account = frappe.db.get_value('Sales Invoice Item', {'name': voucher_detail_no},
"Sales Invoice Item", ['income_account', 'deferred_revenue_account'])
{"name": voucher_detail_no},
["income_account", "deferred_revenue_account"],
)
else: else:
credit_account, debit_account = frappe.db.get_value( credit_account, debit_account = frappe.db.get_value('Purchase Invoice Item', {'name': voucher_detail_no},
"Purchase Invoice Item", ['deferred_expense_account', 'expense_account'])
{"name": voucher_detail_no},
["deferred_expense_account", "expense_account"],
)
if dr_or_cr == "Debit": if dr_or_cr == 'Debit':
return debit_account return debit_account
else: else:
return credit_account return credit_account

View File

@@ -10,17 +10,11 @@ from frappe.utils.nestedset import NestedSet, get_ancestors_of, get_descendants_
import erpnext import erpnext
class RootNotEditable(frappe.ValidationError): class RootNotEditable(frappe.ValidationError): pass
pass class BalanceMismatchError(frappe.ValidationError): pass
class BalanceMismatchError(frappe.ValidationError):
pass
class Account(NestedSet): class Account(NestedSet):
nsm_parent_field = "parent_account" nsm_parent_field = 'parent_account'
def on_update(self): def on_update(self):
if frappe.local.flags.ignore_update_nsm: if frappe.local.flags.ignore_update_nsm:
return return
@@ -28,20 +22,17 @@ class Account(NestedSet):
super(Account, self).on_update() super(Account, self).on_update()
def onload(self): def onload(self):
frozen_accounts_modifier = frappe.db.get_value( frozen_accounts_modifier = frappe.db.get_value("Accounts Settings", "Accounts Settings",
"Accounts Settings", "Accounts Settings", "frozen_accounts_modifier" "frozen_accounts_modifier")
)
if not frozen_accounts_modifier or frozen_accounts_modifier in frappe.get_roles(): if not frozen_accounts_modifier or frozen_accounts_modifier in frappe.get_roles():
self.set_onload("can_freeze_account", True) self.set_onload("can_freeze_account", True)
def autoname(self): def autoname(self):
from erpnext.accounts.utils import get_autoname_with_number from erpnext.accounts.utils import get_autoname_with_number
self.name = get_autoname_with_number(self.account_number, self.account_name, None, self.company)
self.name = get_autoname_with_number(self.account_number, self.account_name, self.company)
def validate(self): def validate(self):
from erpnext.accounts.utils import validate_field_number from erpnext.accounts.utils import validate_field_number
if frappe.local.flags.allow_unverified_charts: if frappe.local.flags.allow_unverified_charts:
return return
self.validate_parent() self.validate_parent()
@@ -58,33 +49,22 @@ class Account(NestedSet):
def validate_parent(self): def validate_parent(self):
"""Fetch Parent Details and validate parent account""" """Fetch Parent Details and validate parent account"""
if self.parent_account: if self.parent_account:
par = frappe.get_cached_value( par = frappe.db.get_value("Account", self.parent_account,
"Account", self.parent_account, ["name", "is_group", "company"], as_dict=1 ["name", "is_group", "company"], as_dict=1)
)
if not par: if not par:
throw( throw(_("Account {0}: Parent account {1} does not exist").format(self.name, self.parent_account))
_("Account {0}: Parent account {1} does not exist").format(self.name, self.parent_account)
)
elif par.name == self.name: elif par.name == self.name:
throw(_("Account {0}: You can not assign itself as parent account").format(self.name)) throw(_("Account {0}: You can not assign itself as parent account").format(self.name))
elif not par.is_group: elif not par.is_group:
throw( throw(_("Account {0}: Parent account {1} can not be a ledger").format(self.name, self.parent_account))
_("Account {0}: Parent account {1} can not be a ledger").format(
self.name, self.parent_account
)
)
elif par.company != self.company: elif par.company != self.company:
throw( throw(_("Account {0}: Parent account {1} does not belong to company: {2}")
_("Account {0}: Parent account {1} does not belong to company: {2}").format( .format(self.name, self.parent_account, self.company))
self.name, self.parent_account, self.company
)
)
def set_root_and_report_type(self): def set_root_and_report_type(self):
if self.parent_account: if self.parent_account:
par = frappe.get_cached_value( par = frappe.db.get_value("Account", self.parent_account,
"Account", self.parent_account, ["report_type", "root_type"], as_dict=1 ["report_type", "root_type"], as_dict=1)
)
if par.report_type: if par.report_type:
self.report_type = par.report_type self.report_type = par.report_type
@@ -92,57 +72,45 @@ class Account(NestedSet):
self.root_type = par.root_type self.root_type = par.root_type
if self.is_group: if self.is_group:
db_value = self.get_doc_before_save() db_value = frappe.db.get_value("Account", self.name, ["report_type", "root_type"], as_dict=1)
if db_value: if db_value:
if self.report_type != db_value.report_type: if self.report_type != db_value.report_type:
frappe.db.sql( frappe.db.sql("update `tabAccount` set report_type=%s where lft > %s and rgt < %s",
"update `tabAccount` set report_type=%s where lft > %s and rgt < %s", (self.report_type, self.lft, self.rgt))
(self.report_type, self.lft, self.rgt),
)
if self.root_type != db_value.root_type: if self.root_type != db_value.root_type:
frappe.db.sql( frappe.db.sql("update `tabAccount` set root_type=%s where lft > %s and rgt < %s",
"update `tabAccount` set root_type=%s where lft > %s and rgt < %s", (self.root_type, self.lft, self.rgt))
(self.root_type, self.lft, self.rgt),
)
if self.root_type and not self.report_type: if self.root_type and not self.report_type:
self.report_type = ( self.report_type = "Balance Sheet" \
"Balance Sheet" if self.root_type in ("Asset", "Liability", "Equity") else "Profit and Loss" if self.root_type in ("Asset", "Liability", "Equity") else "Profit and Loss"
)
def validate_root_details(self): def validate_root_details(self):
doc_before_save = self.get_doc_before_save() # does not exists parent
if frappe.db.exists("Account", self.name):
if doc_before_save and not doc_before_save.parent_account: if not frappe.db.get_value("Account", self.name, "parent_account"):
throw(_("Root cannot be edited."), RootNotEditable) throw(_("Root cannot be edited."), RootNotEditable)
if not self.parent_account and not self.is_group: if not self.parent_account and not self.is_group:
throw(_("The root account {0} must be a group").format(frappe.bold(self.name))) frappe.throw(_("The root account {0} must be a group").format(frappe.bold(self.name)))
def validate_root_company_and_sync_account_to_children(self): def validate_root_company_and_sync_account_to_children(self):
# ignore validation while creating new compnay or while syncing to child companies # ignore validation while creating new compnay or while syncing to child companies
if ( if frappe.local.flags.ignore_root_company_validation or self.flags.ignore_root_company_validation:
frappe.local.flags.ignore_root_company_validation or self.flags.ignore_root_company_validation
):
return return
ancestors = get_root_company(self.company) ancestors = get_root_company(self.company)
if ancestors: if ancestors:
if frappe.get_cached_value( if frappe.get_value("Company", self.company, "allow_account_creation_against_child_company"):
"Company", self.company, "allow_account_creation_against_child_company"
):
return return
if not frappe.db.get_value( if not frappe.db.get_value("Account",
"Account", {"account_name": self.account_name, "company": ancestors[0]}, "name" {'account_name': self.account_name, 'company': ancestors[0]}, 'name'):
):
frappe.throw(_("Please add the account to root level Company - {}").format(ancestors[0])) frappe.throw(_("Please add the account to root level Company - {}").format(ancestors[0]))
elif self.parent_account: elif self.parent_account:
descendants = get_descendants_of("Company", self.company) descendants = get_descendants_of('Company', self.company)
if not descendants: if not descendants: return
return
parent_acc_name_map = {} parent_acc_name_map = {}
parent_acc_name, parent_acc_number = frappe.get_cached_value( parent_acc_name, parent_acc_number = frappe.db.get_value('Account', self.parent_account, \
"Account", self.parent_account, ["account_name", "account_number"] ["account_name", "account_number"])
)
filters = { filters = {
"company": ["in", descendants], "company": ["in", descendants],
"account_name": parent_acc_name, "account_name": parent_acc_name,
@@ -150,21 +118,19 @@ class Account(NestedSet):
if parent_acc_number: if parent_acc_number:
filters["account_number"] = parent_acc_number filters["account_number"] = parent_acc_number
for d in frappe.db.get_values( for d in frappe.db.get_values('Account', filters=filters, fieldname=["company", "name"], as_dict=True):
"Account", filters=filters, fieldname=["company", "name"], as_dict=True
):
parent_acc_name_map[d["company"]] = d["name"] parent_acc_name_map[d["company"]] = d["name"]
if not parent_acc_name_map: if not parent_acc_name_map: return
return
self.create_account_for_child_company(parent_acc_name_map, descendants, parent_acc_name) self.create_account_for_child_company(parent_acc_name_map, descendants, parent_acc_name)
def validate_group_or_ledger(self): def validate_group_or_ledger(self):
doc_before_save = self.get_doc_before_save() if self.get("__islocal"):
if not doc_before_save or cint(doc_before_save.is_group) == cint(self.is_group):
return return
existing_is_group = frappe.db.get_value("Account", self.name, "is_group")
if cint(self.is_group) != cint(existing_is_group):
if self.check_gle_exists(): if self.check_gle_exists():
throw(_("Account with existing transaction cannot be converted to ledger")) throw(_("Account with existing transaction cannot be converted to ledger"))
elif self.is_group: elif self.is_group:
@@ -174,42 +140,28 @@ class Account(NestedSet):
throw(_("Account with child nodes cannot be set as ledger")) throw(_("Account with child nodes cannot be set as ledger"))
def validate_frozen_accounts_modifier(self): def validate_frozen_accounts_modifier(self):
doc_before_save = self.get_doc_before_save() old_value = frappe.db.get_value("Account", self.name, "freeze_account")
if not doc_before_save or doc_before_save.freeze_account == self.freeze_account: if old_value and old_value != self.freeze_account:
return frozen_accounts_modifier = frappe.db.get_value('Accounts Settings', None, 'frozen_accounts_modifier')
if not frozen_accounts_modifier or \
frozen_accounts_modifier = frappe.get_cached_value( frozen_accounts_modifier not in frappe.get_roles():
"Accounts Settings", "Accounts Settings", "frozen_accounts_modifier"
)
if not frozen_accounts_modifier or frozen_accounts_modifier not in frappe.get_roles():
throw(_("You are not authorized to set Frozen value")) throw(_("You are not authorized to set Frozen value"))
def validate_balance_must_be_debit_or_credit(self): def validate_balance_must_be_debit_or_credit(self):
from erpnext.accounts.utils import get_balance_on from erpnext.accounts.utils import get_balance_on
if not self.get("__islocal") and self.balance_must_be: if not self.get("__islocal") and self.balance_must_be:
account_balance = get_balance_on(self.name) account_balance = get_balance_on(self.name)
if account_balance > 0 and self.balance_must_be == "Credit": if account_balance > 0 and self.balance_must_be == "Credit":
frappe.throw( frappe.throw(_("Account balance already in Debit, you are not allowed to set 'Balance Must Be' as 'Credit'"))
_(
"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": elif account_balance < 0 and self.balance_must_be == "Debit":
frappe.throw( frappe.throw(_("Account balance already in Credit, you are not allowed to set 'Balance Must Be' as 'Debit'"))
_(
"Account balance already in Credit, you are not allowed to set 'Balance Must Be' as 'Debit'"
)
)
def validate_account_currency(self): def validate_account_currency(self):
if not self.account_currency: 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")
gl_currency = frappe.db.get_value("GL Entry", {"account": self.name}, "account_currency") elif self.account_currency != frappe.db.get_value("Account", self.name, "account_currency"):
if gl_currency and self.account_currency != gl_currency:
if frappe.db.get_value("GL Entry", {"account": self.name}): if frappe.db.get_value("GL Entry", {"account": self.name}):
frappe.throw(_("Currency can not be changed after making entries using some other currency")) frappe.throw(_("Currency can not be changed after making entries using some other currency"))
@@ -218,52 +170,45 @@ class Account(NestedSet):
company_bold = frappe.bold(company) company_bold = frappe.bold(company)
parent_acc_name_bold = frappe.bold(parent_acc_name) parent_acc_name_bold = frappe.bold(parent_acc_name)
if not parent_acc_name_map.get(company): if not parent_acc_name_map.get(company):
frappe.throw( 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"))
"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 # validate if parent of child company account to be added is a group
if frappe.get_cached_value( if (frappe.db.get_value("Account", self.parent_account, "is_group")
"Account", self.parent_account, "is_group" and not frappe.db.get_value("Account", parent_acc_name_map[company], "is_group")):
) and not frappe.get_cached_value("Account", parent_acc_name_map[company], "is_group"): msg = _("While creating account for Child Company {0}, parent account {1} found as a ledger account.").format(company_bold, parent_acc_name_bold)
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 += "<br><br>"
msg += _( msg += _("Please convert the parent account in corresponding child company to a group account.")
"Please convert the parent account in corresponding child company to a group account."
)
frappe.throw(msg, title=_("Invalid Parent 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: if self.account_number:
filters["account_number"] = 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: if not child_account:
doc = frappe.copy_doc(self) doc = frappe.copy_doc(self)
doc.flags.ignore_root_company_validation = True doc.flags.ignore_root_company_validation = True
doc.update( doc.update({
{
"company": company, "company": company,
# parent account's currency should be passed down to child account's curreny # 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 # if it is None, it picks it up from default company currency, which might be unintended
"account_currency": erpnext.get_company_currency(company), "account_currency": erpnext.get_company_currency(company),
"parent_account": parent_acc_name_map[company], "parent_account": parent_acc_name_map[company]
} })
)
doc.save() 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: elif child_account:
# update the parent company's value in child companies # update the parent company's value in child companies
doc = frappe.get_doc("Account", child_account) doc = frappe.get_doc("Account", child_account)
parent_value_changed = False 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): if doc.get(field) != self.get(field):
parent_value_changed = True parent_value_changed = True
doc.set(field, self.get(field)) doc.set(field, self.get(field))
@@ -298,11 +243,8 @@ class Account(NestedSet):
return frappe.db.get_value("GL Entry", {"account": self.name}) return frappe.db.get_value("GL Entry", {"account": self.name})
def check_if_child_exists(self): def check_if_child_exists(self):
return frappe.db.sql( return frappe.db.sql("""select name from `tabAccount` where parent_account = %s
"""select name from `tabAccount` where parent_account = %s and docstatus != 2""", self.name)
and docstatus != 2""",
self.name,
)
def validate_mandatory(self): def validate_mandatory(self):
if not self.root_type: if not self.root_type:
@@ -318,97 +260,73 @@ class Account(NestedSet):
super(Account, self).on_trash(True) super(Account, self).on_trash(True)
@frappe.whitelist() @frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs @frappe.validate_and_sanitize_search_inputs
def get_parent_account(doctype, txt, searchfield, start, page_len, filters): def get_parent_account(doctype, txt, searchfield, start, page_len, filters):
return frappe.db.sql( return frappe.db.sql("""select name from tabAccount
"""select name from tabAccount
where is_group = 1 and docstatus != 2 and company = %s where is_group = 1 and docstatus != 2 and company = %s
and %s like %s order by name limit %s offset %s""" and %s like %s order by name limit %s, %s""" %
% ("%s", searchfield, "%s", "%s", "%s"), ("%s", searchfield, "%s", "%s", "%s"),
(filters["company"], "%%%s%%" % txt, page_len, start), (filters["company"], "%%%s%%" % txt, start, page_len), as_list=1)
as_list=1,
)
def get_account_currency(account): def get_account_currency(account):
"""Helper function to get account currency""" """Helper function to get account currency"""
if not account: if not account:
return return
def generator(): def generator():
account_currency, company = frappe.get_cached_value( account_currency, company = frappe.get_cached_value("Account", account, ["account_currency", "company"])
"Account", account, ["account_currency", "company"]
)
if not account_currency: 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 account_currency
return frappe.local_cache("account_currency", account, generator) return frappe.local_cache("account_currency", account, generator)
def on_doctype_update(): def on_doctype_update():
frappe.db.add_index("Account", ["lft", "rgt"]) frappe.db.add_index("Account", ["lft", "rgt"])
def get_account_autoname(account_number, account_name, company): def get_account_autoname(account_number, account_name, company):
# first validate if company exists # 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: 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] parts = [account_name.strip(), company.abbr]
if cstr(account_number).strip(): if cstr(account_number).strip():
parts.insert(0, 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): def validate_account_number(name, account_number, company):
if account_number: if account_number:
account_with_same_number = frappe.db.get_value( account_with_same_number = frappe.db.get_value("Account",
"Account", {"account_number": account_number, "company": company, "name": ["!=", name]} {"account_number": account_number, "company": company, "name": ["!=", name]})
)
if account_with_same_number: if account_with_same_number:
frappe.throw( frappe.throw(_("Account Number {0} already used in account {1}")
_("Account Number {0} already used in account {1}").format( .format(account_number, account_with_same_number))
account_number, account_with_same_number
)
)
@frappe.whitelist() @frappe.whitelist()
def update_account_number(name, account_name, account_number=None, from_descendant=False): def update_account_number(name, account_name, account_number=None, from_descendant=False):
account = frappe.get_cached_doc("Account", name) account = frappe.db.get_value("Account", name, "company", as_dict=True)
if not account: if not account: return
return
old_acc_name, old_acc_number = account.account_name, account.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 # check if account exists in parent company
ancestors = get_ancestors_of("Company", account.company) ancestors = get_ancestors_of("Company", account.company)
allow_independent_account_creation = frappe.get_cached_value( allow_independent_account_creation = frappe.get_value("Company", account.company, "allow_account_creation_against_child_company")
"Company", account.company, "allow_account_creation_against_child_company"
)
if ancestors and not allow_independent_account_creation: if ancestors and not allow_independent_account_creation:
for ancestor in ancestors: 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 # same account in parent company exists
allow_child_account_creation = _("Allow Account Creation Against Child Company") allow_child_account_creation = _("Allow Account Creation Against Child Company")
message = _("Account {0} exists in parent company {1}.").format( message = _("Account {0} exists in parent company {1}.").format(frappe.bold(old_acc_name), frappe.bold(ancestor))
frappe.bold(old_acc_name), frappe.bold(ancestor)
)
message += "<br>" message += "<br>"
message += _("Renaming it is only allowed via parent company {0}, to avoid mismatch.").format( message += _("Renaming it is only allowed via parent company {0}, to avoid mismatch.").format(frappe.bold(ancestor))
frappe.bold(ancestor)
)
message += "<br><br>" message += "<br><br>"
message += _("To overrule this, enable '{0}' in company {1}").format( message += _("To overrule this, enable '{0}' in company {1}").format(allow_child_account_creation, frappe.bold(account.company))
allow_child_account_creation, frappe.bold(account.company)
)
frappe.throw(message, title=_("Rename Not Allowed")) frappe.throw(message, title=_("Rename Not Allowed"))
@@ -421,55 +339,42 @@ def update_account_number(name, account_name, account_number=None, from_descenda
if not from_descendant: if not from_descendant:
# Update and rename in child company accounts as well # 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: if descendants:
sync_update_account_number_in_child( sync_update_account_number_in_child(descendants, old_acc_name, account_name, account_number, old_acc_number)
descendants, old_acc_name, account_name, account_number, old_acc_number
)
new_name = get_account_autoname(account_number, account_name, account.company) new_name = get_account_autoname(account_number, account_name, account.company)
if name != new_name: if name != new_name:
frappe.rename_doc("Account", name, new_name, force=1) frappe.rename_doc("Account", name, new_name, force=1)
return new_name return new_name
@frappe.whitelist() @frappe.whitelist()
def merge_account(old, new, is_group, root_type, company): def merge_account(old, new, is_group, root_type, company):
# Validate properties before merging # Validate properties before merging
new_account = frappe.get_cached_doc("Account", new) if not frappe.db.exists("Account", new):
if not new_account:
throw(_("Account {0} does not exist").format(new)) throw(_("Account {0} does not exist").format(new))
if (new_account.is_group, new_account.root_type, new_account.company) != ( val = list(frappe.db.get_value("Account", new,
cint(is_group), ["is_group", "root_type", "company"]))
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 new_account.parent_account == old: if val != [cint(is_group), root_type, company]:
new_account.db_set("parent_account", frappe.get_cached_value("Account", old, "parent_account")) 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.rename_doc("Account", old, new, merge=1, force=1) frappe.rename_doc("Account", old, new, merge=1, force=1)
return new return new
@frappe.whitelist() @frappe.whitelist()
def get_root_company(company): def get_root_company(company):
# return the topmost company in the hierarchy # 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 [] 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 = { filters = {
"company": ["in", descendants], "company": ["in", descendants],
"account_name": old_acc_name, "account_name": old_acc_name,
@@ -477,7 +382,5 @@ def sync_update_account_number_in_child(
if old_acc_number: if old_acc_number:
filters["account_number"] = old_acc_number filters["account_number"] = old_acc_number
for d in frappe.db.get_values( for d in frappe.db.get_values('Account', filters=filters, fieldname=["company", "name"], as_dict=True):
"Account", filters=filters, fieldname=["company", "name"], as_dict=True
):
update_account_number(d["name"], account_name, account_number, from_descendant=True) update_account_number(d["name"], account_name, account_number, from_descendant=True)

View File

@@ -160,7 +160,7 @@ frappe.treeview_settings["Account"] = {
let root_company = treeview.page.fields_dict.root_company.get_value(); let root_company = treeview.page.fields_dict.root_company.get_value();
if(root_company) { if(root_company) {
frappe.throw(__("Please add the account to root level Company - {0}"), [root_company]); frappe.throw(__("Please add the account to root level Company - ") + root_company);
} else { } else {
treeview.new_node(); treeview.new_node();
} }

View File

@@ -10,9 +10,7 @@ from frappe.utils.nestedset import rebuild_tree
from unidecode import unidecode from unidecode import unidecode
def create_charts( def create_charts(company, chart_template=None, existing_company=None, custom_chart=None, from_coa_importer=None):
company, chart_template=None, existing_company=None, custom_chart=None, from_coa_importer=None
):
chart = custom_chart or get_chart(chart_template, existing_company) chart = custom_chart or get_chart(chart_template, existing_company)
if chart: if chart:
accounts = [] accounts = []
@@ -22,29 +20,20 @@ def create_charts(
if root_account: if root_account:
root_type = child.get("root_type") root_type = child.get("root_type")
if account_name not in [ if account_name not in ["account_name", "account_number", "account_type",
"account_name", "root_type", "is_group", "tax_rate"]:
"account_number",
"account_type",
"root_type",
"is_group",
"tax_rate",
]:
account_number = cstr(child.get("account_number")).strip() account_number = cstr(child.get("account_number")).strip()
account_name, account_name_in_db = add_suffix_if_duplicate( account_name, account_name_in_db = add_suffix_if_duplicate(account_name,
account_name, account_number, accounts account_number, accounts)
)
is_group = identify_is_group(child) is_group = identify_is_group(child)
report_type = ( report_type = "Balance Sheet" if root_type in ["Asset", "Liability", "Equity"] \
"Balance Sheet" if root_type in ["Asset", "Liability", "Equity"] else "Profit and Loss" else "Profit and Loss"
)
account = frappe.get_doc( account = frappe.get_doc({
{
"doctype": "Account", "doctype": "Account",
"account_name": child.get("account_name") if from_coa_importer else account_name, "account_name": child.get('account_name') if from_coa_importer else account_name,
"company": company, "company": company,
"parent_account": parent, "parent_account": parent,
"is_group": is_group, "is_group": is_group,
@@ -52,11 +41,9 @@ def create_charts(
"report_type": report_type, "report_type": report_type,
"account_number": account_number, "account_number": account_number,
"account_type": child.get("account_type"), "account_type": child.get("account_type"),
"account_currency": child.get("account_currency") "account_currency": child.get('account_currency') or frappe.db.get_value('Company', company, "default_currency"),
or frappe.get_cached_value("Company", company, "default_currency"), "tax_rate": child.get("tax_rate")
"tax_rate": child.get("tax_rate"), })
}
)
if root_account or frappe.local.flags.allow_unverified_charts: if root_account or frappe.local.flags.allow_unverified_charts:
account.flags.ignore_mandatory = True account.flags.ignore_mandatory = True
@@ -76,10 +63,10 @@ def create_charts(
rebuild_tree("Account", "parent_account") rebuild_tree("Account", "parent_account")
frappe.local.flags.ignore_update_nsm = False frappe.local.flags.ignore_update_nsm = False
def add_suffix_if_duplicate(account_name, account_number, accounts): def add_suffix_if_duplicate(account_name, account_number, accounts):
if account_number: 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: else:
account_name_in_db = unidecode(account_name.strip().lower()) account_name_in_db = unidecode(account_name.strip().lower())
@@ -89,21 +76,16 @@ def add_suffix_if_duplicate(account_name, account_number, accounts):
return account_name, account_name_in_db return account_name, account_name_in_db
def identify_is_group(child): def identify_is_group(child):
if child.get("is_group"): if child.get("is_group"):
is_group = child.get("is_group") is_group = child.get("is_group")
elif len( elif len(set(child.keys()) - set(["account_name", "account_type", "root_type", "is_group", "tax_rate", "account_number"])):
set(child.keys())
- set(["account_name", "account_type", "root_type", "is_group", "tax_rate", "account_number"])
):
is_group = 1 is_group = 1
else: else:
is_group = 0 is_group = 0
return is_group return is_group
def get_chart(chart_template, existing_company=None): def get_chart(chart_template, existing_company=None):
chart = {} chart = {}
if existing_company: if existing_company:
@@ -113,13 +95,11 @@ def get_chart(chart_template, existing_company=None):
from erpnext.accounts.doctype.account.chart_of_accounts.verified import ( from erpnext.accounts.doctype.account.chart_of_accounts.verified import (
standard_chart_of_accounts, standard_chart_of_accounts,
) )
return standard_chart_of_accounts.get() return standard_chart_of_accounts.get()
elif chart_template == "Standard with Numbers": elif chart_template == "Standard with Numbers":
from erpnext.accounts.doctype.account.chart_of_accounts.verified import ( from erpnext.accounts.doctype.account.chart_of_accounts.verified import (
standard_chart_of_accounts_with_account_number, standard_chart_of_accounts_with_account_number,
) )
return standard_chart_of_accounts_with_account_number.get() return standard_chart_of_accounts_with_account_number.get()
else: else:
folders = ("verified",) folders = ("verified",)
@@ -135,7 +115,6 @@ def get_chart(chart_template, existing_company=None):
if chart and json.loads(chart).get("name") == chart_template: if chart and json.loads(chart).get("name") == chart_template:
return json.loads(chart).get("tree") return json.loads(chart).get("tree")
@frappe.whitelist() @frappe.whitelist()
def get_charts_for_country(country, with_standard=False): def get_charts_for_country(country, with_standard=False):
charts = [] charts = []
@@ -143,12 +122,11 @@ def get_charts_for_country(country, with_standard=False):
def _get_chart_name(content): def _get_chart_name(content):
if content: if content:
content = json.loads(content) content = json.loads(content)
if ( if (content and content.get("disabled", "No") == "No") \
content and content.get("disabled", "No") == "No" or frappe.local.flags.allow_unverified_charts:
) or frappe.local.flags.allow_unverified_charts:
charts.append(content["name"]) charts.append(content["name"])
country_code = frappe.get_cached_value("Country", country, "code") country_code = frappe.db.get_value("Country", country, "code")
if country_code: if country_code:
folders = ("verified",) folders = ("verified",)
if frappe.local.flags.allow_unverified_charts: if frappe.local.flags.allow_unverified_charts:
@@ -173,21 +151,11 @@ def get_charts_for_country(country, with_standard=False):
def get_account_tree_from_existing_company(existing_company): def get_account_tree_from_existing_company(existing_company):
all_accounts = frappe.get_all( all_accounts = frappe.get_all('Account',
"Account", filters={'company': existing_company},
filters={"company": existing_company}, fields = ["name", "account_name", "parent_account", "account_type",
fields=[ "is_group", "root_type", "tax_rate", "account_number"],
"name", order_by="lft, rgt")
"account_name",
"parent_account",
"account_type",
"is_group",
"root_type",
"tax_rate",
"account_number",
],
order_by="lft, rgt",
)
account_tree = {} account_tree = {}
@@ -196,7 +164,6 @@ def get_account_tree_from_existing_company(existing_company):
build_account_tree(account_tree, None, all_accounts) build_account_tree(account_tree, None, all_accounts)
return account_tree return account_tree
def build_account_tree(tree, parent, all_accounts): def build_account_tree(tree, parent, all_accounts):
# find children # find children
parent_account = parent.name if parent else "" parent_account = parent.name if parent else ""
@@ -225,29 +192,27 @@ def build_account_tree(tree, parent, all_accounts):
# call recursively to build a subtree for current account # call recursively to build a subtree for current account
build_account_tree(tree[child.account_name], child, all_accounts) build_account_tree(tree[child.account_name], child, all_accounts)
@frappe.whitelist() @frappe.whitelist()
def validate_bank_account(coa, bank_account): def validate_bank_account(coa, bank_account):
accounts = [] accounts = []
chart = get_chart(coa) chart = get_chart(coa)
if chart: if chart:
def _get_account_names(account_master): def _get_account_names(account_master):
for account_name, child in account_master.items(): 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) accounts.append(account_name)
_get_account_names(child) _get_account_names(child)
_get_account_names(chart) _get_account_names(chart)
return bank_account in accounts return (bank_account in accounts)
@frappe.whitelist() @frappe.whitelist()
def build_tree_from_json(chart_template, chart_data=None, from_coa_importer=False): 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) chart = chart_data or get_chart(chart_template)
# if no template selected, return as it is # if no template selected, return as it is
@@ -255,33 +220,22 @@ def build_tree_from_json(chart_template, chart_data=None, from_coa_importer=Fals
return return
accounts = [] accounts = []
def _import_accounts(children, parent): 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(): for account_name, child in children.items():
account = {} account = {}
if account_name in [ if account_name in ["account_name", "account_number", "account_type",\
"account_name", "root_type", "is_group", "tax_rate"]: continue
"account_number",
"account_type",
"root_type",
"is_group",
"tax_rate",
]:
continue
if from_coa_importer: if from_coa_importer:
account_name = child["account_name"] account_name = child['account_name']
account["parent_account"] = parent account['parent_account'] = parent
account["expandable"] = True if identify_is_group(child) else False account['expandable'] = True if identify_is_group(child) else False
account["value"] = ( account['value'] = (cstr(child.get('account_number')).strip() + ' - ' + account_name) \
(cstr(child.get("account_number")).strip() + " - " + account_name) if child.get('account_number') else account_name
if child.get("account_number")
else account_name
)
accounts.append(account) accounts.append(account)
_import_accounts(child, account["value"]) _import_accounts(child, account['value'])
_import_accounts(chart, None) _import_accounts(chart, None)
return accounts return accounts

View File

@@ -20,7 +20,6 @@ charts = {}
all_account_types = [] all_account_types = []
all_roots = {} all_roots = {}
def go(): def go():
global accounts, charts global accounts, charts
default_account_types = get_default_account_types() default_account_types = get_default_account_types()
@@ -35,16 +34,14 @@ def go():
accounts, charts = {}, {} accounts, charts = {}, {}
country_path = os.path.join(path, country_dir) country_path = os.path.join(path, country_dir)
manifest = ast.literal_eval(open(os.path.join(country_path, "__openerp__.py")).read()) manifest = ast.literal_eval(open(os.path.join(country_path, "__openerp__.py")).read())
data_files = ( data_files = manifest.get("data", []) + manifest.get("init_xml", []) + \
manifest.get("data", []) + manifest.get("init_xml", []) + manifest.get("update_xml", []) manifest.get("update_xml", [])
)
files_path = [os.path.join(country_path, d) for d in data_files] files_path = [os.path.join(country_path, d) for d in data_files]
xml_roots = get_xml_roots(files_path) xml_roots = get_xml_roots(files_path)
csv_content = get_csv_contents(files_path) csv_content = get_csv_contents(files_path)
prefix = country_dir if csv_content else None prefix = country_dir if csv_content else None
account_types = get_account_types( account_types = get_account_types(xml_roots.get("account.account.type", []),
xml_roots.get("account.account.type", []), csv_content.get("account.account.type", []), prefix csv_content.get("account.account.type", []), prefix)
)
account_types.update(default_account_types) account_types.update(default_account_types)
if xml_roots: if xml_roots:
@@ -57,15 +54,12 @@ def go():
create_all_roots_file() create_all_roots_file()
def get_default_account_types(): def get_default_account_types():
default_types_root = [] default_types_root = []
default_types_root.append( default_types_root.append(ET.parse(os.path.join(path, "account", "data",
ET.parse(os.path.join(path, "account", "data", "data_account_type.xml")).getroot() "data_account_type.xml")).getroot())
)
return get_account_types(default_types_root, None, prefix="account") return get_account_types(default_types_root, None, prefix="account")
def get_xml_roots(files_path): def get_xml_roots(files_path):
xml_roots = frappe._dict() xml_roots = frappe._dict()
for filepath in files_path: for filepath in files_path:
@@ -74,69 +68,64 @@ def get_xml_roots(files_path):
tree = ET.parse(filepath) tree = ET.parse(filepath)
root = tree.getroot() root = tree.getroot()
for node in root[0].findall("record"): for node in root[0].findall("record"):
if node.get("model") in [ if node.get("model") in ["account.account.template",
"account.account.template", "account.chart.template", "account.account.type"]:
"account.chart.template",
"account.account.type",
]:
xml_roots.setdefault(node.get("model"), []).append(root) xml_roots.setdefault(node.get("model"), []).append(root)
break break
return xml_roots return xml_roots
def get_csv_contents(files_path): def get_csv_contents(files_path):
csv_content = {} csv_content = {}
for filepath in files_path: for filepath in files_path:
fname = os.path.basename(filepath) 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"): if fname.startswith(file_type) and fname.endswith(".csv"):
with open(filepath, "r") as csvfile: with open(filepath, "r") as csvfile:
try: 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: except Exception as e:
continue continue
return csv_content return csv_content
def get_account_types(root_list, csv_content, prefix=None): def get_account_types(root_list, csv_content, prefix=None):
types = {} types = {}
account_type_map = { account_type_map = {
"cash": "Cash", 'cash': 'Cash',
"bank": "Bank", 'bank': 'Bank',
"tr_cash": "Cash", 'tr_cash': 'Cash',
"tr_bank": "Bank", 'tr_bank': 'Bank',
"receivable": "Receivable", 'receivable': 'Receivable',
"tr_receivable": "Receivable", 'tr_receivable': 'Receivable',
"account rec": "Receivable", 'account rec': 'Receivable',
"payable": "Payable", 'payable': 'Payable',
"tr_payable": "Payable", 'tr_payable': 'Payable',
"equity": "Equity", 'equity': 'Equity',
"stocks": "Stock", 'stocks': 'Stock',
"stock": "Stock", 'stock': 'Stock',
"tax": "Tax", 'tax': 'Tax',
"tr_tax": "Tax", 'tr_tax': 'Tax',
"tax-out": "Tax", 'tax-out': 'Tax',
"tax-in": "Tax", 'tax-in': 'Tax',
"charges_personnel": "Chargeable", 'charges_personnel': 'Chargeable',
"fixed asset": "Fixed Asset", 'fixed asset': 'Fixed Asset',
"cogs": "Cost of Goods Sold", 'cogs': 'Cost of Goods Sold',
} }
for root in root_list: for root in root_list:
for node in root[0].findall("record"): for node in root[0].findall("record"):
if node.get("model") == "account.account.type": if node.get("model")=="account.account.type":
data = {} data = {}
for field in node.findall("field"): for field in node.findall("field"):
if ( if field.get("name")=="code" and field.text.lower() != "none" \
field.get("name") == "code" and account_type_map.get(field.text):
and field.text.lower() != "none"
and account_type_map.get(field.text)
):
data["account_type"] = account_type_map[field.text] data["account_type"] = account_type_map[field.text]
node_id = prefix + "." + node.get("id") if prefix else node.get("id") node_id = prefix + "." + node.get("id") if prefix else node.get("id")
types[node_id] = data 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:]: for row in csv_content[1:]:
row_dict = dict(zip(csv_content[0], row)) row_dict = dict(zip(csv_content[0], row))
data = {} data = {}
@@ -147,22 +136,21 @@ def get_account_types(root_list, csv_content, prefix=None):
types[node_id] = data types[node_id] = data
return types return types
def make_maps_for_xml(xml_roots, account_types, country_dir): def make_maps_for_xml(xml_roots, account_types, country_dir):
"""make maps for `charts` and `accounts`""" """make maps for `charts` and `accounts`"""
for model, root_list in xml_roots.items(): for model, root_list in xml_roots.items():
for root in root_list: for root in root_list:
for node in root[0].findall("record"): for node in root[0].findall("record"):
if node.get("model") == "account.account.template": if node.get("model")=="account.account.template":
data = {} data = {}
for field in node.findall("field"): for field in node.findall("field"):
if field.get("name") == "name": if field.get("name")=="name":
data["name"] = field.text 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") parent_id = field.get("ref") or field.get("eval")
data["parent_id"] = parent_id data["parent_id"] = parent_id
if field.get("name") == "user_type": if field.get("name")=="user_type":
value = field.get("ref") value = field.get("ref")
if account_types.get(value, {}).get("account_type"): if account_types.get(value, {}).get("account_type"):
data["account_type"] = account_types[value]["account_type"] data["account_type"] = account_types[value]["account_type"]
@@ -172,17 +160,16 @@ def make_maps_for_xml(xml_roots, account_types, country_dir):
data["children"] = [] data["children"] = []
accounts[node.get("id")] = data accounts[node.get("id")] = data
if node.get("model") == "account.chart.template": if node.get("model")=="account.chart.template":
data = {} data = {}
for field in node.findall("field"): for field in node.findall("field"):
if field.get("name") == "name": if field.get("name")=="name":
data["name"] = field.text 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["account_root_id"] = field.get("ref")
data["id"] = country_dir data["id"] = country_dir
charts.setdefault(node.get("id"), {}).update(data) charts.setdefault(node.get("id"), {}).update(data)
def make_maps_for_csv(csv_content, account_types, country_dir): def make_maps_for_csv(csv_content, account_types, country_dir):
for content in csv_content.get("account.account.template", []): for content in csv_content.get("account.account.template", []):
for row in content[1:]: for row in content[1:]:
@@ -190,7 +177,7 @@ def make_maps_for_csv(csv_content, account_types, country_dir):
account = { account = {
"name": data.get("name"), "name": data.get("name"),
"parent_id": data.get("parent_id:id") or data.get("parent_id/id"), "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") user_type = data.get("user_type/id") or data.get("user_type:id")
if account_types.get(user_type, {}).get("account_type"): if account_types.get(user_type, {}).get("account_type"):
@@ -207,14 +194,12 @@ def make_maps_for_csv(csv_content, account_types, country_dir):
for row in content[1:]: for row in content[1:]:
if row: if row:
data = dict(zip(content[0], row)) data = dict(zip(content[0], row))
charts.setdefault(data.get("id"), {}).update( charts.setdefault(data.get("id"), {}).update({
{ "account_root_id": data.get("account_root_id:id") or \
"account_root_id": data.get("account_root_id:id") or data.get("account_root_id/id"), data.get("account_root_id/id"),
"name": data.get("name"), "name": data.get("name"),
"id": country_dir, "id": country_dir
} })
)
def make_account_trees(): def make_account_trees():
"""build tree hierarchy""" """build tree hierarchy"""
@@ -233,7 +218,6 @@ def make_account_trees():
if "children" in accounts[id] and not accounts[id].get("children"): if "children" in accounts[id] and not accounts[id].get("children"):
del accounts[id]["children"] del accounts[id]["children"]
def make_charts(): def make_charts():
"""write chart files in app/setup/doctype/company/charts""" """write chart files in app/setup/doctype/company/charts"""
for chart_id in charts: for chart_id in charts:
@@ -252,38 +236,34 @@ def make_charts():
chart["country_code"] = src["id"][5:] chart["country_code"] = src["id"][5:]
chart["tree"] = accounts[src["account_root_id"]] chart["tree"] = accounts[src["account_root_id"]]
for key, val in chart["tree"].items(): for key, val in chart["tree"].items():
if key in ["name", "parent_id"]: if key in ["name", "parent_id"]:
chart["tree"].pop(key) chart["tree"].pop(key)
if type(val) == dict: if type(val) == dict:
val["root_type"] = "" val["root_type"] = ""
if chart: if chart:
fpath = os.path.join( fpath = os.path.join("erpnext", "erpnext", "accounts", "doctype", "account",
"erpnext", "erpnext", "accounts", "doctype", "account", "chart_of_accounts", filename + ".json" "chart_of_accounts", filename + ".json")
)
with open(fpath, "r") as chartfile: with open(fpath, "r") as chartfile:
old_content = chartfile.read() old_content = chartfile.read()
if not old_content or ( if not old_content or (json.loads(old_content).get("is_active", "No") == "No" \
json.loads(old_content).get("is_active", "No") == "No" and json.loads(old_content).get("disabled", "No") == "No"):
and json.loads(old_content).get("disabled", "No") == "No"
):
with open(fpath, "w") as chartfile: with open(fpath, "w") as chartfile:
chartfile.write(json.dumps(chart, indent=4, sort_keys=True)) chartfile.write(json.dumps(chart, indent=4, sort_keys=True))
all_roots.setdefault(filename, chart["tree"].keys()) all_roots.setdefault(filename, chart["tree"].keys())
def create_all_roots_file(): 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()): for filename, roots in sorted(all_roots.items()):
f.write(filename) f.write(filename)
f.write("\n----------------------\n") f.write('\n----------------------\n')
for r in sorted(roots): for r in sorted(roots):
f.write(r.encode("utf-8")) f.write(r.encode('utf-8'))
f.write("\n") f.write('\n')
f.write("\n\n\n") f.write('\n\n\n')
if __name__=="__main__":
if __name__ == "__main__":
go() go()

View File

@@ -2,438 +2,397 @@
"country_code": "at", "country_code": "at",
"name": "Austria - Chart of Accounts", "name": "Austria - Chart of Accounts",
"tree": { "tree": {
"Klasse 0 Aktiva: Anlageverm\u00f6gen": { "Summe Abschreibungen und Aufwendungen": {
"0100 Konzessionen ": {"account_type": "Fixed Asset"}, "7010 bis 7080 Abschreibungen auf das Anlageverm\u00f6gen (ausgenommen Finanzanlagen)": {},
"0110 Patentrechte und Lizenzen ": {"account_type": "Fixed Asset"}, "7100 bis 7190 Sonstige Steuern": {
"0120 Datenverarbeitungsprogramme ": {"account_type": "Fixed Asset"}, "account_type": "Tax"
"0130 Marken, Warenzeichen und Musterschutzrechte, sonstige Urheberrechte ": {"account_type": "Fixed Asset"},
"0140 Pacht- und Mietrechte ": {"account_type": "Fixed Asset"},
"0150 Bezugs- und ähnliche Rechte ": {"account_type": "Fixed Asset"},
"0160 Geschäfts-/Firmenwert ": {"account_type": "Fixed Asset"},
"0170 Umgründungsmehrwert ": {"account_type": "Fixed Asset"},
"0180 Geleistete Anzahlungen auf immaterielle Vermögensgegenstände": {"account_type": "Fixed Asset"},
"0190 Kumulierte Abschreibungen zu immateriellen Vermögensgegenständen ": {"account_type": "Fixed Asset"},
"0200 Unbebaute Grundstücke, soweit nicht landwirtschaftlich genutzt ": {"account_type": "Fixed Asset"},
"0210 Bebaute Grundstücke (Grundwert) ": {"account_type": "Fixed Asset"},
"0220 Landwirtschaftlich genutzte Grundstücke ": {"account_type": "Fixed Asset"},
"0230 Grundstücksgleiche Rechte ": {"account_type": "Fixed Asset"},
"0300 Betriebs- und Geschäftsgebäude auf eigenem Grund ": {"account_type": "Fixed Asset"},
"0310 Wohn- und Sozialgebäude auf eigenem Grund ": {"account_type": "Fixed Asset"},
"0320 Betriebs- und Geschäftsgebäude auf fremdem Grund ": {"account_type": "Fixed Asset"},
"0330 Wohn- und Sozialgebäude auf fremdem Grund ": {"account_type": "Fixed Asset"},
"0340 Grundstückseinrichtungen auf eigenem Grund ": {"account_type": "Fixed Asset"},
"0350 Grundstückseinrichtungen auf fremdem Grund ": {"account_type": "Fixed Asset"},
"0360 Bauliche Investitionen in fremden (gepachteten) Betriebs- und Geschäftsgebäuden": {"account_type": "Fixed Asset"},
"0370 Bauliche Investitionen in fremden (gepachteten) Wohn- und Sozialgebäuden": {"account_type": "Fixed Asset"},
"0390 Kumulierte Abschreibungen zu Grundstücken ": {"account_type": "Fixed Asset"},
"0400 Maschinen und Geräte ": {"account_type": "Fixed Asset"},
"0500 Maschinenwerkzeuge ": {"account_type": "Fixed Asset"},
"0510 Allgemeine Werkzeuge und Handwerkzeuge ": {"account_type": "Fixed Asset"},
"0520 Prototypen, Formen, Modelle ": {"account_type": "Fixed Asset"},
"0530 Andere Erzeugungshilfsmittel (auch Softwarewerkzeuge)": {"account_type": "Fixed Asset"},
"0540 Hebezeuge und Montageanlagen ": {"account_type": "Fixed Asset"},
"0550 Geringwertige Vermögensgegenstände, soweit im Erzeugungsprozess ": {"account_type": "Fixed Asset"},
"0560 Festwerte technische Anlagen und Maschinen ": {"account_type": "Fixed Asset"},
"0590 Kumulierte Abschreibungen zu technischen Anlagen und Maschinen ": {"account_type": "Fixed Asset"},
"0600 Betriebs- und Geschäftsausstattung, soweit nicht gesondert angeführt ": {"account_type": "Fixed Asset"},
"0610 Andere Anlagen, soweit nicht gesondert angeführt ": {"account_type": "Fixed Asset"},
"0620 Büromaschinen, EDV-Anlagen ": {"account_type": "Fixed Asset"},
"0630 PKW und Kombis ": {"account_type": "Fixed Asset"},
"0640 LKW ": {"account_type": "Fixed Asset"},
"0650 Andere Beförderungsmittel ": {"account_type": "Fixed Asset"},
"0660 Gebinde ": {"account_type": "Fixed Asset"},
"0670 Geringwertige Vermögensgegenstände, soweit nicht im Erzeugungssprozess verwendet": {"account_type": "Fixed Asset"},
"0680 Festwerte außer technische Anlagen und Maschinen ": {"account_type": "Fixed Asset"},
"0690 Kumulierte Abschreibungen zu anderen Anlagen, Betriebs- und Geschäftsausstattung": {"account_type": "Fixed Asset"},
"0700 Geleistete Anzahlungen auf Sachanlagen ": {"account_type": "Fixed Asset"},
"0710 Anlagen in Bau ": {"account_type": "Fixed Asset"},
"0790 Kumulierte Abschreibungen zu geleisteten Anzahlungen auf Sachanlagen ": {"account_type": "Fixed Asset"},
"0800 Anteile an verbundenen Unternehmen ": {"account_type": "Fixed Asset"},
"0810 Beteiligungen an Gemeinschaftsunternehmen ": {"account_type": "Fixed Asset"},
"0820 Beteiligungen an angeschlossenen (assoziierten) Unternehmen ": {"account_type": "Fixed Asset"},
"0830 Eigene Anteile, Anteile an herrschenden oder mit Mehrheit beteiligten ": {"account_type": "Fixed Asset"},
"0840 Sonstige Beteiligungen ": {"account_type": "Fixed Asset"},
"0850 Ausleihungen an verbundene Unternehmen ": {"account_type": "Fixed Asset"},
"0860 Ausleihungen an Unternehmen mit Beteiligungsverhältnis": {"account_type": "Fixed Asset"},
"0870 Ausleihungen an Gesellschafter ": {"account_type": "Fixed Asset"},
"0880 Sonstige Ausleihungen ": {"account_type": "Fixed Asset"},
"0890 Anteile an Kapitalgesellschaften ohne Beteiligungscharakter ": {"account_type": "Fixed Asset"},
"0900 Anteile an Personengesellschaften ohne Beteiligungscharakter ": {"account_type": "Fixed Asset"},
"0910 Genossenschaftsanteile ohne Beteiligungscharakter ": {"account_type": "Fixed Asset"},
"0920 Anteile an Investmentfonds ": {"account_type": "Fixed Asset"},
"0930 Festverzinsliche Wertpapiere des Anlagevermögens ": {"account_type": "Fixed Asset"},
"0980 Geleistete Anzahlungen auf Finanzanlagen ": {"account_type": "Fixed Asset"},
"0990 Kumulierte Abschreibungen zu Finanzanlagen ": {"account_type": "Fixed Asset"},
"root_type": "Asset"
}, },
"Klasse 1 Aktiva: Vorr\u00e4te": { "7200 bis 7290 Instandhaltung u. Reinigung durh Dritte, Entsorgung, Beleuchtung": {},
"1000 Bezugsverrechnung": {"account_type": "Stock"}, "7300 bis 7310 Transporte durch Dritte": {},
"1100 Rohstoffe": {"account_type": "Stock"}, "7320 bis 7330 Kfz - Aufwand": {},
"1200 Bezogene Teile": {"account_type": "Stock"}, "7340 bis 7350 Reise- und Fahraufwand": {},
"1300 Hilfsstoffe": {"account_type": "Stock"}, "7360 bis 7370 Tag- und N\u00e4chtigungsgelder": {},
"1350 Betriebsstoffe": {"account_type": "Stock"}, "7380 bis 7390 Nachrichtenaufwand": {},
"1360 Vorrat Energietraeger": {"account_type": "Stock"}, "7400 bis 7430 Miet- und Pachtaufwand": {},
"1400 Unfertige Erzeugnisse": {"account_type": "Stock"}, "7440 bis 7470 Leasingaufwand": {},
"1500 Fertige Erzeugnisse": {"account_type": "Stock"}, "7480 bis 7490 Lizenzaufwand": {},
"1600 Handelswarenvorrat": {"account_type": "Stock Received But Not Billed"}, "7500 bis 7530 Aufwand f\u00fcr beigestelltes Personal": {},
"1700 Noch nicht abrechenbare Leistungen": {"account_type": "Stock"}, "7540 bis 7570 Provisionen an Dritte": {},
"1900 Wertberichtigungen": {"account_type": "Stock"}, "7580 bis 7590 Aufsichtsratsverg\u00fctungen": {},
"1800 Geleistete Anzahlungen": {"account_type": "Stock"}, "7610 bis 7620 Druckerzeugnisse und Vervielf\u00e4ltigungen": {},
"1900 Wertberichtigungen": {"account_type": "Stock"}, "7650 bis 7680 Werbung und Repr\u00e4sentationen": {},
"root_type": "Asset" "7700 bis 7740 Versicherungen": {},
"7750 bis 7760 Beratungs- und Pr\u00fcfungsaufwand": {},
"7800 bis 7810 Schadensf\u00e4lle": {},
"7840 bis 7880 Verschiedene betriebliche Aufwendungen": {},
"7910 bis 7950 Aufwandsstellenrechung der Hersteller": {},
"Abschreibungen auf aktivierte Aufwendungen f\u00fcr das Ingangs. u. Erweitern des Betriebes": {},
"Abschreibungen vom Umlaufverm\u00f6gen, soweit diese die im Unternehmen \u00fcblichen Abschreibungen \u00fcbersteigen": {},
"Aufwandsstellenrechnung": {},
"Aus- und Fortbildung": {},
"Buchwert abgegangener Anlagen, ausgenommen Finanzanlagen": {},
"B\u00fcromaterial und Drucksorten": {},
"Fachliteratur und Zeitungen ": {},
"Herstellungskosten der zur Erzielung der Umsatzerl\u00f6se erbrachten Leistungen": {},
"Mitgliedsbeitr\u00e4ge": {},
"Skontoertr\u00e4ge auf sonstige betriebliche Aufwendungen": {},
"Sonstige betrieblichen Aufwendungen": {},
"Spenden und Trinkgelder": {},
"Spesen des Geldverkehrs": {},
"Verluste aus dem Abgang vom Anlageverm\u00f6gen, ausgenommen Finanzanlagen": {},
"Vertriebskosten": {},
"Verwaltungskosten": {},
"root_type": "Expense"
}, },
"Klasse 3 Passiva: Verbindlichkeiten": { "Summe Betriebliche Ertr\u00e4ge": {
"3000 Allgemeine Verbindlichkeiten (Schuld)": {"account_type": "Payable"}, "4400 bis 4490 Erl\u00f6sschm\u00e4lerungen": {},
"3010 R\u00fcckstellungen f\u00fcr Pensionen": {"account_type": "Payable"}, "4500 bis 4570 Ver\u00e4nderungen des Bestandes an fertigen und unfertigen Erzeugn. sowie an noch nicht abrechenbaren Leistungen": {},
"3020 Steuerr\u00fcckstellungen": {"account_type": "Tax"}, "4580 bis 4590 andere aktivierte Eigenleistungen": {},
"3041 Sonstige R\u00fcckstellungen": {"account_type": "Payable"}, "4600 bis 4620 Erl\u00f6se aus dem Abgang vom Anlageverm\u00f6gen, ausgen. Finanzanlagen": {},
"3110 Verbindlichkeiten gegen\u00fcber Bank": {"account_type": "Payable"}, "4630 bis 4650 Ertr\u00e4ge aus dem Abgang vom Anlageverm\u00f6gen, ausgen. Finanzanlagen": {},
"3150 Verbindlichkeiten Darlehen": {"account_type": "Payable"}, "4660 bis 4670 Ertr\u00e4ge aus der Zuschreibung zum Anlageverm\u00f6gen, ausgen. Finanzanlagen": {},
"3185 Verbindlichkeiten Kreditkarte": {"account_type": "Payable"}, "4700 bis 4790 Ertr\u00e4ge aus der Aufl\u00f6sung von R\u00fcckstellungen": {},
"3380 Verbindlichkeiten aus der Annahme gezogener Wechsel u. d. Ausstellungen eigener Wechsel": { "4800 bis 4990 \u00dcbrige betriebliche Ertr\u00e4ge": {},
"Erl\u00f6se 0 % Ausfuhrlieferungen/Drittl\u00e4nder": {},
"Erl\u00f6se 10 %": {},
"Erl\u00f6se 20 %": {},
"Erl\u00f6se aus im Inland stpfl. EG Lieferungen 10 % USt": {},
"Erl\u00f6se aus im Inland stpfl. EG Lieferungen 20 % USt": {},
"Erl\u00f6se i.g. Lieferungen (stfr)": {},
"root_type": "Income"
},
"Summe Eigenkapital R\u00fccklagen Abschlusskonten": {
"9000 bis 9180 Gezeichnetes bzw. gewidmetes Kapital": {
"account_type": "Equity"
},
"9200 bis 9290 Kapitalr\u00fccklagen": {
"account_type": "Equity"
},
"9300 bis 9380 Gewinnr\u00fccklagen": {
"account_type": "Equity"
},
"9400 bis 9590 Bewertungsreserven uns sonst. unversteuerte R\u00fccklagen": {
"account_type": "Equity"
},
"9600 bis 9690 Privat und Verrechnungskonten bei Einzelunternehmen und Personengesellschaften": {},
"9700 bis 9790 Einlagen stiller Gesellschafter ": {},
"9900 bis 9999 Evidenzkonten": {},
"Bilanzgewinn (-verlust )": {
"account_type": "Equity"
},
"Er\u00f6ffnungsbilanz": {},
"Gewinn- und Verlustrechnung": {},
"Schlussbilanz": {},
"nicht eingeforderte ausstehende Einlagen": {
"account_type": "Equity"
},
"root_type": "Equity"
},
"Summe Finanzertr\u00e4ge und Aufwendungen": {
"8000 bis 8040 Ertr\u00e4ge aus Beteiligungen": {},
"8050 bis 8090 Ertr\u00e4ge aus anderen Wertpapieren und Ausleihungen des Finanzanlageverm\u00f6gens": {},
"8100 bis 8130 Sonstige Zinsen und \u00e4hnliche Ertr\u00e4ge": {},
"8220 bis 8250 Aufwendungen aus Beteiligungen": {},
"8260 bis 8270 Aufwendungen aus sonst. Fiananzanlagen und aus Wertpapieren des Umlaufverm\u00f6gens": {},
"8280 bis 8340 Zinsen und \u00e4hnliche Aufwendungem": {},
"8400 bis 8440 Au\u00dferordentliche Ertr\u00e4ge": {},
"8450 bis 8490 Au\u00dferordentliche Aufwendungen": {},
"8500 bis 8590 Steuern vom Einkommen und vom Ertrag": {
"account_type": "Tax"
},
"8600 bis 8690 Aufl\u00f6sung unversteuerten R\u00fccklagen": {},
"8700 bis 8740 Aufl\u00f6sung von Kapitalr\u00fccklagen": {},
"8750 bis 8790 Aufl\u00f6sung von Gewinnr\u00fccklagen": {},
"8800 bis 8890 Zuweisung von unversteuerten R\u00fccklagen": {},
"Buchwert abgegangener Beteiligungen": {},
"Buchwert abgegangener Wertpapiere des Umlaufverm\u00f6gens": {},
"Buchwert abgegangener sonstiger Finanzanlagen": {},
"Erl\u00f6se aus dem Abgang von Beteiligungen": {},
"Erl\u00f6se aus dem Abgang von Wertpapieren des Umlaufverm\u00f6gens": {},
"Erl\u00f6se aus dem Abgang von sonstigen Finanzanlagen": {},
"Ertr\u00e4ge aus dem Abgang von und der Zuschreibung zu Finanzanlagen": {},
"Ertr\u00e4ge aus dem Abgang von und der Zuschreibung zu Wertpapieren des Umlaufverm\u00f6gens": {},
"Gewinabfuhr bzw. Verlust\u00fcberrechnung aus Ergebnisabf\u00fchrungsvertr\u00e4gen": {},
"nicht ausgenutzte Lieferantenskonti": {},
"root_type": "Income"
},
"Summe Fremdkapital": {
"3020 bis 3030 Steuerr\u00fcckstellungen": {},
"3040 bis 3090 Sonstige R\u00fcckstellungen": {},
"3110 bis 3170 Verbindlichkeiten gegen\u00fcber Kredidinstituten": {},
"3180 bis 3190 Verbindlichkeiten gegen\u00fcber Finanzinstituten": {},
"3380 bis 3390 Verbindlichkeiten aus der Annahme gezogener Wechsel u. d. Ausstellungen eigener Wechsel": {
"account_type": "Payable" "account_type": "Payable"
}, },
"3400 Verbindlichkeiten gegen\u00fc. verb. Untern., Verbindl. gegen\u00fc. Untern., mit denen eine Beteiligungsverh\u00e4lnis besteht": {}, "3400 bis 3470 Verbindlichkeiten gegen\u00fc. verb. Untern., Verbindl. gegen\u00fc. Untern., mit denen eine Beteiligungsverh\u00e4lnis besteht": {},
"3460 Verbindlichkeiten gegenueber Gesellschaftern": {"account_type": "Payable"}, "3600 bis 3690 Verbindlichkeiten im Rahmen der sozialen Sicherheit": {},
"3470 Einlagen stiller Gesellschafter": {"account_type": "Payable"}, "3700 bis 3890 \u00dcbrige sonstige Verbindlichkeiten": {},
"3585 Verbindlichkeiten Lohnsteuer": {"account_type": "Tax"}, "3900 bis 3990 Passive Rechnungsabgrenzungsposten": {},
"3590 Verbindlichkeiten Kommunalabgaben": {"account_type": "Tax"}, "Anleihen (einschlie\u00dflich konvertibler)": {},
"3595 Verbindlichkeiten Dienstgeberbeitrag": {"account_type": "Tax"}, "Erhaltene Anzahlungenauf Bestellungen": {},
"3600 Verbindlichkeiten Sozialversicherung": {"account_type": "Payable"}, "R\u00fcckstellungen f\u00fcr Abfertigung": {},
"3640 Verbindlichkeiten Loehne und Gehaelter": {"account_type": "Payable"}, "R\u00fcckstellungen f\u00fcr Pensionen": {},
"3700 Sonstige Verbindlichkeiten": {"account_type": "Payable"}, "USt. \u00a719 /art (reverse charge)": {
"3900 Passive Rechnungsabgrenzungsposten": {"account_type": "Payable"},
"3100 Anleihen (einschlie\u00dflich konvertibler)": {"account_type": "Payable"},
"3200 Erhaltene Anzahlungen auf Bestellungen": {"account_type": "Payable"},
"3040 R\u00fcckstellungen f\u00fcr Abfertigung": {"account_type": "Payable"},
"3530 USt. \u00a719 (reverse charge)": {
"account_type": "Tax" "account_type": "Tax"
}, },
"3500 Verbindlichkeiten aus Umsatzsteuer": {"account_type": "Tax"}, "Umsatzsteuer": {},
"3580 Umsatzsteuer Zahllast": { "Umsatzsteuer Zahllast": {
"account_type": "Tax" "account_type": "Tax"
}, },
"3510 Umsatzsteuer Inland 20%": { "Umsatzsteuer aus i.g. Erwerb 10%": {
"account_type": "Tax" "account_type": "Tax"
}, },
"3515 Umsatzsteuer Inland 10%": { "Umsatzsteuer aus i.g. Erwerb 20%": {
"account_type": "Tax" "account_type": "Tax"
}, },
"3520 Umsatzsteuer aus i.g. Erwerb 20%": { "Umsatzsteuer aus i.g. Lieferungen 10%": {
"account_type": "Tax" "account_type": "Tax"
}, },
"3525 Umsatzsteuer aus i.g. Erwerb 10%": { "Umsatzsteuer aus i.g. Lieferungen 20%": {
"account_type": "Tax" "account_type": "Tax"
}, },
"3560 Umsatzsteuer-Evidenzkonto f\u00fcr erhaltene Anzahlungen auf Bestellungen": {}, "Umsatzsteuer-Evidenzkonto f\u00fcr erhaltene Anzahlungen auf Bestellungen": {},
"3360 Verbindlichkeiten aus Lieferungen u. Leistungen EU": { "Verbindlichkeiten aus Lieferungen u. Leistungen EU": {
"account_type": "Payable" "account_type": "Payable"
}, },
"3000 Verbindlichkeiten aus Lieferungen u. Leistungen Inland": { "Verbindlichkeiten aus Lieferungen u. Leistungen Inland": {
"account_type": "Payable" "account_type": "Payable"
}, },
"3370 Verbindlichkeiten aus Lieferungen u. Leistungen sonst. Ausland": { "Verbindlichkeiten aus Lieferungen u. Leistungen sonst. Ausland": {
"account_type": "Payable" "account_type": "Payable"
}, },
"3400 Verbindlichkeiten gegen\u00fcber verbundenen Unternehmen": {}, "Verbindlichkeiten gegen\u00fcber Gesellschaften": {},
"3570 Verrechnung Finanzamt": { "Verrechnung Finanzamt": {
"account_type": "Tax" "account_type": "Tax"
}, },
"root_type": "Liability" "root_type": "Liability"
}, },
"Klasse 2 Aktiva: Umlaufverm\u00f6gen, Rechnungsabgrenzungen": { "Summe Kontoklasse 0 Anlageverm\u00f6gen": {
"2030 Forderungen aus Lieferungen und Leistungen Inland (0% USt, umsatzsteuerfrei)": { "44 bis 49 Sonstige Maschinen und maschinelle Anlagen": {},
"920 bis 930 Festverzinsliche Wertpapiere des Anlageverm\u00f6gens": {},
"940 bis 970 Sonstige Finanzanlagen, Wertrechte": {},
"Allgemeine Werkzeuge und Handwerkzeuge": {},
"Andere Bef\u00f6rderungsmittel": {},
"Andere Betriebs- und Gesch\u00e4ftsausstattung": {},
"Andere Erzeugungshilfsmittel": {},
"Anlagen im Bau": {},
"Anteile an Investmentfonds": {},
"Anteile an Kapitalgesellschaften ohne Beteiligungscharakter": {},
"Anteile an Personengesellschaften ohne Beteiligungscharakter": {},
"Anteile an verbundenen Unternehmen": {},
"Antriebsmaschinen": {},
"Aufwendungen f\u00fcs das Ingangssetzen u. Erweitern eines Betriebes": {},
"Ausleihungen an verbundene Unternehmen": {},
"Ausleihungen an verbundene Unternehmen, mit denen ein Beteiligungsverh\u00e4lnis besteht": {},
"Bauliche Investitionen in fremden (gepachteten) Betriebs- und Gesch\u00e4ftsgeb\u00e4uden": {},
"Bauliche Investitionen in fremden (gepachteten) Wohn- und Sozialgeb\u00e4uden": {},
"Bebaute Grundst\u00fccke (Grundwert)": {},
"Beheizungs- und Beleuchtungsanlagen": {},
"Beteiligungen an Gemeinschaftunternehmen": {},
"Beteiligungen an angeschlossenen (assoziierten) Unternehmen": {},
"Betriebs- und Gesch\u00e4ftsgeb\u00e4ude auf eigenem Grund": {},
"Betriebs- und Gesch\u00e4ftsgeb\u00e4ude auf fremdem Grund": {},
"B\u00fcromaschinen, EDV - Anlagen": {},
"Datenverarbeitungsprogramme": {},
"Energieversorgungsanlagen": {},
"Fertigungsmaschinen": {},
"Gebinde": {},
"Geleistete Anzahlungen": {},
"Genossenschaften ohne Beteiligungscharakter": {},
"Geringwertige Verm\u00f6gensgegenst\u00e4nde, soweit im Erzeugerprozess verwendet": {},
"Geringwertige Verm\u00f6gensgegenst\u00e4nde, soweit nicht im Erzeugungsprozess verwendet": {},
"Gesch\u00e4fts(Firmen)wert": {},
"Grundst\u00fcckseinrichtunten auf eigenem Grund": {},
"Grundst\u00fcckseinrichtunten auf fremdem Grund": {},
"Grundst\u00fccksgleiche Rechte": {},
"Hebezeuge und Montageanlagen": {},
"Konzessionen": {},
"Kumulierte Abschreibungen": {},
"LKW": {},
"Marken, Warenzeichen und Musterschutzrechte": {},
"Maschinenwerkzeuge": {},
"Nachrichten- und Kontrollanlagen": {},
"PKW": {},
"Pacht- und Mietrechte": {},
"Patentrechte und Lizenzen": {},
"Sonstige Ausleihungen": {},
"Sonstige Beteiligungen": {},
"Transportanlagen": {},
"Unbebaute Grundst\u00fccke": {},
"Vorrichtungen, Formen und Modelle": {},
"Wohn- und Sozialgeb\u00e4ude auf eigenem Grund": {},
"Wohn- und Sozialgeb\u00e4ude auf fremdem Grund": {},
"root_type": "Asset"
},
"Summe Personalaufwand": {
"6000 bis 6190 L\u00f6hne": {},
"6200 bis 6390 Geh\u00e4lter": {},
"6400 bis 6440 Aufwendungen f\u00fcr Abfertigungen": {},
"6450 bis 6490 Aufwendungen f\u00fcr Altersversorgung": {},
"6500 bis 6550 Gesetzlicher Sozialaufwand Arbeiter": {},
"6560 bis 6590 Gesetzlicher Sozialaufwand Angestellte": {},
"6600 bis 6650 Lohnabh\u00e4ngige Abgaben und Pflichtbeitr\u00e4gte": {},
"6660 bis 6690 Gehaltsabh\u00e4ngige Abgaben und Pflichtbeitr\u00e4gte": {},
"6700 bis 6890 Sonstige Sozialaufwendungen": {},
"Aufwandsstellenrechnung": {},
"root_type": "Expense"
},
"Summe Umlaufverm\u00f6gen": {
"2000 bis 2007 Forderungen aus Lief. und Leist. Inland": {
"account_type": "Receivable" "account_type": "Receivable"
}, },
"2010 Forderungen aus Lieferungen und Leistungen Inland (10% USt, umsatzsteuerfrei)": { "2100 bis 2120 Forderungen aus Lief. und Leist. EU": {
"account_type": "Receivable" "account_type": "Receivable"
}, },
"2000 Forderungen aus Lieferungen und Leistungen Inland (20% USt, umsatzsteuerfrei)": { "2150 bis 2170 Forderungen aus Lief. und Leist. Ausland": {
"account_type": "Receivable" "account_type": "Receivable"
}, },
"2040 Forderungen aus Lieferungen und Leistungen Inland (sonstiger USt-Satz)": { "2200 bis 2220 Forderungen gegen\u00fcber verbundenen Unternehmen": {
"account_type": "Receivable" "account_type": "Receivable"
}, },
"2100 Forderungen aus Lieferungen und Leistungen EU": { "2250 bis 2270 Forderungen gegen\u00fcber Unternehmen, mit denen ein Beteiligungsverh\u00e4ltnis besteht": {
"account_type": "Receivable" "account_type": "Receivable"
}, },
"2150 Forderungen aus Lieferungen und Leistungen Ausland (Nicht-EU)": { "2300 bis 2460 Sonstige Forderungen und Verm\u00f6gensgegenst\u00e4nde": {
"account_type": "Receivable" "account_type": "Receivable"
}, },
"2200 Forderungen gegen\u00fcber verbundenen Unternehmen": { "2630 bis 2670 Sonstige Wertpapiere": {
"account_type": "Receivable" "account_type": "Receivable"
}, },
"2250 Forderungen gegen\u00fcber Unternehmen, mit denen ein Beteiligungsverh\u00e4ltnis besteht": { "2750 bis 2770 Kassenbest\u00e4nde in Fremdw\u00e4hrung": {
"account_type": "Receivable" "account_type": "Receivable"
}, },
"2300 Sonstige Forderungen und Verm\u00f6gensgegenst\u00e4nde": { "Aktive Rechnungsabrenzungsposten": {
"account_type": "Receivable" "account_type": "Receivable"
}, },
"2630 Sonstige Wertpapiere": { "Anteile an verbundenen Unternehmen": {
"account_type": "Stock"
},
"2750 Kassenbest\u00e4nde in Fremdw\u00e4hrung": {
"account_type": "Cash"
},
"2900 Aktive Rechnungsabrenzungsposten": {
"account_type": "Receivable" "account_type": "Receivable"
}, },
"2600 Anteile an verbundenen Unternehmen": { "Bank / Guthaben bei Kreditinstituten": {
"account_type": "Equity"
},
"2680 Besitzwechsel ohne Forderungen": {
"account_type": "Receivable" "account_type": "Receivable"
}, },
"2950 Aktiviertes Disagio": { "Besitzwechsel ...": {
"account_type": "Receivable" "account_type": "Receivable"
}, },
"2610 Eigene Anteile und Wertpapiere an mit Mehrheit beteiligten Unternehmen": { "Disagio": {
"account_type": "Receivable" "account_type": "Receivable"
}, },
"2570 Einfuhrumsatzsteuer (bezahlt)": {"account_type": "Tax"}, "Eigene Anteile (Wertpapiere)": {
"2460 Eingeforderte aber noch nicht eingezahlte Einlagen": {
"account_type": "Receivable" "account_type": "Receivable"
}, },
"2180 Einzelwertberichtigungen zu Forderungen aus Lief. und Leist. Ausland": { "Einfuhrumsatzsteuer (bezahlt)": {},
"Eingeforderte aber noch nicht eingezahlte Einlagen": {
"account_type": "Receivable" "account_type": "Receivable"
}, },
"2130 Einzelwertberichtigungen zu Forderungen aus Lief. und Leist. EU": { "Einzelwertberichtigungen zu Forderungen aus Lief. und Leist. Ausland": {
"account_type": "Receivable" "account_type": "Receivable"
}, },
"2080 Einzelwertberichtigungen zu Forderungen aus Lief. und Leist. Inland ": { "Einzelwertberichtigungen zu Forderungen aus Lief. und Leist. EU": {
"account_type": "Receivable" "account_type": "Receivable"
}, },
"2270 Einzelwertberichtigungen zu Forderungen gegen\u00fcber Unternehmen mit denen ein Beteiligungsverh\u00e4ltnis besteht": { "Einzelwertberichtigungen zu Forderungen aus Lief. und Leist. Inland ": {
"account_type": "Receivable" "account_type": "Receivable"
}, },
"2230 Einzelwertberichtigungen zu Forderungen gegen\u00fcber verbundenen Unternehmen": { "Einzelwertberichtigungen zu Forderungen gegen\u00fcber Unternehmen mit denen ein Beteiligungsverh\u00e4ltnis besteht": {
"account_type": "Receivable" "account_type": "Receivable"
}, },
"2470 Einzelwertberichtigungen zu sonstigen Forderungen und Verm\u00f6gensgegenst\u00e4nden": { "Einzelwertberichtigungen zu Forderungen gegen\u00fcber verbundenen Unternehmen": {
"account_type": "Receivable" "account_type": "Receivable"
}, },
"2700 Kassenbestand": { "Einzelwertberichtigungen zu sonstigen Forderungen und Verm\u00f6gensgegenst\u00e4nden": {
"account_type": "Cash"
},
"2190 Pauschalwertberichtigungen zu Forderungen aus Lief. und Leist. sonstiges Ausland": {
"account_type": "Receivable" "account_type": "Receivable"
}, },
"2130 Pauschalwertberichtigungen zu Forderungen aus Lief. und Leist. EU": { "Kassenbestand": {
"account_type": "Receivable" "account_type": "Receivable"
}, },
"2100 Pauschalwertberichtigungen zu Forderungen aus Lief. und Leist. Inland ": { "Pauschalwertberichtigungen zu Forderungen aus Lief. und Leist. Ausland": {
"account_type": "Receivable" "account_type": "Receivable"
}, },
"2280 Pauschalwertberichtigungen zu Forderungen gegen\u00fcber Unternehmen mit denen ein Beteiligungsverh\u00e4ltnis besteht": { "Pauschalwertberichtigungen zu Forderungen aus Lief. und Leist. EU": {
"account_type": "Receivable" "account_type": "Receivable"
}, },
"2240 Pauschalwertberichtigungen zu Forderungen gegen\u00fcber verbundenen Unternehmen": { "Pauschalwertberichtigungen zu Forderungen aus Lief. und Leist. Inland ": {
"account_type": "Receivable" "account_type": "Receivable"
}, },
"2480 Pauschalwertberichtigungen zu sonstigen Forderungen und Verm\u00f6gensgegenst\u00e4nden": { "Pauschalwertberichtigungen zu Forderungen gegen\u00fcber Unternehmen mit denen ein Beteiligungsverh\u00e4ltnis besteht": {
"account_type": "Receivable" "account_type": "Receivable"
}, },
"2740 Postwertzeichen": { "Pauschalwertberichtigungen zu Forderungen gegen\u00fcber verbundenen Unternehmen": {
"account_type": "Cash"
},
"2780 Schecks in Euro": {
"account_type": "Cash"
},
"2800 Guthaben bei Bank": {
"account_type": "Bank"
},
"2801 Guthaben bei Bank - Sparkonto": {
"account_type": "Bank"
},
"2810 Guthaben bei Paypal": {
"account_type": "Bank"
},
"2930 Mietvorauszahlungen": {
"account_type": "Receivable" "account_type": "Receivable"
}, },
"2980 Abgrenzung latenter Steuern": { "Pauschalwertberichtigungen zu sonstigen Forderungen und Verm\u00f6gensgegenst\u00e4nden": {
"account_type": "Receivable" "account_type": "Receivable"
}, },
"2500 Vorsteuer": { "Postwertzeichen": {
"account_type": "Receivable" "account_type": "Receivable"
}, },
"2510 Vorsteuer Inland 10%": { "Schecks in Inlandsw\u00e4hrung": {
"account_type": "Receivable"
},
"Sonstige Anteile": {
"account_type": "Receivable"
},
"Stempelmarken": {
"account_type": "Receivable"
},
"Steuerabgrenzung": {
"account_type": "Receivable"
},
"Unterschiedsbetrag gem. Abschnitt XII Pensionskassengesetz": {
"account_type": "Receivable"
},
"Unterschiedsbetrag zur gebotenen Pensionsr\u00fcckstellung": {
"account_type": "Receivable"
},
"Vorsteuer": {
"account_type": "Receivable"
},
"Vorsteuer aus ig. Erwerb 10%": {
"account_type": "Tax" "account_type": "Tax"
}, },
"2895 Schwebende Geldbewegugen": { "Vorsteuer aus ig. Erwerb 20%": {
"account_type": "Bank"
},
"2513 Vorsteuer Inland 5%": {
"account_type": "Tax" "account_type": "Tax"
}, },
"2515 Vorsteuer Inland 20%": { "Vorsteuer \u00a719/Art 19 ( reverse charge ) ": {
"account_type": "Tax" "account_type": "Tax"
}, },
"2520 Vorsteuer aus innergemeinschaftlichem Erwerb 10%": { "Wertberichtigungen": {
"account_type": "Tax"
},
"2525 Vorsteuer aus innergemeinschaftlichem Erwerb 20%": {
"account_type": "Tax"
},
"2530 Vorsteuer \u00a719/Art 19 ( reverse charge ) ": {
"account_type": "Tax"
},
"2690 Wertberichtigungen zu Wertpapieren und Anteilen": {
"account_type": "Receivable" "account_type": "Receivable"
}, },
"root_type": "Asset" "root_type": "Asset"
}, },
"Klasse 4: Betriebliche Erträge": { "Summe Vorr\u00e4te": {
"4000 Erlöse 20 %": {"account_type": "Income Account"}, "1000 bis 1090 Bezugsverrechnung": {},
"4020 Erl\u00f6se 0 % steuerbefreit": {"account_type": "Income Account"}, "1100 bis 1190 Rohstoffe": {},
"4010 Erl\u00f6se 10 %": {"account_type": "Income Account"}, "1200 bis 1290 Bezogene Teile": {},
"4030 Erl\u00f6se 13 %": {"account_type": "Income Account"}, "1300 bis 1340 Hilfsstoffe": {},
"4040 Erl\u00f6se 0 % innergemeinschaftliche Lieferungen": {"account_type": "Income Account"}, "1350 bis 1390 Betriebsstoffe": {},
"4400 Erl\u00f6sreduktion 0 % steuerbefreit": {"account_type": "Expense Account"}, "1400 bis 1490 Unfertige Erzeugniss": {},
"4410 Erl\u00f6sreduktion 10 %": {"account_type": "Expense Account"}, "1500 bis 1590 Fertige Erzeugniss": {},
"4420 Erl\u00f6sreduktion 20 %": {"account_type": "Expense Account"}, "1600 bis 1690 Waren": {},
"4430 Erl\u00f6sreduktion 13 %": {"account_type": "Expense Account"}, "1700 bis 1790 Noch nicht abgerechenbare Leistungen": {},
"4440 Erl\u00f6sreduktion 0 % innergemeinschaftliche Lieferungen": {"account_type": "Expense Account"}, "1900 bis 1990 Wertberichtigungen": {},
"4500 Ver\u00e4nderungen des Bestandes an fertigen und unfertigen Erzeugn. sowie an noch nicht abrechenbaren Leistungen": {"account_type": "Income Account"}, "geleistete Anzahlungen": {},
"4580 Aktivierte Eigenleistungen": {"account_type": "Income Account"}, "root_type": "Asset"
"4600 Erl\u00f6se aus dem Abgang vom Anlageverm\u00f6gen, ausgen. Finanzanlagen": {"account_type": "Income Account"},
"4630 Ertr\u00e4ge aus dem Abgang vom Anlageverm\u00f6gen, ausgen. Finanzanlagen": {"account_type": "Income Account"},
"4660 Ertr\u00e4ge aus der Zuschreibung zum Anlageverm\u00f6gen, ausgen. Finanzanlagen": {"account_type": "Income Account"},
"4700 Ertr\u00e4ge aus der Aufl\u00f6sung von R\u00fcckstellungen": {"account_type": "Income Account"},
"4800 \u00dcbrige betriebliche Ertr\u00e4ge": {"account_type": "Income Account"},
"root_type": "Income"
}, },
"Klasse 5: Aufwand f\u00fcr Material und Leistungen": { "Summe Wareneinsatz": {
"5000 Einkauf Partnerleistungen": {"account_type": "Cost of Goods Sold"}, "5100 bis 5190 Verbrauch an Rohstoffen": {},
"5100 Verbrauch an Rohstoffen": {"account_type": "Cost of Goods Sold"}, "5200 bis 5290 Verbrauch von bezogenen Fertig- und Einzelteilen": {},
"5200 Verbrauch von bezogenen Fertig- und Einzelteilen": {"account_type": "Cost of Goods Sold"}, "5300 bis 5390 Verbrauch von Hilfsstoffen": {},
"5300 Verbrauch von Hilfsstoffen": {"account_type": "Cost of Goods Sold"}, "5400 bis 5490 Verbrauch von Betriebsstoffen": {},
"5340 Verbrauch Verpackungsmaterial": {"account_type": "Cost of Goods Sold"}, "5500 bis 5590 Verbrauch von Werkzeugen und anderen Erzeugungshilfsmittel": {},
"5470 Verbrauch von Kleinmaterial": {"account_type": "Cost of Goods Sold"}, "5600 bis 5690 Verbrauch von Brenn- und Treibstoffen, Energie und Wasser": {},
"5450 Verbrauch von Reinigungsmaterial": {"account_type": "Cost of Goods Sold"}, "5700 bis 5790 Sonstige bezogene Herstellungsleistungen": {},
"5400 Verbrauch von Betriebsstoffen": {"account_type": "Cost of Goods Sold"}, "Aufwandsstellenrechnung": {},
"5500 Verbrauch von Werkzeugen und anderen Erzeugungshilfsmittel": {"account_type": "Cost of Goods Sold"}, "Skontoertr\u00e4ge auf Materialaufwand": {},
"5600 Verbrauch von Brenn- und Treibstoffen, Energie und Wasser": {"account_type": "Cost of Goods Sold"}, "Skontoertr\u00e4ge auf sonstige bezogene Herstellungsleistungen": {},
"5700 Bearbeitung durch Dritte": {"account_type": "Cost of Goods Sold"}, "Wareneinkauf 10 %": {},
"5900 Aufwandsstellenrechnung Material": {"account_type": "Cost of Goods Sold"}, "Wareneinkauf 20 %": {},
"5820 Skontoertr\u00e4ge (20% USt.)": {"account_type": "Income Account"}, "Wareneinkauf igErwerb 10 % VSt/10 % USt": {},
"5810 Skontoertr\u00e4ge (10% USt.)": {"account_type": "Income Account"}, "Wareneinkauf igErwerb 20 % VSt/20 % USt": {},
"5010 Handelswareneinkauf 10 %": {"account_type": "Cost of Goods Sold"}, "Wareneinkauf igErwerb ohne Vorsteuerabzug und 10 % USt": {},
"5020 Handelswareneinkauf 20 %": {"account_type": "Cost of Goods Sold"}, "Wareneinkauf igErwerb ohne Vorsteuerabzug und 20 % USt": {},
"5040 Handelswareneinkauf innergemeinschaftlicher Erwerb 10 % VSt/10 % USt": {"account_type": "Cost of Goods Sold"},
"5050 Handelswareneinkauf innergemeinschaftlicher Erwerb 20 % VSt/20 % USt": {"account_type": "Cost of Goods Sold"},
"5070 Handelswareneinkauf innergemeinschaftlicher Erwerb ohne Vorsteuerabzug und 10 % USt": {"account_type": "Cost of Goods Sold"},
"5080 Handelswareneinkauf innergemeinschaftlicher Erwerb ohne Vorsteuerabzug und 20 % USt": {"account_type": "Cost of Goods Sold"},
"root_type": "Expense" "root_type": "Expense"
},
"Klasse 6: Personalaufwand": {
"6000 L\u00f6hne": {"account_type": "Payable"},
"6200 Geh\u00e4lter": {"account_type": "Payable"},
"6400 Aufwendungen f\u00fcr Abfertigungen": {"account_type": "Payable"},
"6450 Aufwendungen f\u00fcr Altersversorgung": {"account_type": "Payable"},
"6500 Gesetzlicher Sozialaufwand Arbeiter": {"account_type": "Payable"},
"6560 Gesetzlicher Sozialaufwand Angestellte": {"account_type": "Payable"},
"6600 Lohnabh\u00e4ngige Abgaben und Pflichtbeitr\u00e4gte": {"account_type": "Payable"},
"6660 Gehaltsabh\u00e4ngige Abgaben und Pflichtbeitr\u00e4gte": {"account_type": "Payable"},
"6700 Sonstige Sozialaufwendungen": {"account_type": "Payable"},
"6900 Aufwandsstellenrechnung Personal": {"account_type": "Payable"},
"root_type": "Expense"
},
"Klasse 7: Abschreibungen und sonstige betriebliche Aufwendungen": {
"7010 Abschreibungen auf das Anlageverm\u00f6gen (ausgenommen Finanzanlagen)": {"account_type": "Depreciation"},
"7100 Sonstige Steuern und Geb\u00fchren": {"account_type": "Tax"},
"7200 Instandhaltung u. Reinigung durch Dritte, Entsorgung, Energie": {"account_type": "Expense Account"},
"7300 Transporte durch Dritte": {"account_type": "Expense Account"},
"7310 Fahrrad - Aufwand": {"account_type": "Expense Account"},
"7320 Kfz - Aufwand": {"account_type": "Expense Account"},
"7330 LKW - Aufwand": {"account_type": "Expense Account"},
"7340 Lastenrad - Aufwand": {"account_type": "Expense Account"},
"7350 Reise- und Fahraufwand": {"account_type": "Expense Account"},
"7360 Tag- und N\u00e4chtigungsgelder": {"account_type": "Expense Account"},
"7380 Nachrichtenaufwand": {"account_type": "Expense Account"},
"7400 Miet- und Pachtaufwand": {"account_type": "Expense Account"},
"7440 Leasingaufwand": {"account_type": "Expense Account"},
"7480 Lizenzaufwand": {"account_type": "Expense Account"},
"7500 Aufwand f\u00fcr beigestelltes Personal": {"account_type": "Expense Account"},
"7540 Provisionen an Dritte": {"account_type": "Expense Account"},
"7580 Aufsichtsratsverg\u00fctungen": {"account_type": "Expense Account"},
"7610 Druckerzeugnisse und Vervielf\u00e4ltigungen": {"account_type": "Expense Account"},
"7650 Werbung und Repr\u00e4sentationen": {"account_type": "Expense Account"},
"7700 Versicherungen": {"account_type": "Expense Account"},
"7750 Beratungs- und Pr\u00fcfungsaufwand": {"account_type": "Expense Account"},
"7800 Forderungsverluste und Schadensf\u00e4lle": {"account_type": "Expense Account"},
"7840 Verschiedene betriebliche Aufwendungen": {"account_type": "Expense Account"},
"7910 Aufwandsstellenrechung der Hersteller": {"account_type": "Expense Account"},
"7060 Sofortabschreibungen geringwertig": {"account_type": "Expense Account"},
"7070 Abschreibungen vom Umlaufverm\u00f6gen, soweit diese die im Unternehmen \u00fcblichen Abschreibungen \u00fcbersteigen": {"account_type": "Depreciation"},
"7900 Aufwandsstellenrechnung": {"account_type": "Expense Account"},
"7770 Aus- und Fortbildung": {"account_type": "Expense Account"},
"7820 Buchwert abgegangener Anlagen, ausgenommen Finanzanlagen": {"account_type": "Expense Account"},
"7600 B\u00fcromaterial und Drucksorten": {"account_type": "Expense Account"},
"7630 Fachliteratur und Zeitungen ": {"account_type": "Expense Account"},
"7960 Herstellungskosten der zur Erzielung der Umsatzerl\u00f6se erbrachten Leistungen": {"account_type": "Expense Account"},
"7780 Mitgliedsbeitr\u00e4ge": {"account_type": "Expense Account"},
"7880 Skontoertr\u00e4ge auf sonstige betriebliche Aufwendungen": {"account_type": "Expense Account"},
"7990 Sonstige betrieblichen Aufwendungen": {"account_type": "Expense Account"},
"7680 Spenden und Trinkgelder": {"account_type": "Expense Account"},
"7790 Spesen des Geldverkehrs": {"account_type": "Expense Account"},
"7830 Verluste aus dem Abgang vom Anlageverm\u00f6gen, ausgenommen Finanzanlagen": {"account_type": "Expense Account"},
"7970 Vertriebskosten": {"account_type": "Expense Account"},
"7980 Verwaltungskosten": {"account_type": "Expense Account"},
"root_type": "Expense"
},
"Klasse 8: Finanz- und ausserordentliche Ertr\u00e4ge und Aufwendungen": {
"8000 Ertr\u00e4ge aus Beteiligungen": {"account_type": "Income Account"},
"8050 Ertr\u00e4ge aus anderen Wertpapieren und Ausleihungen des Finanzanlageverm\u00f6gens": {"account_type": "Income Account"},
"8100 Zinsen aus Bankguthaben": {"account_type": "Income Account"},
"8110 Zinsen aus gewaehrten Darlehen": {"account_type": "Income Account"},
"8130 Verzugszinsenertraege": {"account_type": "Income Account"},
"8220 Aufwendungen aus Beteiligungen": {"account_type": "Expense Account"},
"8260 Aufwendungen aus sonst. Fiananzanlagen und aus Wertpapieren des Umlaufverm\u00f6gens": {},
"8280 Zinsen und \u00e4hnliche Aufwendungem": {"account_type": "Expense Account"},
"8400 Au\u00dferordentliche Ertr\u00e4ge": {"account_type": "Income Account"},
"8450 Au\u00dferordentliche Aufwendungen": {"account_type": "Expense Account"},
"8500 Steuern vom Einkommen und vom Ertrag": {
"account_type": "Tax"
},
"8600 Aufl\u00f6sung unversteuerten R\u00fccklagen": {"account_type": "Income Account"},
"8700 Aufl\u00f6sung von Kapitalr\u00fccklagen": {"account_type": "Income Account"},
"8750 Aufl\u00f6sung von Gewinnr\u00fccklagen": {"account_type": "Income Account"},
"8800 Zuweisung zu unversteuerten R\u00fccklagen": {"account_type": "Expense Account"},
"8900 Zuweisung zu Gewinnr\u00fccklagen": {"account_type": "Expense Account"},
"8100 Buchwert abgegangener Beteiligungen": {"account_type": "Expense Account"},
"8130 Buchwert abgegangener Wertpapiere des Umlaufverm\u00f6gens": {"account_type": "Expense Account"},
"8120 Buchwert abgegangener sonstiger Finanzanlagen": {"account_type": "Expense Account"},
"8990 Gewinnabfuhr bzw. Verlust\u00fcberrechnung aus Ergebnisabf\u00fchrungsvertr\u00e4gen": {"account_type": "Expense Account"},
"8350 nicht ausgenutzte Lieferantenskonti": {"account_type": "Expense Account"},
"root_type": "Income"
},
"Klasse 9 Passiva: Eigenkapital, R\u00fccklagen, stille Einlagen, Abschlusskonten": {
"9000 Gezeichnetes bzw. gewidmetes Kapital": {
"account_type": "Equity"
},
"9200 Kapitalr\u00fccklagen": {
"account_type": "Equity"
},
"9300 Gewinnr\u00fccklagen": {
"account_type": "Equity"
},
"9400 Bewertungsreserven uns sonst. unversteuerte R\u00fccklagen": {
"account_type": "Equity"
},
"9600 Private Entnahmen": {"account_type": "Equity"},
"9610 Privatsteuern": {"account_type": "Equity"},
"9700 Einlagen stiller Gesellschafter ": {"account_type": "Equity"},
"9900 Evidenzkonto": {"account_type": "Equity"},
"9800 Er\u00f6ffnungsbilanzkonto (EBK)": {"account_type": "Equity"},
"9880 Jahresergebnis laut Gewinn- und Verlustrechnung (G+V)": {"account_type": "Equity"},
"9850 Schlussbilanzkonto (SBK)": {"account_type": "Round Off"},
"9190 nicht eingeforderte ausstehende Einlagen und berechtigte Entnahmen von Gesellschaftern": {
"account_type": "Equity"
},
"root_type": "Equity"
}
} }
} }
}

View File

@@ -9,61 +9,118 @@ def get():
return { return {
_("Application of Funds (Assets)"): { _("Application of Funds (Assets)"): {
_("Current Assets"): { _("Current Assets"): {
_("Accounts Receivable"): {_("Debtors"): {"account_type": "Receivable"}}, _("Accounts Receivable"): {
_("Bank Accounts"): {"account_type": "Bank", "is_group": 1}, _("Debtors"): {
_("Cash In Hand"): {_("Cash"): {"account_type": "Cash"}, "account_type": "Cash"}, "account_type": "Receivable"
_("Loans and Advances (Assets)"): { }
_("Employee Advances"): {}, },
_("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"): {}
}, },
_("Securities and Deposits"): {_("Earnest Money"): {}},
_("Stock Assets"): { _("Stock Assets"): {
_("Stock In Hand"): {"account_type": "Stock"}, _("Stock In Hand"): {
"account_type": "Stock"
},
"account_type": "Stock", "account_type": "Stock",
}, },
_("Tax Assets"): {"is_group": 1}, _("Tax Assets"): {
"is_group": 1
}
}, },
_("Fixed Assets"): { _("Fixed Assets"): {
_("Capital Equipments"): {"account_type": "Fixed Asset"}, _("Capital Equipments"): {
_("Electronic Equipments"): {"account_type": "Fixed Asset"}, "account_type": "Fixed Asset"
_("Furnitures and Fixtures"): {"account_type": "Fixed Asset"}, },
_("Office Equipments"): {"account_type": "Fixed Asset"}, _("Electronic Equipments"): {
_("Plants and Machineries"): {"account_type": "Fixed Asset"}, "account_type": "Fixed Asset"
_("Buildings"): {"account_type": "Fixed Asset"}, },
_("Softwares"): {"account_type": "Fixed Asset"}, _("Furnitures and Fixtures"): {
_("Accumulated Depreciation"): {"account_type": "Accumulated Depreciation"}, "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"): { _("CWIP Account"): {
"account_type": "Capital Work in Progress", "account_type": "Capital Work in Progress",
}
}, },
_("Investments"): {
"is_group": 1
}, },
_("Investments"): {"is_group": 1}, _("Temporary Accounts"): {
_("Temporary Accounts"): {_("Temporary Opening"): {"account_type": "Temporary"}}, _("Temporary Opening"): {
"root_type": "Asset", "account_type": "Temporary"
}
},
"root_type": "Asset"
}, },
_("Expenses"): { _("Expenses"): {
_("Direct Expenses"): { _("Direct Expenses"): {
_("Stock Expenses"): { _("Stock Expenses"): {
_("Cost of Goods Sold"): {"account_type": "Cost of Goods Sold"}, _("Cost of Goods Sold"): {
"account_type": "Cost of Goods Sold"
},
_("Expenses Included In Asset Valuation"): { _("Expenses Included In Asset Valuation"): {
"account_type": "Expenses Included In Asset Valuation" "account_type": "Expenses Included In Asset Valuation"
}, },
_("Expenses Included In Valuation"): {"account_type": "Expenses Included In Valuation"}, _("Expenses Included In Valuation"): {
_("Stock Adjustment"): {"account_type": "Stock Adjustment"}, "account_type": "Expenses Included In Valuation"
},
_("Stock Adjustment"): {
"account_type": "Stock Adjustment"
}
}, },
}, },
_("Indirect Expenses"): { _("Indirect Expenses"): {
_("Administrative Expenses"): {}, _("Administrative Expenses"): {},
_("Commission on Sales"): {}, _("Commission on Sales"): {},
_("Depreciation"): {"account_type": "Depreciation"}, _("Depreciation"): {
"account_type": "Depreciation"
},
_("Entertainment Expenses"): {}, _("Entertainment Expenses"): {},
_("Freight and Forwarding Charges"): {"account_type": "Chargeable"}, _("Freight and Forwarding Charges"): {
"account_type": "Chargeable"
},
_("Legal Expenses"): {}, _("Legal Expenses"): {},
_("Marketing Expenses"): {"account_type": "Chargeable"}, _("Marketing Expenses"): {
_("Miscellaneous Expenses"): {"account_type": "Chargeable"}, "account_type": "Chargeable"
},
_("Miscellaneous Expenses"): {
"account_type": "Chargeable"
},
_("Office Maintenance Expenses"): {}, _("Office Maintenance Expenses"): {},
_("Office Rent"): {}, _("Office Rent"): {},
_("Postal Expenses"): {}, _("Postal Expenses"): {},
_("Print and Stationery"): {}, _("Print and Stationery"): {},
_("Round Off"): {"account_type": "Round Off"}, _("Round Off"): {
"account_type": "Round Off"
},
_("Salary"): {}, _("Salary"): {},
_("Sales Expenses"): {}, _("Sales Expenses"): {},
_("Telephone Expenses"): {}, _("Telephone Expenses"): {},
@@ -71,39 +128,61 @@ def get():
_("Utility Expenses"): {}, _("Utility Expenses"): {},
_("Write Off"): {}, _("Write Off"): {},
_("Exchange Gain/Loss"): {}, _("Exchange Gain/Loss"): {},
_("Gain/Loss on Asset Disposal"): {}, _("Gain/Loss on Asset Disposal"): {}
}, },
"root_type": "Expense", "root_type": "Expense"
}, },
_("Income"): { _("Income"): {
_("Direct Income"): {_("Sales"): {}, _("Service"): {}}, _("Direct Income"): {
_("Indirect Income"): {"is_group": 1}, _("Sales"): {},
"root_type": "Income", _("Service"): {}
},
_("Indirect Income"): {
"is_group": 1
},
"root_type": "Income"
}, },
_("Source of Funds (Liabilities)"): { _("Source of Funds (Liabilities)"): {
_("Current Liabilities"): { _("Current Liabilities"): {
_("Accounts Payable"): { _("Accounts Payable"): {
_("Creditors"): {"account_type": "Payable"}, _("Creditors"): {
"account_type": "Payable"
},
_("Payroll Payable"): {}, _("Payroll Payable"): {},
}, },
_("Stock Liabilities"): { _("Stock Liabilities"): {
_("Stock Received But Not Billed"): {"account_type": "Stock Received But Not Billed"}, _("Stock Received But Not Billed"): {
_("Asset Received But Not Billed"): {"account_type": "Asset 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
}, },
_("Duties and Taxes"): {"account_type": "Tax", "is_group": 1},
_("Loans (Liabilities)"): { _("Loans (Liabilities)"): {
_("Secured Loans"): {}, _("Secured Loans"): {},
_("Unsecured Loans"): {}, _("Unsecured Loans"): {},
_("Bank Overdraft Account"): {}, _("Bank Overdraft Account"): {},
}, },
}, },
"root_type": "Liability", "root_type": "Liability"
}, },
_("Equity"): { _("Equity"): {
_("Capital Stock"): {"account_type": "Equity"}, _("Capital Stock"): {
_("Dividends Paid"): {"account_type": "Equity"}, "account_type": "Equity"
_("Opening Balance Equity"): {"account_type": "Equity"},
_("Retained Earnings"): {"account_type": "Equity"},
"root_type": "Equity",
}, },
_("Dividends Paid"): {
"account_type": "Equity"
},
_("Opening Balance Equity"): {
"account_type": "Equity"
},
_("Retained Earnings"): {
"account_type": "Equity"
},
"root_type": "Equity"
}
} }

View File

@@ -10,149 +10,284 @@ def get():
_("Application of Funds (Assets)"): { _("Application of Funds (Assets)"): {
_("Current Assets"): { _("Current Assets"): {
_("Accounts Receivable"): { _("Accounts Receivable"): {
_("Debtors"): {"account_type": "Receivable", "account_number": "1310"}, _("Debtors"): {
"account_number": "1300", "account_type": "Receivable",
"account_number": "1310"
},
"account_number": "1300"
},
_("Bank Accounts"): {
"account_type": "Bank",
"is_group": 1,
"account_number": "1200"
}, },
_("Bank Accounts"): {"account_type": "Bank", "is_group": 1, "account_number": "1200"},
_("Cash In Hand"): { _("Cash In Hand"): {
_("Cash"): {"account_type": "Cash", "account_number": "1110"}, _("Cash"): {
"account_type": "Cash", "account_type": "Cash",
"account_number": "1100", "account_number": "1110"
},
"account_type": "Cash",
"account_number": "1100"
}, },
_("Loans and Advances (Assets)"): { _("Loans and Advances (Assets)"): {
_("Employee Advances"): {"account_number": "1610"}, _("Employee Advances"): {
"account_number": "1600", "account_number": "1610"
},
"account_number": "1600"
}, },
_("Securities and Deposits"): { _("Securities and Deposits"): {
_("Earnest Money"): {"account_number": "1651"}, _("Earnest Money"): {
"account_number": "1650", "account_number": "1651"
},
"account_number": "1650"
}, },
_("Stock Assets"): { _("Stock Assets"): {
_("Stock In Hand"): {"account_type": "Stock", "account_number": "1410"}, _("Stock In Hand"): {
"account_type": "Stock", "account_type": "Stock",
"account_number": "1400", "account_number": "1410"
}, },
_("Tax Assets"): {"is_group": 1, "account_number": "1500"}, "account_type": "Stock",
"account_number": "1100-1600", "account_number": "1400"
},
_("Tax Assets"): {
"is_group": 1,
"account_number": "1500"
},
"account_number": "1100-1600"
}, },
_("Fixed Assets"): { _("Fixed Assets"): {
_("Capital Equipments"): {"account_type": "Fixed Asset", "account_number": "1710"}, _("Capital Equipments"): {
_("Electronic Equipments"): {"account_type": "Fixed Asset", "account_number": "1720"}, "account_type": "Fixed Asset",
_("Furnitures and Fixtures"): {"account_type": "Fixed Asset", "account_number": "1730"}, "account_number": "1710"
_("Office Equipments"): {"account_type": "Fixed Asset", "account_number": "1740"}, },
_("Plants and Machineries"): {"account_type": "Fixed Asset", "account_number": "1750"}, _("Electronic Equipments"): {
_("Buildings"): {"account_type": "Fixed Asset", "account_number": "1760"}, "account_type": "Fixed Asset",
_("Softwares"): {"account_type": "Fixed Asset", "account_number": "1770"}, "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"): { _("Accumulated Depreciation"): {
"account_type": "Accumulated Depreciation", "account_type": "Accumulated Depreciation",
"account_number": "1780", "account_number": "1780"
}, },
_("CWIP Account"): {"account_type": "Capital Work in Progress", "account_number": "1790"}, _("CWIP Account"): {
"account_number": "1700", "account_type": "Capital Work in Progress",
"account_number": "1790"
},
"account_number": "1700"
},
_("Investments"): {
"is_group": 1,
"account_number": "1800"
}, },
_("Investments"): {"is_group": 1, "account_number": "1800"},
_("Temporary Accounts"): { _("Temporary Accounts"): {
_("Temporary Opening"): {"account_type": "Temporary", "account_number": "1910"}, _("Temporary Opening"): {
"account_number": "1900", "account_type": "Temporary",
"account_number": "1910"
},
"account_number": "1900"
}, },
"root_type": "Asset", "root_type": "Asset",
"account_number": "1000", "account_number": "1000"
}, },
_("Expenses"): { _("Expenses"): {
_("Direct Expenses"): { _("Direct Expenses"): {
_("Stock Expenses"): { _("Stock Expenses"): {
_("Cost of Goods Sold"): {"account_type": "Cost of Goods Sold", "account_number": "5111"}, _("Cost of Goods Sold"): {
"account_type": "Cost of Goods Sold",
"account_number": "5111"
},
_("Expenses Included In Asset Valuation"): { _("Expenses Included In Asset Valuation"): {
"account_type": "Expenses Included In Asset Valuation", "account_type": "Expenses Included In Asset Valuation",
"account_number": "5112", "account_number": "5112"
}, },
_("Expenses Included In Valuation"): { _("Expenses Included In Valuation"): {
"account_type": "Expenses Included In Valuation", "account_type": "Expenses Included In Valuation",
"account_number": "5118", "account_number": "5118"
}, },
_("Stock Adjustment"): {"account_type": "Stock Adjustment", "account_number": "5119"}, _("Stock Adjustment"): {
"account_number": "5110", "account_type": "Stock Adjustment",
"account_number": "5119"
}, },
"account_number": "5100", "account_number": "5110"
},
"account_number": "5100"
}, },
_("Indirect Expenses"): { _("Indirect Expenses"): {
_("Administrative Expenses"): {"account_number": "5201"}, _("Administrative Expenses"): {
_("Commission on Sales"): {"account_number": "5202"}, "account_number": "5201"
_("Depreciation"): {"account_type": "Depreciation", "account_number": "5203"}, },
_("Entertainment Expenses"): {"account_number": "5204"}, _("Commission on Sales"): {
_("Freight and Forwarding Charges"): {"account_type": "Chargeable", "account_number": "5205"}, "account_number": "5202"
_("Legal Expenses"): {"account_number": "5206"}, },
_("Marketing Expenses"): {"account_type": "Chargeable", "account_number": "5207"}, _("Depreciation"): {
_("Office Maintenance Expenses"): {"account_number": "5208"}, "account_type": "Depreciation",
_("Office Rent"): {"account_number": "5209"}, "account_number": "5203"
_("Postal Expenses"): {"account_number": "5210"}, },
_("Print and Stationery"): {"account_number": "5211"}, _("Entertainment Expenses"): {
_("Round Off"): {"account_type": "Round Off", "account_number": "5212"}, "account_number": "5204"
_("Salary"): {"account_number": "5213"}, },
_("Sales Expenses"): {"account_number": "5214"}, _("Freight and Forwarding Charges"): {
_("Telephone Expenses"): {"account_number": "5215"}, "account_type": "Chargeable",
_("Travel Expenses"): {"account_number": "5216"}, "account_number": "5205"
_("Utility Expenses"): {"account_number": "5217"}, },
_("Write Off"): {"account_number": "5218"}, _("Legal Expenses"): {
_("Exchange Gain/Loss"): {"account_number": "5219"}, "account_number": "5206"
_("Gain/Loss on Asset Disposal"): {"account_number": "5220"}, },
_("Miscellaneous Expenses"): {"account_type": "Chargeable", "account_number": "5221"}, _("Marketing Expenses"): {
"account_number": "5200", "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", "root_type": "Expense",
"account_number": "5000", "account_number": "5000"
}, },
_("Income"): { _("Income"): {
_("Direct Income"): { _("Direct Income"): {
_("Sales"): {"account_number": "4110"}, _("Sales"): {
_("Service"): {"account_number": "4120"}, "account_number": "4110"
"account_number": "4100", },
_("Service"): {
"account_number": "4120"
},
"account_number": "4100"
},
_("Indirect Income"): {
"is_group": 1,
"account_number": "4200"
}, },
_("Indirect Income"): {"is_group": 1, "account_number": "4200"},
"root_type": "Income", "root_type": "Income",
"account_number": "4000", "account_number": "4000"
}, },
_("Source of Funds (Liabilities)"): { _("Source of Funds (Liabilities)"): {
_("Current Liabilities"): { _("Current Liabilities"): {
_("Accounts Payable"): { _("Accounts Payable"): {
_("Creditors"): {"account_type": "Payable", "account_number": "2110"}, _("Creditors"): {
_("Payroll Payable"): {"account_number": "2120"}, "account_type": "Payable",
"account_number": "2100", "account_number": "2110"
},
_("Payroll Payable"): {
"account_number": "2120"
},
"account_number": "2100"
}, },
_("Stock Liabilities"): { _("Stock Liabilities"): {
_("Stock Received But Not Billed"): { _("Stock Received But Not Billed"): {
"account_type": "Stock Received But Not Billed", "account_type": "Stock Received But Not Billed",
"account_number": "2210", "account_number": "2210"
}, },
_("Asset Received But Not Billed"): { _("Asset Received But Not Billed"): {
"account_type": "Asset Received But Not Billed", "account_type": "Asset Received But Not Billed",
"account_number": "2211", "account_number": "2211"
}, },
"account_number": "2200", "account_number": "2200"
}, },
_("Duties and Taxes"): { _("Duties and Taxes"): {
_("TDS Payable"): {"account_number": "2310"}, _("TDS Payable"): {
"account_number": "2310"
},
"account_type": "Tax", "account_type": "Tax",
"is_group": 1, "is_group": 1,
"account_number": "2300", "account_number": "2300"
}, },
_("Loans (Liabilities)"): { _("Loans (Liabilities)"): {
_("Secured Loans"): {"account_number": "2410"}, _("Secured Loans"): {
_("Unsecured Loans"): {"account_number": "2420"}, "account_number": "2410"
_("Bank Overdraft Account"): {"account_number": "2430"},
"account_number": "2400",
}, },
"account_number": "2100-2400", _("Unsecured Loans"): {
"account_number": "2420"
},
_("Bank Overdraft Account"): {
"account_number": "2430"
},
"account_number": "2400"
},
"account_number": "2100-2400"
}, },
"root_type": "Liability", "root_type": "Liability",
"account_number": "2000", "account_number": "2000"
}, },
_("Equity"): { _("Equity"): {
_("Capital Stock"): {"account_type": "Equity", "account_number": "3100"}, _("Capital Stock"): {
_("Dividends Paid"): {"account_type": "Equity", "account_number": "3200"}, "account_type": "Equity",
_("Opening Balance Equity"): {"account_type": "Equity", "account_number": "3300"}, "account_number": "3100"
_("Retained Earnings"): {"account_type": "Equity", "account_number": "3400"},
"root_type": "Equity",
"account_number": "3000",
}, },
_("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"
}
} }

View File

@@ -20,9 +20,8 @@ class TestAccount(unittest.TestCase):
acc.company = "_Test Company" acc.company = "_Test Company"
acc.insert() acc.insert()
account_number, account_name = frappe.db.get_value( account_number, account_name = frappe.db.get_value("Account", "1210 - Debtors - _TC",
"Account", "1210 - Debtors - _TC", ["account_number", "account_name"] ["account_number", "account_name"])
)
self.assertEqual(account_number, "1210") self.assertEqual(account_number, "1210")
self.assertEqual(account_name, "Debtors") self.assertEqual(account_name, "Debtors")
@@ -31,12 +30,8 @@ class TestAccount(unittest.TestCase):
update_account_number("1210 - Debtors - _TC", new_account_name, new_account_number) update_account_number("1210 - Debtors - _TC", new_account_name, new_account_number)
new_acc = frappe.db.get_value( new_acc = frappe.db.get_value("Account", "1211-11-4 - 6 - - Debtors 1 - Test - - _TC",
"Account", ["account_name", "account_number"], as_dict=1)
"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_name, "Debtors 1 - Test -")
self.assertEqual(new_acc.account_number, "1211-11-4 - 6 -") self.assertEqual(new_acc.account_number, "1211-11-4 - 6 -")
@@ -84,9 +79,7 @@ class TestAccount(unittest.TestCase):
self.assertEqual(parent, "Securities and Deposits - _TC") self.assertEqual(parent, "Securities and Deposits - _TC")
merge_account( merge_account("Securities and Deposits - _TC", "Cash In Hand - _TC", doc.is_group, doc.root_type, doc.company)
"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 = frappe.db.get_value("Account", "Earnest Money - _TC", "parent_account")
# Parent account of the child account changes after merging # Parent account of the child account changes after merging
@@ -98,28 +91,14 @@ class TestAccount(unittest.TestCase):
doc = frappe.get_doc("Account", "Current Assets - _TC") doc = frappe.get_doc("Account", "Current Assets - _TC")
# Raise error as is_group property doesn't match # Raise error as is_group property doesn't match
self.assertRaises( self.assertRaises(frappe.ValidationError, merge_account, "Current Assets - _TC",\
frappe.ValidationError, "Accumulated Depreciation - _TC", doc.is_group, doc.root_type, doc.company)
merge_account,
"Current Assets - _TC",
"Accumulated Depreciation - _TC",
doc.is_group,
doc.root_type,
doc.company,
)
doc = frappe.get_doc("Account", "Capital Stock - _TC") doc = frappe.get_doc("Account", "Capital Stock - _TC")
# Raise error as root_type property doesn't match # Raise error as root_type property doesn't match
self.assertRaises( self.assertRaises(frappe.ValidationError, merge_account, "Capital Stock - _TC",\
frappe.ValidationError, "Softwares - _TC", doc.is_group, doc.root_type, doc.company)
merge_account,
"Capital Stock - _TC",
"Softwares - _TC",
doc.is_group,
doc.root_type,
doc.company,
)
def test_account_sync(self): def test_account_sync(self):
frappe.local.flags.pop("ignore_root_company_validation", None) frappe.local.flags.pop("ignore_root_company_validation", None)
@@ -130,12 +109,8 @@ class TestAccount(unittest.TestCase):
acc.company = "_Test Company 3" acc.company = "_Test Company 3"
acc.insert() acc.insert()
acc_tc_4 = frappe.db.get_value( acc_tc_4 = frappe.db.get_value('Account', {'account_name': "Test Sync Account", "company": "_Test Company 4"})
"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_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_4, "Test Sync Account - _TC4")
self.assertEqual(acc_tc_5, "Test Sync Account - _TC5") self.assertEqual(acc_tc_5, "Test Sync Account - _TC5")
@@ -163,26 +138,8 @@ class TestAccount(unittest.TestCase):
update_account_number(acc.name, "Test Rename Sync Account", "1234") update_account_number(acc.name, "Test Rename Sync Account", "1234")
# Check if renamed in children # Check if renamed in children
self.assertTrue( self.assertTrue(frappe.db.exists("Account", {'account_name': "Test Rename Sync Account", "company": "_Test Company 4", "account_number": "1234"}))
frappe.db.exists( self.assertTrue(frappe.db.exists("Account", {'account_name': "Test Rename Sync Account", "company": "_Test Company 5", "account_number": "1234"}))
"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 - _TC3")
frappe.delete_doc("Account", "1234 - Test Rename Sync Account - _TC4") frappe.delete_doc("Account", "1234 - Test Rename Sync Account - _TC4")
@@ -198,71 +155,25 @@ class TestAccount(unittest.TestCase):
acc.company = "_Test Company 3" acc.company = "_Test Company 3"
acc.insert() acc.insert()
self.assertTrue( self.assertTrue(frappe.db.exists("Account", {'account_name': "Test Group Account", "company": "_Test Company 4"}))
frappe.db.exists( self.assertTrue(frappe.db.exists("Account", {'account_name': "Test Group Account", "company": "_Test Company 5"}))
"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 # Try renaming child company account
acc_tc_5 = frappe.db.get_value( acc_tc_5 = frappe.db.get_value('Account', {'account_name': "Test Group Account", "company": "_Test Company 5"})
"Account", {"account_name": "Test Group Account", "company": "_Test Company 5"} self.assertRaises(frappe.ValidationError, update_account_number, acc_tc_5, "Test Modified Account")
)
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 # Rename child company account with allow_account_creation_against_child_company enabled
frappe.db.set_value( frappe.db.set_value("Company", "_Test Company 5", "allow_account_creation_against_child_company", 1)
"Company", "_Test Company 5", "allow_account_creation_against_child_company", 1
)
update_account_number(acc_tc_5, "Test Modified Account") update_account_number(acc_tc_5, "Test Modified Account")
self.assertTrue( self.assertTrue(frappe.db.exists("Account", {'name': "Test Modified Account - _TC5", "company": "_Test Company 5"}))
frappe.db.exists(
"Account", {"name": "Test Modified Account - _TC5", "company": "_Test Company 5"}
)
)
frappe.db.set_value( frappe.db.set_value("Company", "_Test Company 5", "allow_account_creation_against_child_company", 0)
"Company", "_Test Company 5", "allow_account_creation_against_child_company", 0
)
to_delete = [ to_delete = ["Test Group Account - _TC3", "Test Group Account - _TC4", "Test Modified Account - _TC5"]
"Test Group Account - _TC3",
"Test Group Account - _TC4",
"Test Modified Account - _TC5",
]
for doc in to_delete: for doc in to_delete:
frappe.delete_doc("Account", doc) 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): def _make_test_records(verbose=None):
from frappe.test_runner import make_test_objects from frappe.test_runner import make_test_objects
@@ -273,16 +184,20 @@ def _make_test_records(verbose=None):
["_Test Bank USD", "Bank Accounts", 0, "Bank", "USD"], ["_Test Bank USD", "Bank Accounts", 0, "Bank", "USD"],
["_Test Bank EUR", "Bank Accounts", 0, "Bank", "EUR"], ["_Test Bank EUR", "Bank Accounts", 0, "Bank", "EUR"],
["_Test Cash", "Cash In Hand", 0, "Cash", None], ["_Test Cash", "Cash In Hand", 0, "Cash", None],
["_Test Account Stock Expenses", "Direct Expenses", 1, None, None], ["_Test Account Stock Expenses", "Direct Expenses", 1, None, None],
["_Test Account Shipping Charges", "_Test Account Stock Expenses", 0, "Chargeable", 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 Customs Duty", "_Test Account Stock Expenses", 0, "Tax", None],
["_Test Account Insurance Charges", "_Test Account Stock Expenses", 0, "Chargeable", 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 Account Stock Adjustment", "_Test Account Stock Expenses", 0, "Stock Adjustment", None],
["_Test Employee Advance", "Current Liabilities", 0, None, None], ["_Test Employee Advance", "Current Liabilities", 0, None, None],
["_Test Account Tax Assets", "Current Assets", 1, None, None], ["_Test Account Tax Assets", "Current Assets", 1, None, None],
["_Test Account VAT", "_Test Account Tax Assets", 0, "Tax", None], ["_Test Account VAT", "_Test Account Tax Assets", 0, "Tax", None],
["_Test Account Service Tax", "_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 Reserves and Surplus", "Current Liabilities", 0, None, None],
["_Test Account Cost for Goods Sold", "Expenses", 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 Excise Duty", "_Test Account Tax Assets", 0, "Tax", None],
["_Test Account Education Cess", "_Test Account Tax Assets", 0, "Tax", None], ["_Test Account Education Cess", "_Test Account Tax Assets", 0, "Tax", None],
@@ -291,45 +206,38 @@ def _make_test_records(verbose=None):
["_Test Account Discount", "Direct Expenses", 0, None, None], ["_Test Account Discount", "Direct Expenses", 0, None, None],
["_Test Write Off", "Indirect Expenses", 0, None, None], ["_Test Write Off", "Indirect Expenses", 0, None, None],
["_Test Exchange Gain/Loss", "Indirect Expenses", 0, None, None], ["_Test Exchange Gain/Loss", "Indirect Expenses", 0, None, None],
["_Test Account Sales", "Direct Income", 0, None, None], ["_Test Account Sales", "Direct Income", 0, None, None],
# related to Account Inventory Integration # related to Account Inventory Integration
["_Test Account Stock In Hand", "Current Assets", 0, None, None], ["_Test Account Stock In Hand", "Current Assets", 0, None, None],
# fixed asset depreciation # fixed asset depreciation
["_Test Fixed Asset", "Current Assets", 0, "Fixed Asset", None], ["_Test Fixed Asset", "Current Assets", 0, "Fixed Asset", None],
["_Test Accumulated Depreciations", "Current Assets", 0, "Accumulated Depreciation", None], ["_Test Accumulated Depreciations", "Current Assets", 0, "Accumulated Depreciation", None],
["_Test Depreciations", "Expenses", 0, None, None], ["_Test Depreciations", "Expenses", 0, None, None],
["_Test Gain/Loss on Asset Disposal", "Expenses", 0, None, None], ["_Test Gain/Loss on Asset Disposal", "Expenses", 0, None, None],
# Receivable / Payable Account # Receivable / Payable Account
["_Test Receivable", "Current Assets", 0, "Receivable", None], ["_Test Receivable", "Current Assets", 0, "Receivable", None],
["_Test Payable", "Current Liabilities", 0, "Payable", None], ["_Test Payable", "Current Liabilities", 0, "Payable", None],
["_Test Receivable USD", "Current Assets", 0, "Receivable", "USD"], ["_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 [ for company, abbr in [["_Test Company", "_TC"], ["_Test Company 1", "_TC1"], ["_Test Company with perpetual inventory", "TCP1"]]:
["_Test Company", "_TC"], test_objects = make_test_objects("Account", [{
["_Test Company 1", "_TC1"],
["_Test Company with perpetual inventory", "TCP1"],
]:
test_objects = make_test_objects(
"Account",
[
{
"doctype": "Account", "doctype": "Account",
"account_name": account_name, "account_name": account_name,
"parent_account": parent_account + " - " + abbr, "parent_account": parent_account + " - " + abbr,
"company": company, "company": company,
"is_group": is_group, "is_group": is_group,
"account_type": account_type, "account_type": account_type,
"account_currency": currency, "account_currency": currency
} } for account_name, parent_account, is_group, account_type, currency in accounts])
for account_name, parent_account, is_group, account_type, currency in accounts
],
)
return test_objects return test_objects
def get_inventory_account(company, warehouse=None): def get_inventory_account(company, warehouse=None):
account = None account = None
if warehouse: if warehouse:
@@ -339,24 +247,19 @@ def get_inventory_account(company, warehouse=None):
return account return account
def create_account(**kwargs): def create_account(**kwargs):
account = frappe.db.get_value( account = frappe.db.get_value("Account", filters={"account_name": kwargs.get("account_name"), "company": kwargs.get("company")})
"Account", filters={"account_name": kwargs.get("account_name"), "company": kwargs.get("company")}
)
if account: if account:
return account return account
else: else:
account = frappe.get_doc( account = frappe.get_doc(dict(
dict( doctype = "Account",
doctype="Account", account_name = kwargs.get('account_name'),
account_name=kwargs.get("account_name"), account_type = kwargs.get('account_type'),
account_type=kwargs.get("account_type"), parent_account = kwargs.get('parent_account'),
parent_account=kwargs.get("parent_account"), company = kwargs.get('company'),
company=kwargs.get("company"), account_currency = kwargs.get('account_currency')
account_currency=kwargs.get("account_currency"), ))
)
)
account.save() account.save()
return account.name return account.name

View File

@@ -17,21 +17,13 @@ class AccountingDimension(Document):
self.set_fieldname_and_label() self.set_fieldname_and_label()
def validate(self): def validate(self):
if self.document_type in core_doctypes_list + ( if self.document_type in core_doctypes_list + ('Accounting Dimension', 'Project',
"Accounting Dimension", 'Cost Center', 'Accounting Dimension Detail', 'Company', 'Account') :
"Project",
"Cost Center",
"Accounting Dimension Detail",
"Company",
"Account",
):
msg = _("Not allowed to create accounting dimension for {0}").format(self.document_type) msg = _("Not allowed to create accounting dimension for {0}").format(self.document_type)
frappe.throw(msg) frappe.throw(msg)
exists = frappe.db.get_value( exists = frappe.db.get_value("Accounting Dimension", {'document_type': self.document_type}, ['name'])
"Accounting Dimension", {"document_type": self.document_type}, ["name"]
)
if exists and self.is_new(): if exists and self.is_new():
frappe.throw(_("Document Type already used as a dimension")) frappe.throw(_("Document Type already used as a dimension"))
@@ -50,13 +42,13 @@ class AccountingDimension(Document):
if frappe.flags.in_test: if frappe.flags.in_test:
make_dimension_in_accounting_doctypes(doc=self) make_dimension_in_accounting_doctypes(doc=self)
else: 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): def on_trash(self):
if frappe.flags.in_test: if frappe.flags.in_test:
delete_accounting_dimension(doc=self) delete_accounting_dimension(doc=self)
else: 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): def set_fieldname_and_label(self):
if not self.label: if not self.label:
@@ -68,7 +60,6 @@ class AccountingDimension(Document):
def on_update(self): def on_update(self):
frappe.flags.accounting_dimensions = None frappe.flags.accounting_dimensions = None
def make_dimension_in_accounting_doctypes(doc, doclist=None): def make_dimension_in_accounting_doctypes(doc, doclist=None):
if not doclist: if not doclist:
doclist = get_doctypes_with_dimensions() doclist = get_doctypes_with_dimensions()
@@ -79,9 +70,9 @@ def make_dimension_in_accounting_doctypes(doc, doclist=None):
for doctype in doclist: for doctype in doclist:
if (doc_count + 1) % 2 == 0: if (doc_count + 1) % 2 == 0:
insert_after_field = "dimension_col_break" insert_after_field = 'dimension_col_break'
else: else:
insert_after_field = "accounting_dimensions_section" insert_after_field = 'accounting_dimensions_section'
df = { df = {
"fieldname": doc.fieldname, "fieldname": doc.fieldname,
@@ -89,33 +80,30 @@ def make_dimension_in_accounting_doctypes(doc, doclist=None):
"fieldtype": "Link", "fieldtype": "Link",
"options": doc.document_type, "options": doc.document_type,
"insert_after": insert_after_field, "insert_after": insert_after_field,
"owner": "Administrator", "owner": "Administrator"
} }
meta = frappe.get_meta(doctype, cached=False) meta = frappe.get_meta(doctype, cached=False)
fieldnames = [d.fieldname for d in meta.get("fields")] fieldnames = [d.fieldname for d in meta.get("fields")]
if df["fieldname"] not in fieldnames: if df['fieldname'] not in fieldnames:
if doctype == "Budget": if doctype == "Budget":
add_dimension_to_budget_doctype(df.copy(), doc) add_dimension_to_budget_doctype(df.copy(), doc)
else: else:
create_custom_field(doctype, df, ignore_validate=True) create_custom_field(doctype, df)
count += 1 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) frappe.clear_cache(doctype=doctype)
def add_dimension_to_budget_doctype(df, doc): def add_dimension_to_budget_doctype(df, doc):
df.update( df.update({
{
"insert_after": "cost_center", "insert_after": "cost_center",
"depends_on": "eval:doc.budget_against == '{0}'".format(doc.document_type), "depends_on": "eval:doc.budget_against == '{0}'".format(doc.document_type)
} })
)
create_custom_field("Budget", df, ignore_validate=True) create_custom_field("Budget", df)
property_setter = frappe.db.exists("Property Setter", "Budget-budget_against-options") property_setter = frappe.db.exists("Property Setter", "Budget-budget_against-options")
@@ -124,44 +112,36 @@ def add_dimension_to_budget_doctype(df, doc):
property_setter_doc.value = property_setter_doc.value + "\n" + doc.document_type property_setter_doc.value = property_setter_doc.value + "\n" + doc.document_type
property_setter_doc.save() property_setter_doc.save()
frappe.clear_cache(doctype="Budget") frappe.clear_cache(doctype='Budget')
else: else:
frappe.get_doc( frappe.get_doc({
{
"doctype": "Property Setter", "doctype": "Property Setter",
"doctype_or_field": "DocField", "doctype_or_field": "DocField",
"doc_type": "Budget", "doc_type": "Budget",
"field_name": "budget_against", "field_name": "budget_against",
"property": "options", "property": "options",
"property_type": "Text", "property_type": "Text",
"value": "\nCost Center\nProject\n" + doc.document_type, "value": "\nCost Center\nProject\n" + doc.document_type
} }).insert(ignore_permissions=True)
).insert(ignore_permissions=True)
def delete_accounting_dimension(doc): def delete_accounting_dimension(doc):
doclist = get_doctypes_with_dimensions() doclist = get_doctypes_with_dimensions()
frappe.db.sql( frappe.db.sql("""
"""
DELETE FROM `tabCustom Field` DELETE FROM `tabCustom Field`
WHERE fieldname = %s WHERE fieldname = %s
AND dt IN (%s)""" AND dt IN (%s)""" % #nosec
% ("%s", ", ".join(["%s"] * len(doclist))), # nosec ('%s', ', '.join(['%s']* len(doclist))), tuple([doc.fieldname] + doclist))
tuple([doc.fieldname] + doclist),
)
frappe.db.sql( frappe.db.sql("""
"""
DELETE FROM `tabProperty Setter` DELETE FROM `tabProperty Setter`
WHERE field_name = %s WHERE field_name = %s
AND doc_type IN (%s)""" AND doc_type IN (%s)""" % #nosec
% ("%s", ", ".join(["%s"] * len(doclist))), # nosec ('%s', ', '.join(['%s']* len(doclist))), tuple([doc.fieldname] + doclist))
tuple([doc.fieldname] + doclist),
)
budget_against_property = frappe.get_doc("Property Setter", "Budget-budget_against-options") 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: if doc.document_type in value_list:
value_list.remove(doc.document_type) value_list.remove(doc.document_type)
@@ -172,7 +152,6 @@ def delete_accounting_dimension(doc):
for doctype in doclist: for doctype in doclist:
frappe.clear_cache(doctype=doctype) frappe.clear_cache(doctype=doctype)
@frappe.whitelist() @frappe.whitelist()
def disable_dimension(doc): def disable_dimension(doc):
if frappe.flags.in_test: if frappe.flags.in_test:
@@ -180,11 +159,10 @@ def disable_dimension(doc):
else: else:
frappe.enqueue(toggle_disabling, doc=doc) frappe.enqueue(toggle_disabling, doc=doc)
def toggle_disabling(doc): def toggle_disabling(doc):
doc = json.loads(doc) doc = json.loads(doc)
if doc.get("disabled"): if doc.get('disabled'):
df = {"read_only": 1} df = {"read_only": 1}
else: else:
df = {"read_only": 0} df = {"read_only": 0}
@@ -192,7 +170,7 @@ def toggle_disabling(doc):
doclist = get_doctypes_with_dimensions() doclist = get_doctypes_with_dimensions()
for doctype in doclist: 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: if field:
custom_field = frappe.get_doc("Custom Field", field) custom_field = frappe.get_doc("Custom Field", field)
custom_field.update(df) custom_field.update(df)
@@ -200,82 +178,61 @@ def toggle_disabling(doc):
frappe.clear_cache(doctype=doctype) frappe.clear_cache(doctype=doctype)
def get_doctypes_with_dimensions(): def get_doctypes_with_dimensions():
return frappe.get_hooks("accounting_dimension_doctypes") 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: if frappe.flags.accounting_dimensions is None:
frappe.flags.accounting_dimensions = frappe.get_all( frappe.flags.accounting_dimensions = frappe.get_all("Accounting Dimension",
"Accounting Dimension", fields=["label", "fieldname", "disabled", "document_type"])
fields=["label", "fieldname", "disabled", "document_type"],
filters=filters,
)
if as_list: if as_list:
return [d.fieldname for d in frappe.flags.accounting_dimensions] return [d.fieldname for d in frappe.flags.accounting_dimensions]
else: else:
return frappe.flags.accounting_dimensions return frappe.flags.accounting_dimensions
def get_checks_for_pl_and_bs_accounts(): def get_checks_for_pl_and_bs_accounts():
dimensions = frappe.db.sql( dimensions = frappe.db.sql("""SELECT p.label, p.disabled, p.fieldname, c.default_dimension, c.company, c.mandatory_for_pl, c.mandatory_for_bs
"""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 FROM `tabAccounting Dimension`p ,`tabAccounting Dimension Detail` c
WHERE p.name = c.parent""", WHERE p.name = c.parent""", as_dict=1)
as_dict=1,
)
return dimensions return dimensions
def get_dimension_with_children(doctype, dimension):
def get_dimension_with_children(doctype, dimensions): if isinstance(dimension, list):
dimension = dimension[0]
if isinstance(dimensions, str):
dimensions = [dimensions]
all_dimensions = [] all_dimensions = []
for dimension in dimensions:
lft, rgt = frappe.db.get_value(doctype, dimension, ["lft", "rgt"]) lft, rgt = frappe.db.get_value(doctype, dimension, ["lft", "rgt"])
children = frappe.get_all( children = frappe.get_all(doctype, filters={"lft": [">=", lft], "rgt": ["<=", rgt]}, order_by="lft")
doctype, filters={"lft": [">=", lft], "rgt": ["<=", rgt]}, order_by="lft"
)
all_dimensions += [c.name for c in children] all_dimensions += [c.name for c in children]
return all_dimensions return all_dimensions
@frappe.whitelist() @frappe.whitelist()
def get_dimensions(with_cost_center_and_project=False): def get_dimensions(with_cost_center_and_project=False):
dimension_filters = frappe.db.sql( dimension_filters = frappe.db.sql("""
"""
SELECT label, fieldname, document_type SELECT label, fieldname, document_type
FROM `tabAccounting Dimension` FROM `tabAccounting Dimension`
WHERE disabled = 0 WHERE disabled = 0
""", """, as_dict=1)
as_dict=1,
)
default_dimensions = frappe.db.sql( default_dimensions = frappe.db.sql("""SELECT p.fieldname, c.company, c.default_dimension
"""SELECT p.fieldname, c.company, c.default_dimension
FROM `tabAccounting Dimension Detail` c, `tabAccounting Dimension` p FROM `tabAccounting Dimension Detail` c, `tabAccounting Dimension` p
WHERE c.parent = p.name""", WHERE c.parent = p.name""", as_dict=1)
as_dict=1,
)
if with_cost_center_and_project: if with_cost_center_and_project:
dimension_filters.extend( dimension_filters.extend([
[ {
{"fieldname": "cost_center", "document_type": "Cost Center"}, 'fieldname': 'cost_center',
{"fieldname": "project", "document_type": "Project"}, 'document_type': 'Cost Center'
] },
) {
'fieldname': 'project',
'document_type': 'Project'
}
])
default_dimensions_map = {} default_dimensions_map = {}
for dimension in default_dimensions: for dimension in default_dimensions:

View File

@@ -8,8 +8,7 @@ import frappe
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry 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 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): class TestAccountingDimension(unittest.TestCase):
def setUp(self): def setUp(self):
@@ -19,9 +18,7 @@ class TestAccountingDimension(unittest.TestCase):
si = create_sales_invoice(do_not_save=1) si = create_sales_invoice(do_not_save=1)
si.location = "Block 1" si.location = "Block 1"
si.append( si.append("items", {
"items",
{
"item_code": "_Test Item", "item_code": "_Test Item",
"warehouse": "_Test Warehouse - _TC", "warehouse": "_Test Warehouse - _TC",
"qty": 1, "qty": 1,
@@ -30,16 +27,15 @@ class TestAccountingDimension(unittest.TestCase):
"expense_account": "Cost of Goods Sold - _TC", "expense_account": "Cost of Goods Sold - _TC",
"cost_center": "_Test Cost Center - _TC", "cost_center": "_Test Cost Center - _TC",
"department": "_Test Department - _TC", "department": "_Test Department - _TC",
"location": "Block 1", "location": "Block 1"
}, })
)
si.save() si.save()
si.submit() si.submit()
gle = frappe.get_doc("GL Entry", {"voucher_no": si.name, "account": "Sales - _TC"}) 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): def test_dimension_against_journal_entry(self):
je = make_journal_entry("Sales - _TC", "Sales Expenses - _TC", 500, save=False) je = make_journal_entry("Sales - _TC", "Sales Expenses - _TC", 500, save=False)
@@ -54,14 +50,12 @@ class TestAccountingDimension(unittest.TestCase):
gle = frappe.get_doc("GL Entry", {"voucher_no": je.name, "account": "Sales - _TC"}) 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"}) gle1 = frappe.get_doc("GL Entry", {"voucher_no": je.name, "account": "Sales Expenses - _TC"})
self.assertEqual(gle.get("department"), "_Test Department - _TC") self.assertEqual(gle.get('department'), "_Test Department - _TC")
self.assertEqual(gle1.get("department"), "_Test Department - _TC") self.assertEqual(gle1.get('department'), "_Test Department - _TC")
def test_mandatory(self): def test_mandatory(self):
si = create_sales_invoice(do_not_save=1) si = create_sales_invoice(do_not_save=1)
si.append( si.append("items", {
"items",
{
"item_code": "_Test Item", "item_code": "_Test Item",
"warehouse": "_Test Warehouse - _TC", "warehouse": "_Test Warehouse - _TC",
"qty": 1, "qty": 1,
@@ -69,9 +63,8 @@ class TestAccountingDimension(unittest.TestCase):
"income_account": "Sales - _TC", "income_account": "Sales - _TC",
"expense_account": "Cost of Goods Sold - _TC", "expense_account": "Cost of Goods Sold - _TC",
"cost_center": "_Test Cost Center - _TC", "cost_center": "_Test Cost Center - _TC",
"location": "", "location": ""
}, })
)
si.save() si.save()
self.assertRaises(frappe.ValidationError, si.submit) self.assertRaises(frappe.ValidationError, si.submit)
@@ -79,39 +72,31 @@ class TestAccountingDimension(unittest.TestCase):
def tearDown(self): def tearDown(self):
disable_dimension() disable_dimension()
def create_dimension(): def create_dimension():
frappe.set_user("Administrator") frappe.set_user("Administrator")
if not frappe.db.exists("Accounting Dimension", {"document_type": "Department"}): if not frappe.db.exists("Accounting Dimension", {"document_type": "Department"}):
frappe.get_doc( frappe.get_doc({
{
"doctype": "Accounting Dimension", "doctype": "Accounting Dimension",
"document_type": "Department", "document_type": "Department",
} }).insert()
).insert()
else: else:
dimension = frappe.get_doc("Accounting Dimension", "Department") dimension = frappe.get_doc("Accounting Dimension", "Department")
dimension.disabled = 0 dimension.disabled = 0
dimension.save() dimension.save()
if not frappe.db.exists("Accounting Dimension", {"document_type": "Location"}): if not frappe.db.exists("Accounting Dimension", {"document_type": "Location"}):
dimension1 = frappe.get_doc( dimension1 = frappe.get_doc({
{
"doctype": "Accounting Dimension", "doctype": "Accounting Dimension",
"document_type": "Location", "document_type": "Location",
} })
)
dimension1.append( dimension1.append("dimension_defaults", {
"dimension_defaults",
{
"company": "_Test Company", "company": "_Test Company",
"reference_document": "Location", "reference_document": "Location",
"default_dimension": "Block 1", "default_dimension": "Block 1",
"mandatory_for_bs": 1, "mandatory_for_bs": 1
}, })
)
dimension1.insert() dimension1.insert()
dimension1.save() dimension1.save()
@@ -120,7 +105,6 @@ def create_dimension():
dimension1.disabled = 0 dimension1.disabled = 0
dimension1.save() dimension1.save()
def disable_dimension(): def disable_dimension():
dimension1 = frappe.get_doc("Accounting Dimension", "Department") dimension1 = frappe.get_doc("Accounting Dimension", "Department")
dimension1.disabled = 1 dimension1.disabled = 1

View File

@@ -3,6 +3,10 @@
frappe.ui.form.on('Accounting Dimension Filter', { frappe.ui.form.on('Accounting Dimension Filter', {
refresh: function(frm, cdt, cdn) { refresh: function(frm, cdt, cdn) {
if (frm.doc.accounting_dimension) {
frm.set_df_property('dimensions', 'label', frm.doc.accounting_dimension, cdn, 'dimension_value');
}
let help_content = let help_content =
`<table class="table table-bordered" style="background-color: var(--scrollbar-track-color);"> `<table class="table table-bordered" style="background-color: var(--scrollbar-track-color);">
<tr><td> <tr><td>
@@ -64,7 +68,6 @@ frappe.ui.form.on('Accounting Dimension Filter', {
frm.clear_table("dimensions"); frm.clear_table("dimensions");
let row = frm.add_child("dimensions"); let row = frm.add_child("dimensions");
row.accounting_dimension = frm.doc.accounting_dimension; row.accounting_dimension = frm.doc.accounting_dimension;
frm.fields_dict["dimensions"].grid.update_docfield_property("dimension_value", "label", frm.doc.accounting_dimension);
frm.refresh_field("dimensions"); frm.refresh_field("dimensions");
frm.trigger('setup_filters'); frm.trigger('setup_filters');
}, },

View File

@@ -19,27 +19,17 @@ class AccountingDimensionFilter(Document):
WHERE d.name = a.parent WHERE d.name = a.parent
and d.name != %s and d.name != %s
and d.accounting_dimension = %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] 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: if account.applicable_on_account in account_list:
frappe.throw( frappe.throw(_("Row {0}: {1} account already applied for Accounting Dimension {2}").format(
_("Row {0}: {1} account already applied for Accounting Dimension {2}").format( account.idx, frappe.bold(account.applicable_on_account), frappe.bold(self.accounting_dimension)))
account.idx,
frappe.bold(account.applicable_on_account),
frappe.bold(self.accounting_dimension),
)
)
def get_dimension_filter_map(): def get_dimension_filter_map():
filters = frappe.db.sql( filters = frappe.db.sql("""
"""
SELECT SELECT
a.applicable_on_account, d.dimension_value, p.accounting_dimension, a.applicable_on_account, d.dimension_value, p.accounting_dimension,
p.allow_or_restrict, a.is_mandatory p.allow_or_restrict, a.is_mandatory
@@ -50,30 +40,22 @@ def get_dimension_filter_map():
p.name = a.parent p.name = a.parent
AND p.disabled = 0 AND p.disabled = 0
AND p.name = d.parent AND p.name = d.parent
""", """, as_dict=1)
as_dict=1,
)
dimension_filter_map = {} dimension_filter_map = {}
for f in filters: for f in filters:
f.fieldname = scrub(f.accounting_dimension) f.fieldname = scrub(f.accounting_dimension)
build_map( build_map(dimension_filter_map, f.fieldname, f.applicable_on_account, f.dimension_value,
dimension_filter_map, f.allow_or_restrict, f.is_mandatory)
f.fieldname,
f.applicable_on_account,
f.dimension_value,
f.allow_or_restrict,
f.is_mandatory,
)
return dimension_filter_map return dimension_filter_map
def build_map(map_object, dimension, account, filter_value, allow_or_restrict, is_mandatory): def build_map(map_object, dimension, account, filter_value, allow_or_restrict, is_mandatory):
map_object.setdefault( map_object.setdefault((dimension, account), {
(dimension, account), 'allowed_dimensions': [],
{"allowed_dimensions": [], "is_mandatory": is_mandatory, "allow_or_restrict": allow_or_restrict}, 'is_mandatory': is_mandatory,
) 'allow_or_restrict': allow_or_restrict
map_object[(dimension, account)]["allowed_dimensions"].append(filter_value) })
map_object[(dimension, account)]['allowed_dimensions'].append(filter_value)

View File

@@ -12,8 +12,7 @@ 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.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.exceptions import InvalidAccountDimensionError, MandatoryAccountDimensionError from erpnext.exceptions import InvalidAccountDimensionError, MandatoryAccountDimensionError
test_dependencies = ["Location", "Cost Center", "Department"] test_dependencies = ['Location', 'Cost Center', 'Department']
class TestAccountingDimensionFilter(unittest.TestCase): class TestAccountingDimensionFilter(unittest.TestCase):
def setUp(self): def setUp(self):
@@ -23,9 +22,9 @@ class TestAccountingDimensionFilter(unittest.TestCase):
def test_allowed_dimension_validation(self): def test_allowed_dimension_validation(self):
si = create_sales_invoice(do_not_save=1) si = create_sales_invoice(do_not_save=1)
si.items[0].cost_center = "Main - _TC" si.items[0].cost_center = 'Main - _TC'
si.department = "Accounts - _TC" si.department = 'Accounts - _TC'
si.location = "Block 1" si.location = 'Block 1'
si.save() si.save()
self.assertRaises(InvalidAccountDimensionError, si.submit) self.assertRaises(InvalidAccountDimensionError, si.submit)
@@ -33,12 +32,12 @@ class TestAccountingDimensionFilter(unittest.TestCase):
def test_mandatory_dimension_validation(self): def test_mandatory_dimension_validation(self):
si = create_sales_invoice(do_not_save=1) si = create_sales_invoice(do_not_save=1)
si.department = "" si.department = ''
si.location = "Block 1" si.location = 'Block 1'
# Test with no department for Sales Account # Test with no department for Sales Account
si.items[0].department = "" si.items[0].department = ''
si.items[0].cost_center = "_Test Cost Center 2 - _TC" si.items[0].cost_center = '_Test Cost Center 2 - _TC'
si.save() si.save()
self.assertRaises(MandatoryAccountDimensionError, si.submit) self.assertRaises(MandatoryAccountDimensionError, si.submit)
@@ -53,54 +52,53 @@ class TestAccountingDimensionFilter(unittest.TestCase):
if si.docstatus == 1: if si.docstatus == 1:
si.cancel() si.cancel()
def create_accounting_dimension_filter(): def create_accounting_dimension_filter():
if not frappe.db.get_value( if not frappe.db.get_value('Accounting Dimension Filter',
"Accounting Dimension Filter", {"accounting_dimension": "Cost Center"} {'accounting_dimension': 'Cost Center'}):
): frappe.get_doc({
frappe.get_doc( 'doctype': 'Accounting Dimension Filter',
{ 'accounting_dimension': 'Cost Center',
"doctype": "Accounting Dimension Filter", 'allow_or_restrict': 'Allow',
"accounting_dimension": "Cost Center", 'company': '_Test Company',
"allow_or_restrict": "Allow", 'accounts': [{
"company": "_Test Company", 'applicable_on_account': 'Sales - _TC',
"accounts": [ }],
{ 'dimensions': [{
"applicable_on_account": "Sales - _TC", 'accounting_dimension': 'Cost Center',
} 'dimension_value': '_Test Cost Center 2 - _TC'
], }]
"dimensions": [ }).insert()
{"accounting_dimension": "Cost Center", "dimension_value": "_Test Cost Center 2 - _TC"}
],
}
).insert()
else: 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.disabled = 0
doc.save() doc.save()
if not frappe.db.get_value("Accounting Dimension Filter", {"accounting_dimension": "Department"}): if not frappe.db.get_value('Accounting Dimension Filter',
frappe.get_doc( {'accounting_dimension': 'Department'}):
{ frappe.get_doc({
"doctype": "Accounting Dimension Filter", 'doctype': 'Accounting Dimension Filter',
"accounting_dimension": "Department", 'accounting_dimension': 'Department',
"allow_or_restrict": "Allow", 'allow_or_restrict': 'Allow',
"company": "_Test Company", 'company': '_Test Company',
"accounts": [{"applicable_on_account": "Sales - _TC", "is_mandatory": 1}], 'accounts': [{
"dimensions": [{"accounting_dimension": "Department", "dimension_value": "Accounts - _TC"}], 'applicable_on_account': 'Sales - _TC',
} 'is_mandatory': 1
).insert() }],
'dimensions': [{
'accounting_dimension': 'Department',
'dimension_value': 'Accounts - _TC'
}]
}).insert()
else: 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.disabled = 0
doc.save() doc.save()
def disable_dimension_filter(): 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.disabled = 1
doc.save() 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.disabled = 1
doc.save() doc.save()

View File

@@ -7,9 +7,7 @@ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
class OverlapError(frappe.ValidationError): class OverlapError(frappe.ValidationError): pass
pass
class AccountingPeriod(Document): class AccountingPeriod(Document):
def validate(self): def validate(self):
@@ -19,12 +17,11 @@ class AccountingPeriod(Document):
self.bootstrap_doctypes_for_closing() self.bootstrap_doctypes_for_closing()
def autoname(self): 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]) self.name = " - ".join([self.period_name, company_abbr])
def validate_overlap(self): def validate_overlap(self):
existing_accounting_period = frappe.db.sql( existing_accounting_period = frappe.db.sql("""select name from `tabAccounting Period`
"""select name from `tabAccounting Period`
where ( where (
(%(start_date)s between start_date and end_date) (%(start_date)s between start_date and end_date)
or (%(end_date)s between start_date and end_date) or (%(end_date)s between start_date and end_date)
@@ -35,23 +32,18 @@ class AccountingPeriod(Document):
"start_date": self.start_date, "start_date": self.start_date,
"end_date": self.end_date, "end_date": self.end_date,
"name": self.name, "name": self.name,
"company": self.company, "company": self.company
}, }, as_dict=True)
as_dict=True,
)
if len(existing_accounting_period) > 0: if len(existing_accounting_period) > 0:
frappe.throw( frappe.throw(_("Accounting Period overlaps with {0}")
_("Accounting Period overlaps with {0}").format(existing_accounting_period[0].get("name")), .format(existing_accounting_period[0].get("name")), OverlapError)
OverlapError,
)
@frappe.whitelist() @frappe.whitelist()
def get_doctypes_for_closing(self): def get_doctypes_for_closing(self):
docs_for_closing = [] docs_for_closing = []
# get period closing doctypes from all the apps doctypes = ["Sales Invoice", "Purchase Invoice", "Journal Entry", "Payroll Entry", \
doctypes = frappe.get_hooks("period_closing_doctypes") "Bank Clearance", "Asset", "Stock Entry"]
closed_doctypes = [{"document_type": doctype, "closed": 1} for doctype in doctypes] closed_doctypes = [{"document_type": doctype, "closed": 1} for doctype in doctypes]
for closed_doctype in closed_doctypes: for closed_doctype in closed_doctypes:
docs_for_closing.append(closed_doctype) docs_for_closing.append(closed_doctype)
@@ -61,7 +53,7 @@ class AccountingPeriod(Document):
def bootstrap_doctypes_for_closing(self): def bootstrap_doctypes_for_closing(self):
if len(self.closed_documents) == 0: if len(self.closed_documents) == 0:
for doctype_for_closing in self.get_doctypes_for_closing(): for doctype_for_closing in self.get_doctypes_for_closing():
self.append( self.append('closed_documents', {
"closed_documents", "document_type": doctype_for_closing.document_type,
{"document_type": doctype_for_closing.document_type, "closed": doctype_for_closing.closed}, "closed": doctype_for_closing.closed
) })

View File

@@ -10,38 +10,29 @@ 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.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.general_ledger import ClosedAccountingPeriod from erpnext.accounts.general_ledger import ClosedAccountingPeriod
test_dependencies = ["Item"] test_dependencies = ['Item']
class TestAccountingPeriod(unittest.TestCase): class TestAccountingPeriod(unittest.TestCase):
def test_overlap(self): def test_overlap(self):
ap1 = create_accounting_period( ap1 = create_accounting_period(start_date = "2018-04-01",
start_date="2018-04-01", end_date="2018-06-30", company="Wind Power LLC" end_date = "2018-06-30", company = "Wind Power LLC")
)
ap1.save() ap1.save()
ap2 = create_accounting_period( ap2 = create_accounting_period(start_date = "2018-06-30",
start_date="2018-06-30", end_date = "2018-07-10", company = "Wind Power LLC", period_name = "Test Accounting Period 1")
end_date="2018-07-10",
company="Wind Power LLC",
period_name="Test Accounting Period 1",
)
self.assertRaises(OverlapError, ap2.save) self.assertRaises(OverlapError, ap2.save)
def test_accounting_period(self): 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() ap1.save()
doc = create_sales_invoice( doc = create_sales_invoice(do_not_submit=1, cost_center="_Test Company - _TC", warehouse="Stores - _TC")
do_not_submit=1, cost_center="_Test Company - _TC", warehouse="Stores - _TC"
)
self.assertRaises(ClosedAccountingPeriod, doc.submit) self.assertRaises(ClosedAccountingPeriod, doc.submit)
def tearDown(self): def tearDown(self):
for d in frappe.get_all("Accounting Period"): for d in frappe.get_all("Accounting Period"):
frappe.delete_doc("Accounting Period", d.name) frappe.delete_doc("Accounting Period", d.name)
def create_accounting_period(**args): def create_accounting_period(**args):
args = frappe._dict(args) args = frappe._dict(args)
@@ -50,6 +41,8 @@ def create_accounting_period(**args):
accounting_period.end_date = args.end_date or add_months(nowdate(), 1) accounting_period.end_date = args.end_date or add_months(nowdate(), 1)
accounting_period.company = args.company or "_Test Company" accounting_period.company = args.company or "_Test Company"
accounting_period.period_name = args.period_name or "_Test_Period_Name_1" 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 return accounting_period

View File

@@ -7,30 +7,35 @@
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"invoice_and_billing_tab", "accounts_transactions_settings_section",
"enable_features_section", "over_billing_allowance",
"unlink_payment_on_cancellation_of_invoice", "role_allowed_to_over_bill",
"unlink_advance_payment_on_cancelation_of_order", "credit_controller",
"column_break_13", "make_payment_via_journal_entry",
"delete_linked_ledger_entries", "column_break_11",
"invoicing_features_section",
"check_supplier_invoice_uniqueness", "check_supplier_invoice_uniqueness",
"unlink_payment_on_cancellation_of_invoice",
"automatically_fetch_payment_terms", "automatically_fetch_payment_terms",
"column_break_17", "delete_linked_ledger_entries",
"book_asset_depreciation_entry_automatically",
"unlink_advance_payment_on_cancelation_of_order",
"enable_common_party_accounting", "enable_common_party_accounting",
"allow_multi_currency_invoices_against_single_party_account", "post_change_gl_entries",
"report_setting_section", "enable_discount_accounting",
"use_custom_cash_flow", "tax_settings_section",
"determine_address_tax_category_from",
"column_break_19",
"add_taxes_from_item_tax_template",
"period_closing_settings_section",
"acc_frozen_upto",
"frozen_accounts_modifier",
"column_break_4",
"deferred_accounting_settings_section", "deferred_accounting_settings_section",
"book_deferred_entries_based_on", "book_deferred_entries_based_on",
"column_break_18", "column_break_18",
"automatically_process_deferred_accounting_entry", "automatically_process_deferred_accounting_entry",
"book_deferred_entries_via_journal_entry", "book_deferred_entries_via_journal_entry",
"submit_journal_entries", "submit_journal_entries",
"tax_settings_section",
"determine_address_tax_category_from",
"column_break_19",
"add_taxes_from_item_tax_template",
"print_settings", "print_settings",
"show_inclusive_tax_in_print", "show_inclusive_tax_in_print",
"column_break_12", "column_break_12",
@@ -38,25 +43,8 @@
"currency_exchange_section", "currency_exchange_section",
"allow_stale", "allow_stale",
"stale_days", "stale_days",
"invoicing_settings_tab", "report_settings_sb",
"accounts_transactions_settings_section", "use_custom_cash_flow"
"over_billing_allowance",
"column_break_11",
"role_allowed_to_over_bill",
"credit_controller",
"make_payment_via_journal_entry",
"pos_tab",
"pos_setting_section",
"post_change_gl_entries",
"assets_tab",
"asset_settings_section",
"book_asset_depreciation_entry_automatically",
"closing_settings_tab",
"period_closing_settings_section",
"acc_frozen_upto",
"column_break_25",
"frozen_accounts_modifier",
"report_settings_sb"
], ],
"fields": [ "fields": [
{ {
@@ -82,6 +70,10 @@
"label": "Determine Address Tax Category From", "label": "Determine Address Tax Category From",
"options": "Billing Address\nShipping Address" "options": "Billing Address\nShipping Address"
}, },
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{ {
"fieldname": "credit_controller", "fieldname": "credit_controller",
"fieldtype": "Link", "fieldtype": "Link",
@@ -91,7 +83,6 @@
}, },
{ {
"default": "0", "default": "0",
"description": "Enabling ensure each Purchase Invoice has a unique value in Supplier Invoice No. field",
"fieldname": "check_supplier_invoice_uniqueness", "fieldname": "check_supplier_invoice_uniqueness",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Check Supplier Invoice Number Uniqueness" "label": "Check Supplier Invoice Number Uniqueness"
@@ -177,7 +168,7 @@
"description": "Only select this if you have set up the Cash Flow Mapper documents", "description": "Only select this if you have set up the Cash Flow Mapper documents",
"fieldname": "use_custom_cash_flow", "fieldname": "use_custom_cash_flow",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Enable Custom Cash Flow Format" "label": "Use Custom Cash Flow Format"
}, },
{ {
"default": "0", "default": "0",
@@ -250,7 +241,7 @@
{ {
"fieldname": "accounts_transactions_settings_section", "fieldname": "accounts_transactions_settings_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Credit Limit Settings" "label": "Transactions Settings"
}, },
{ {
"fieldname": "column_break_11", "fieldname": "column_break_11",
@@ -274,79 +265,16 @@
}, },
{ {
"default": "0", "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>", "description": "If enabled, additional ledger entries will be made for discounts in a separate Discount Account",
"fieldname": "enable_common_party_accounting", "fieldname": "enable_discount_accounting",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Enable Common Party Accounting" "label": "Enable Discount Accounting"
},
{
"fieldname": "enable_features_section",
"fieldtype": "Section Break",
"label": "Invoice Cancellation"
},
{
"fieldname": "column_break_13",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_25",
"fieldtype": "Column Break"
},
{
"fieldname": "asset_settings_section",
"fieldtype": "Section Break",
"label": "Asset Settings"
},
{
"fieldname": "invoicing_settings_tab",
"fieldtype": "Tab Break",
"label": "Credit Limits"
},
{
"fieldname": "assets_tab",
"fieldtype": "Tab Break",
"label": "Assets"
},
{
"fieldname": "closing_settings_tab",
"fieldtype": "Tab Break",
"label": "Accounts Closing"
},
{
"fieldname": "pos_setting_section",
"fieldtype": "Section Break",
"label": "POS Setting"
},
{
"fieldname": "invoice_and_billing_tab",
"fieldtype": "Tab Break",
"label": "Invoice and Billing"
},
{
"fieldname": "invoicing_features_section",
"fieldtype": "Section Break",
"label": "Invoicing Features"
},
{
"fieldname": "column_break_17",
"fieldtype": "Column Break"
},
{
"fieldname": "pos_tab",
"fieldtype": "Tab Break",
"label": "POS"
},
{
"fieldname": "report_setting_section",
"fieldtype": "Section Break",
"label": "Report Setting"
}, },
{ {
"default": "0", "default": "0",
"description": "Enabling this will allow creation of multi-currency invoices against single party account in company currency", "fieldname": "enable_common_party_accounting",
"fieldname": "allow_multi_currency_invoices_against_single_party_account",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Allow multi-currency invoices against single party account " "label": "Enable Common Party Accounting"
} }
], ],
"icon": "icon-cog", "icon": "icon-cog",
@@ -354,7 +282,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2022-11-27 21:49:52.538655", "modified": "2021-10-11 17:42:36.427699",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Accounts Settings", "name": "Accounts Settings",
@@ -381,6 +309,5 @@
"quick_entry": 1, "quick_entry": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "ASC", "sort_order": "ASC",
"states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@@ -18,38 +18,48 @@ class AccountsSettings(Document):
frappe.clear_cache() frappe.clear_cache()
def validate(self): def validate(self):
frappe.db.set_default( frappe.db.set_default("add_taxes_from_item_tax_template",
"add_taxes_from_item_tax_template", self.get("add_taxes_from_item_tax_template", 0) self.get("add_taxes_from_item_tax_template", 0))
)
frappe.db.set_default( frappe.db.set_default("enable_common_party_accounting",
"enable_common_party_accounting", self.get("enable_common_party_accounting", 0) self.get("enable_common_party_accounting", 0))
)
self.validate_stale_days() self.validate_stale_days()
self.enable_payment_schedule_in_print() self.enable_payment_schedule_in_print()
self.toggle_discount_accounting_fields()
self.validate_pending_reposts() self.validate_pending_reposts()
def validate_stale_days(self): def validate_stale_days(self):
if not self.allow_stale and cint(self.stale_days) <= 0: if not self.allow_stale and cint(self.stale_days) <= 0:
frappe.msgprint( 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): def enable_payment_schedule_in_print(self):
show_in_print = cint(self.show_payment_schedule_in_print) show_in_print = cint(self.show_payment_schedule_in_print)
for doctype in ("Sales Order", "Sales Invoice", "Purchase Order", "Purchase Invoice"): for doctype in ("Sales Order", "Sales Invoice", "Purchase Order", "Purchase Invoice"):
make_property_setter( make_property_setter(doctype, "due_date", "print_hide", show_in_print, "Check", validate_fields_for_doctype=False)
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)
)
make_property_setter( def toggle_discount_accounting_fields(self):
doctype, enable_discount_accounting = cint(self.enable_discount_accounting)
"payment_schedule",
"print_hide", for doctype in ["Sales Invoice Item", "Purchase Invoice Item"]:
0 if show_in_print else 1, make_property_setter(doctype, "discount_account", "hidden", not(enable_discount_accounting), "Check", validate_fields_for_doctype=False)
"Check", if enable_discount_accounting:
validate_fields_for_doctype=False, 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)
def validate_pending_reposts(self): def validate_pending_reposts(self):
if self.acc_frozen_upto: if self.acc_frozen_upto:

View File

@@ -7,12 +7,12 @@ class TestAccountsSettings(unittest.TestCase):
def tearDown(self): def tearDown(self):
# Just in case `save` method succeeds, we need to take things back to default so that other tests # Just in case `save` method succeeds, we need to take things back to default so that other tests
# don't break # 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.allow_stale = 1
cur_settings.save() cur_settings.save()
def test_stale_days(self): 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.allow_stale = 0
cur_settings.stale_days = 0 cur_settings.stale_days = 0

View File

@@ -15,4 +15,4 @@ class Bank(Document):
load_address_and_contact(self) load_address_and_contact(self)
def on_trash(self): def on_trash(self):
delete_contact_and_address("Bank", self.name) delete_contact_and_address('Bank', self.name)

View File

@@ -3,6 +3,11 @@ from frappe import _
def get_data(): def get_data():
return { return {
"fieldname": "bank", 'fieldname': 'bank',
"transactions": [{"label": _("Bank Details"), "items": ["Bank Account", "Bank Guarantee"]}], 'transactions': [
{
'label': _('Bank Details'),
'items': ['Bank Account', 'Bank Guarantee']
}
]
} }

View File

@@ -27,6 +27,7 @@
"bank_account_no", "bank_account_no",
"address_and_contact", "address_and_contact",
"address_html", "address_html",
"website",
"column_break_13", "column_break_13",
"contact_html", "contact_html",
"integration_details_section", "integration_details_section",
@@ -155,6 +156,11 @@
"fieldtype": "HTML", "fieldtype": "HTML",
"label": "Address HTML" "label": "Address HTML"
}, },
{
"fieldname": "website",
"fieldtype": "Data",
"label": "Website"
},
{ {
"fieldname": "column_break_13", "fieldname": "column_break_13",
"fieldtype": "Column Break" "fieldtype": "Column Break"
@@ -202,7 +208,7 @@
} }
], ],
"links": [], "links": [],
"modified": "2022-05-04 15:49:42.620630", "modified": "2020-10-23 16:48:06.303658",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Bank Account", "name": "Bank Account",
@@ -237,6 +243,5 @@
"search_fields": "bank,account", "search_fields": "bank,account",
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@@ -20,7 +20,7 @@ class BankAccount(Document):
self.name = self.account_name + " - " + self.bank self.name = self.account_name + " - " + self.bank
def on_trash(self): def on_trash(self):
delete_contact_and_address("BankAccount", self.name) delete_contact_and_address('BankAccount', self.name)
def validate(self): def validate(self):
self.validate_company() self.validate_company()
@@ -31,9 +31,9 @@ class BankAccount(Document):
frappe.throw(_("Company is manadatory for company account")) frappe.throw(_("Company is manadatory for company account"))
def validate_iban(self): def validate_iban(self):
""" '''
Algorithm: https://en.wikipedia.org/wiki/International_Bank_Account_Number#Validating_the_IBAN Algorithm: https://en.wikipedia.org/wiki/International_Bank_Account_Number#Validating_the_IBAN
""" '''
# IBAN field is optional # IBAN field is optional
if not self.iban: if not self.iban:
return return
@@ -43,7 +43,7 @@ class BankAccount(Document):
return str(9 + ord(c) - 64) return str(9 + ord(c) - 64)
# remove whitespaces, upper case to get the right number from ord() # 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 # Move country code and checksum from the start to the end
flipped = iban[4:] + iban[:4] 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] encoded = [encode_char(c) if ord(c) >= 65 and ord(c) <= 90 else c for c in flipped]
try: try:
to_check = int("".join(encoded)) to_check = int(''.join(encoded))
except ValueError: except ValueError:
frappe.throw(_("IBAN is not valid")) frappe.throw(_('IBAN is not valid'))
if to_check % 97 != 1: if to_check % 97 != 1:
frappe.throw(_("IBAN is not valid")) frappe.throw(_('IBAN is not valid'))
@frappe.whitelist() @frappe.whitelist()
@@ -69,14 +69,12 @@ def make_bank_account(doctype, docname):
return doc return doc
@frappe.whitelist() @frappe.whitelist()
def get_party_bank_account(party_type, party): 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() @frappe.whitelist()
def get_bank_account_details(bank_account): def get_bank_account_details(bank_account):
return frappe.get_cached_value( return frappe.db.get_value("Bank Account",
"Bank Account", bank_account, ["account", "bank", "bank_account_no"], as_dict=1 bank_account, ['account', 'bank', 'bank_account_no'], as_dict=1)
)

View File

@@ -3,18 +3,25 @@ from frappe import _
def get_data(): def get_data():
return { return {
"fieldname": "bank_account", 'fieldname': 'bank_account',
"non_standard_fieldnames": { 'non_standard_fieldnames': {
"Customer": "default_bank_account", 'Customer': 'default_bank_account',
"Supplier": "default_bank_account", 'Supplier': 'default_bank_account',
}, },
"transactions": [ 'transactions': [
{ {
"label": _("Payments"), 'label': _('Payments'),
"items": ["Payment Entry", "Payment Request", "Payment Order", "Payroll Entry"], 'items': ['Payment Entry', 'Payment Request', 'Payment Order', 'Payroll Entry']
}, },
{"label": _("Party"), "items": ["Customer", "Supplier"]}, {
{"items": ["Bank Guarantee"]}, 'label': _('Party'),
{"items": ["Journal Entry"]}, 'items': ['Customer', 'Supplier']
], },
{
'items': ['Bank Guarantee']
},
{
'items': ['Journal Entry']
}
]
} }

View File

@@ -8,28 +8,28 @@ from frappe import ValidationError
# test_records = frappe.get_test_records('Bank Account') # test_records = frappe.get_test_records('Bank Account')
class TestBankAccount(unittest.TestCase): class TestBankAccount(unittest.TestCase):
def test_validate_iban(self): def test_validate_iban(self):
valid_ibans = [ valid_ibans = [
"GB82 WEST 1234 5698 7654 32", 'GB82 WEST 1234 5698 7654 32',
"DE91 1000 0000 0123 4567 89", 'DE91 1000 0000 0123 4567 89',
"FR76 3000 6000 0112 3456 7890 189", 'FR76 3000 6000 0112 3456 7890 189'
] ]
invalid_ibans = [ invalid_ibans = [
# wrong checksum (3rd place) # wrong checksum (3rd place)
"GB72 WEST 1234 5698 7654 32", 'GB72 WEST 1234 5698 7654 32',
"DE81 1000 0000 0123 4567 89", 'DE81 1000 0000 0123 4567 89',
"FR66 3000 6000 0112 3456 7890 189", 'FR66 3000 6000 0112 3456 7890 189'
] ]
bank_account = frappe.get_doc({"doctype": "Bank Account"}) bank_account = frappe.get_doc({'doctype':'Bank Account'})
try: try:
bank_account.validate_iban() bank_account.validate_iban()
except AttributeError: except AttributeError:
msg = "BankAccount.validate_iban() failed for empty IBAN" msg = 'BankAccount.validate_iban() failed for empty IBAN'
self.fail(msg=msg) self.fail(msg=msg)
for iban in valid_ibans: for iban in valid_ibans:
@@ -37,11 +37,11 @@ class TestBankAccount(unittest.TestCase):
try: try:
bank_account.validate_iban() bank_account.validate_iban()
except ValidationError: 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) self.fail(msg=msg)
for not_iban in invalid_ibans: for not_iban in invalid_ibans:
bank_account.iban = not_iban 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): with self.assertRaises(ValidationError, msg=msg):
bank_account.validate_iban() bank_account.validate_iban()

View File

@@ -4,23 +4,6 @@
frappe.ui.form.on("Bank Clearance", { frappe.ui.form.on("Bank Clearance", {
setup: function(frm) { setup: function(frm) {
frm.add_fetch("account", "account_currency", "account_currency"); frm.add_fetch("account", "account_currency", "account_currency");
frm.set_query("account", function() {
return {
"filters": {
"account_type": ["in",["Bank","Cash"]],
"is_group": 0,
}
};
});
frm.set_query("bank_account", function () {
return {
filters: {
'is_company_account': 1
},
};
});
}, },
onload: function(frm) { onload: function(frm) {
@@ -29,7 +12,14 @@ frappe.ui.form.on("Bank Clearance", {
locals[":Company"][frappe.defaults.get_user_default("Company")]["default_bank_account"]: ""; locals[":Company"][frappe.defaults.get_user_default("Company")]["default_bank_account"]: "";
frm.set_value("account", default_bank_account); frm.set_value("account", default_bank_account);
frm.set_query("account", function() {
return {
"filters": {
"account_type": ["in",["Bank","Cash"]],
"is_group": 0
}
};
});
frm.set_value("from_date", frappe.datetime.month_start()); frm.set_value("from_date", frappe.datetime.month_start());
frm.set_value("to_date", frappe.datetime.month_end()); frm.set_value("to_date", frappe.datetime.month_end());
@@ -37,14 +27,6 @@ frappe.ui.form.on("Bank Clearance", {
refresh: function(frm) { refresh: function(frm) {
frm.disable_save(); frm.disable_save();
if (frm.doc.account && frm.doc.from_date && frm.doc.to_date) {
frm.add_custom_button(__('Get Payment Entries'), () =>
frm.trigger("get_payment_entries")
);
frm.change_custom_button_type('Get Payment Entries', null, 'primary');
}
}, },
update_clearance_date: function(frm) { update_clearance_date: function(frm) {
@@ -54,30 +36,22 @@ frappe.ui.form.on("Bank Clearance", {
callback: function(r, rt) { callback: function(r, rt) {
frm.refresh_field("payment_entries"); frm.refresh_field("payment_entries");
frm.refresh_fields(); frm.refresh_fields();
if (!frm.doc.payment_entries.length) {
frm.change_custom_button_type('Get Payment Entries', null, 'primary');
frm.change_custom_button_type('Update Clearance Date', null, 'default');
}
} }
}); });
}, },
get_payment_entries: function(frm) { get_payment_entries: function(frm) {
return frappe.call({ return frappe.call({
method: "get_payment_entries", method: "get_payment_entries",
doc: frm.doc, doc: frm.doc,
callback: function(r, rt) { callback: function(r, rt) {
frm.refresh_field("payment_entries"); frm.refresh_field("payment_entries");
frm.refresh_fields();
if (frm.doc.payment_entries.length) { $(frm.fields_dict.payment_entries.wrapper).find("[data-fieldname=amount]").each(function(i,v){
frm.add_custom_button(__('Update Clearance Date'), () => if (i !=0){
frm.trigger("update_clearance_date") $(v).addClass("text-right")
);
frm.change_custom_button_type('Get Payment Entries', null, 'default');
frm.change_custom_button_type('Update Clearance Date', null, 'primary');
} }
})
} }
}); });
} }

View File

@@ -1,5 +1,4 @@
{ {
"actions": [],
"allow_copy": 1, "allow_copy": 1,
"creation": "2013-01-10 16:34:05", "creation": "2013-01-10 16:34:05",
"doctype": "DocType", "doctype": "DocType",
@@ -14,8 +13,11 @@
"bank_account", "bank_account",
"include_reconciled_entries", "include_reconciled_entries",
"include_pos_transactions", "include_pos_transactions",
"get_payment_entries",
"section_break_10", "section_break_10",
"payment_entries" "payment_entries",
"update_clearance_date",
"total_amount"
], ],
"fields": [ "fields": [
{ {
@@ -74,6 +76,11 @@
"fieldtype": "Check", "fieldtype": "Check",
"label": "Include POS Transactions" "label": "Include POS Transactions"
}, },
{
"fieldname": "get_payment_entries",
"fieldtype": "Button",
"label": "Get Payment Entries"
},
{ {
"fieldname": "section_break_10", "fieldname": "section_break_10",
"fieldtype": "Section Break" "fieldtype": "Section Break"
@@ -84,14 +91,25 @@
"fieldtype": "Table", "fieldtype": "Table",
"label": "Payment Entries", "label": "Payment Entries",
"options": "Bank Clearance Detail" "options": "Bank Clearance Detail"
},
{
"fieldname": "update_clearance_date",
"fieldtype": "Button",
"label": "Update Clearance Date"
},
{
"fieldname": "total_amount",
"fieldtype": "Currency",
"label": "Total Amount",
"options": "account_currency",
"read_only": 1
} }
], ],
"hide_toolbar": 1, "hide_toolbar": 1,
"icon": "fa fa-check", "icon": "fa fa-check",
"idx": 1, "idx": 1,
"issingle": 1, "issingle": 1,
"links": [], "modified": "2020-04-06 16:12:06.628008",
"modified": "2022-11-28 17:24:13.008692",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Bank Clearance", "name": "Bank Clearance",
@@ -108,6 +126,5 @@
"quick_entry": 1, "quick_entry": 1,
"read_only": 1, "read_only": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "ASC", "sort_order": "ASC"
"states": []
} }

View File

@@ -5,13 +5,11 @@
import frappe import frappe
from frappe import _, msgprint from frappe import _, msgprint
from frappe.model.document import Document from frappe.model.document import Document
from frappe.query_builder.custom import ConstantColumn from frappe.utils import flt, fmt_money, getdate, nowdate
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): class BankClearance(Document):
@frappe.whitelist() @frappe.whitelist()
@@ -26,8 +24,7 @@ class BankClearance(Document):
if not self.include_reconciled_entries: if not self.include_reconciled_entries:
condition = "and (clearance_date IS NULL or clearance_date='0000-00-00')" condition = "and (clearance_date IS NULL or clearance_date='0000-00-00')"
journal_entries = frappe.db.sql( journal_entries = frappe.db.sql("""
"""
select select
"Journal Entry" as payment_document, t1.name as payment_entry, "Journal Entry" as payment_document, t1.name as payment_entry,
t1.cheque_no as cheque_number, t1.cheque_date, t1.cheque_no as cheque_number, t1.cheque_date,
@@ -41,18 +38,12 @@ class BankClearance(Document):
and ifnull(t1.is_opening, 'No') = 'No' {condition} and ifnull(t1.is_opening, 'No') = 'No' {condition}
group by t2.account, t1.name group by t2.account, t1.name
order by t1.posting_date ASC, t1.name DESC order by t1.posting_date ASC, t1.name DESC
""".format( """.format(condition=condition), {"account": self.account, "from": self.from_date, "to": self.to_date}, as_dict=1)
condition=condition
),
{"account": self.account, "from": self.from_date, "to": self.to_date},
as_dict=1,
)
if self.bank_account: 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 select
"Payment Entry" as payment_document, name as payment_entry, "Payment Entry" as payment_document, name as payment_entry,
reference_no as cheque_number, reference_date as cheque_date, reference_no as cheque_number, reference_date as cheque_date,
@@ -67,75 +58,12 @@ class BankClearance(Document):
{condition} {condition}
order by order by
posting_date ASC, name DESC posting_date ASC, name DESC
""".format( """.format(condition=condition), {"account": self.account, "from":self.from_date,
condition=condition "to": self.to_date, "bank_account": self.bank_account}, as_dict=1)
),
{
"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, order=frappe.qb.desc)
).run(as_dict=1)
loan_repayment = frappe.qb.DocType("Loan Repayment")
query = (
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.posting_date >= self.from_date)
.where(loan_repayment.posting_date <= self.to_date)
.where(loan_repayment.payment_account.isin([self.bank_account, self.account]))
)
if frappe.db.has_column("Loan Repayment", "repay_from_salary"):
query = query.where((loan_repayment.repay_from_salary == 0))
query = query.orderby(loan_repayment.posting_date).orderby(
loan_repayment.name, order=frappe.qb.desc
)
loan_repayments = query.run(as_dict=True)
pos_sales_invoices, pos_purchase_invoices = [], [] pos_sales_invoices, pos_purchase_invoices = [], []
if self.include_pos_transactions: if self.include_pos_transactions:
pos_sales_invoices = frappe.db.sql( pos_sales_invoices = frappe.db.sql("""
"""
select select
"Sales Invoice Payment" as payment_document, sip.name as payment_entry, sip.amount as debit, "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, si.posting_date, si.customer as against_account, sip.clearance_date,
@@ -146,13 +74,9 @@ class BankClearance(Document):
and account.name = sip.account and si.posting_date >= %(from)s and si.posting_date <= %(to)s and account.name = sip.account and si.posting_date >= %(from)s and si.posting_date <= %(to)s
order by order by
si.posting_date ASC, si.name DESC 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 select
"Purchase Invoice" as payment_document, pi.name as payment_entry, pi.paid_amount as credit, "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, pi.posting_date, pi.supplier as against_account, pi.clearance_date,
@@ -163,62 +87,46 @@ class BankClearance(Document):
and pi.posting_date >= %(from)s and pi.posting_date <= %(to)s and pi.posting_date >= %(from)s and pi.posting_date <= %(to)s
order by order by
pi.posting_date ASC, pi.name DESC 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( entries = sorted(list(payment_entries) + list(journal_entries + list(pos_sales_invoices) + list(pos_purchase_invoices)),
list(payment_entries) key=lambda k: k['posting_date'] or getdate(nowdate()))
+ 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', [])
default_currency = erpnext.get_default_currency() self.total_amount = 0.0
for d in entries: 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) formatted_amount = fmt_money(abs(amount), 2, d.account_currency)
d.amount = formatted_amount + " " + (_("Dr") if amount > 0 else _("Cr")) d.amount = formatted_amount + " " + (_("Dr") if amount > 0 else _("Cr"))
d.posting_date = getdate(d.posting_date)
d.pop("credit") d.pop("credit")
d.pop("debit") d.pop("debit")
d.pop("account_currency") d.pop("account_currency")
row.update(d) row.update(d)
self.total_amount += flt(amount)
@frappe.whitelist() @frappe.whitelist()
def update_clearance_date(self): def update_clearance_date(self):
clearance_date_updated = False clearance_date_updated = False
for d in self.get("payment_entries"): for d in self.get('payment_entries'):
if d.clearance_date: if d.clearance_date:
if not d.payment_document: if not d.payment_document:
frappe.throw(_("Row #{0}: Payment document is required to complete the transaction")) 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): if d.cheque_date and getdate(d.clearance_date) < getdate(d.cheque_date):
frappe.throw( frappe.throw(_("Row #{0}: Clearance date {1} cannot be before Cheque Date {2}")
_("Row #{0}: Clearance date {1} cannot be before Cheque Date {2}").format( .format(d.idx, d.clearance_date, d.cheque_date))
d.idx, d.clearance_date, d.cheque_date
)
)
if d.clearance_date or self.include_reconciled_entries: if d.clearance_date or self.include_reconciled_entries:
if not d.clearance_date: if not d.clearance_date:
d.clearance_date = None d.clearance_date = None
payment_entry = frappe.get_doc(d.payment_document, d.payment_entry) 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 clearance_date_updated = True

View File

@@ -1,96 +1,9 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt # See license.txt
# import frappe
import unittest 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): class TestBankClearance(unittest.TestCase):
@classmethod pass
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()

View File

@@ -43,13 +43,20 @@ frappe.ui.form.on('Bank Guarantee', {
reference_docname: function(frm) { reference_docname: function(frm) {
if (frm.doc.reference_docname && frm.doc.reference_doctype) { if (frm.doc.reference_docname && frm.doc.reference_doctype) {
let fields_to_fetch = ["grand_total"];
let party_field = frm.doc.reference_doctype == "Sales Order" ? "customer" : "supplier"; let party_field = frm.doc.reference_doctype == "Sales Order" ? "customer" : "supplier";
if (frm.doc.reference_doctype == "Sales Order") {
fields_to_fetch.push("project");
}
fields_to_fetch.push(party_field);
frappe.call({ frappe.call({
method: "erpnext.accounts.doctype.bank_guarantee.bank_guarantee.get_voucher_details", method: "erpnext.accounts.doctype.bank_guarantee.bank_guarantee.get_vouchar_detials",
args: { args: {
"bank_guarantee_type": frm.doc.bg_type, "column_list": fields_to_fetch,
"reference_name": frm.doc.reference_docname "doctype": frm.doc.reference_doctype,
"docname": frm.doc.reference_docname
}, },
callback: function(r) { callback: function(r) {
if (r.message) { if (r.message) {

View File

@@ -2,8 +2,11 @@
# For license information, please see license.txt # For license information, please see license.txt
import json
import frappe import frappe
from frappe import _ from frappe import _
from frappe.desk.search import sanitize_searchfield
from frappe.model.document import Document from frappe.model.document import Document
@@ -20,20 +23,10 @@ class BankGuarantee(Document):
if not self.bank: if not self.bank:
frappe.throw(_("Enter the name of the bank or lending institution before submittting.")) frappe.throw(_("Enter the name of the bank or lending institution before submittting."))
@frappe.whitelist() @frappe.whitelist()
def get_voucher_details(bank_guarantee_type: str, reference_name: str): def get_vouchar_detials(column_list, doctype, docname):
if not isinstance(reference_name, str): column_list = json.loads(column_list)
raise TypeError("reference_name must be a string") for col in column_list:
sanitize_searchfield(col)
fields_to_fetch = ["grand_total"] return frappe.db.sql(''' select {columns} from `tab{doctype}` where name=%s'''
.format(columns=", ".join(column_list), doctype=doctype), docname, as_dict=1)[0]
if bank_guarantee_type == "Receiving":
doctype = "Sales Order"
fields_to_fetch.append("customer")
fields_to_fetch.append("project")
else:
doctype = "Purchase Order"
fields_to_fetch.append("supplier")
return frappe.db.get_value(doctype, reference_name, fields_to_fetch, as_dict=True)

View File

@@ -12,13 +12,6 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
}, },
}; };
}); });
let no_bank_transactions_text =
`<div class="text-muted text-center">${__("No Matching Bank Transactions Found")}</div>`
set_field_options("no_bank_transactions", no_bank_transactions_text);
},
onload: function (frm) {
frm.trigger('bank_account');
}, },
refresh: function (frm) { refresh: function (frm) {
@@ -58,7 +51,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
bank_account: function (frm) { bank_account: function (frm) {
frappe.db.get_value( frappe.db.get_value(
"Bank Account", "Bank Account",
frm.doc.bank_account, frm.bank_account,
"account", "account",
(r) => { (r) => {
frappe.db.get_value( frappe.db.get_value(
@@ -67,7 +60,6 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
"account_currency", "account_currency",
(r) => { (r) => {
frm.currency = r.account_currency; frm.currency = r.account_currency;
frm.trigger("render_chart");
} }
); );
} }
@@ -132,7 +124,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
} }
}, },
render_chart: frappe.utils.debounce((frm) => { render_chart(frm) {
frm.cards_manager = new erpnext.accounts.bank_reconciliation.NumberCardManager( frm.cards_manager = new erpnext.accounts.bank_reconciliation.NumberCardManager(
{ {
$reconciliation_tool_cards: frm.get_field( $reconciliation_tool_cards: frm.get_field(
@@ -144,7 +136,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
currency: frm.currency, currency: frm.currency,
} }
); );
}, 500), },
render(frm) { render(frm) {
if (frm.doc.bank_account) { if (frm.doc.bank_account) {

View File

@@ -81,7 +81,8 @@
}, },
{ {
"fieldname": "no_bank_transactions", "fieldname": "no_bank_transactions",
"fieldtype": "HTML" "fieldtype": "HTML",
"options": "<div class=\"text-muted text-center\">No Matching Bank Transactions Found</div>"
} }
], ],
"hide_toolbar": 1, "hide_toolbar": 1,

View File

@@ -7,9 +7,9 @@ import json
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.query_builder.custom import ConstantColumn
from frappe.utils import flt from frappe.utils import flt
from erpnext import get_company_currency
from erpnext.accounts.doctype.bank_transaction.bank_transaction import get_paid_amount from erpnext.accounts.doctype.bank_transaction.bank_transaction import get_paid_amount
from erpnext.accounts.report.bank_reconciliation_statement.bank_reconciliation_statement import ( from erpnext.accounts.report.bank_reconciliation_statement.bank_reconciliation_statement import (
get_amounts_not_reflected_in_system, get_amounts_not_reflected_in_system,
@@ -21,63 +21,48 @@ from erpnext.accounts.utils import get_balance_on
class BankReconciliationTool(Document): class BankReconciliationTool(Document):
pass pass
@frappe.whitelist() @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 # returns bank transactions for a bank account
filters = [] filters = []
filters.append(["bank_account", "=", bank_account]) filters.append(['bank_account', '=', bank_account])
filters.append(["docstatus", "=", 1]) filters.append(['docstatus', '=', 1])
filters.append(["unallocated_amount", ">", 0]) filters.append(['unallocated_amount', '>', 0])
if to_date: if to_date:
filters.append(["date", "<=", to_date]) filters.append(['date', '<=', to_date])
if from_date: if from_date:
filters.append(["date", ">=", from_date]) filters.append(['date', '>=', from_date])
transactions = frappe.get_all( transactions = frappe.get_all(
"Bank Transaction", 'Bank Transaction',
fields=[ fields = ['date', 'deposit', 'withdrawal', 'currency',
"date", 'description', 'name', 'bank_account', 'company',
"deposit", 'unallocated_amount', 'reference_number', 'party_type', 'party'],
"withdrawal", filters = filters
"currency",
"description",
"name",
"bank_account",
"company",
"unallocated_amount",
"reference_number",
"party_type",
"party",
],
filters=filters,
) )
return transactions return transactions
@frappe.whitelist() @frappe.whitelist()
def get_account_balance(bank_account, till_date): def get_account_balance(bank_account, till_date):
# returns account balance till the specified date # returns account balance till the specified date
account = frappe.get_cached_value("Bank Account", bank_account, "account") account = frappe.db.get_value('Bank Account', bank_account, 'account')
filters = frappe._dict( filters = frappe._dict({
{"account": account, "report_date": till_date, "include_pos_transactions": 1} "account": account,
) "report_date": till_date,
"include_pos_transactions": 1
})
data = get_entries(filters) data = get_entries(filters)
balance_as_per_system = get_balance_on(filters["account"], filters["report_date"]) balance_as_per_system = get_balance_on(filters["account"], filters["report_date"])
total_debit, total_credit = 0, 0 total_debit, total_credit = 0,0
for d in data: for d in data:
total_debit += flt(d.debit) total_debit += flt(d.debit)
total_credit += flt(d.credit) total_credit += flt(d.credit)
amounts_not_reflected_in_system = get_amounts_not_reflected_in_system(filters) amounts_not_reflected_in_system = get_amounts_not_reflected_in_system(filters)
bank_bal = ( bank_bal = flt(balance_as_per_system) - flt(total_debit) + flt(total_credit) \
flt(balance_as_per_system)
- flt(total_debit)
+ flt(total_credit)
+ amounts_not_reflected_in_system + amounts_not_reflected_in_system
)
return bank_bal return bank_bal
@@ -90,96 +75,71 @@ def update_bank_transaction(bank_transaction_name, reference_number, party_type=
bank_transaction.party_type = party_type bank_transaction.party_type = party_type
bank_transaction.party = party bank_transaction.party = party
bank_transaction.save() bank_transaction.save()
return frappe.db.get_all( return frappe.db.get_all('Bank Transaction',
"Bank Transaction", filters={
filters={"name": bank_transaction_name}, 'name': bank_transaction_name
fields=[ },
"date", fields=['date', 'deposit', 'withdrawal', 'currency',
"deposit", 'description', 'name', 'bank_account', 'company',
"withdrawal", 'unallocated_amount', 'reference_number',
"currency", 'party_type', 'party'],
"description",
"name",
"bank_account",
"company",
"unallocated_amount",
"reference_number",
"party_type",
"party",
],
)[0] )[0]
@frappe.whitelist() @frappe.whitelist()
def create_journal_entry_bts( def create_journal_entry_bts( bank_transaction_name, reference_number=None, reference_date=None, posting_date=None, entry_type=None,
bank_transaction_name, second_account=None, mode_of_payment=None, party_type=None, party=None, allow_edit=None):
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 # Create a new journal entry based on the bank transaction
bank_transaction = frappe.db.get_values( bank_transaction = frappe.db.get_values(
"Bank Transaction", "Bank Transaction", bank_transaction_name,
bank_transaction_name, fieldname=["name", "deposit", "withdrawal", "bank_account"] ,
fieldname=["name", "deposit", "withdrawal", "bank_account"], as_dict=True
as_dict=True,
)[0] )[0]
company_account = frappe.get_cached_value( company_account = frappe.get_value("Bank Account", bank_transaction.bank_account, "account")
"Bank Account", bank_transaction.bank_account, "account" account_type = frappe.db.get_value("Account", second_account, "account_type")
)
account_type = frappe.get_cached_value("Account", second_account, "account_type")
if account_type in ["Receivable", "Payable"]: if account_type in ["Receivable", "Payable"]:
if not (party_type and party): if not (party_type and party):
frappe.throw( frappe.throw(_("Party Type and Party is required for Receivable / Payable account {0}").format( second_account))
_("Party Type and Party is required for Receivable / Payable account {0}").format(
second_account
)
)
accounts = [] accounts = []
# Multi Currency? # Multi Currency?
accounts.append( accounts.append({
{
"account": second_account, "account": second_account,
"credit_in_account_currency": bank_transaction.deposit if bank_transaction.deposit > 0 else 0, "credit_in_account_currency": bank_transaction.deposit
"debit_in_account_currency": bank_transaction.withdrawal if bank_transaction.deposit > 0
else 0,
"debit_in_account_currency":bank_transaction.withdrawal
if bank_transaction.withdrawal > 0 if bank_transaction.withdrawal > 0
else 0, else 0,
"party_type": party_type, "party_type":party_type,
"party": party, "party":party,
} })
)
accounts.append( accounts.append({
{
"account": company_account, "account": company_account,
"bank_account": bank_transaction.bank_account, "bank_account": bank_transaction.bank_account,
"credit_in_account_currency": bank_transaction.withdrawal "credit_in_account_currency": bank_transaction.withdrawal
if bank_transaction.withdrawal > 0 if bank_transaction.withdrawal > 0
else 0, else 0,
"debit_in_account_currency": bank_transaction.deposit if bank_transaction.deposit > 0 else 0, "debit_in_account_currency":bank_transaction.deposit
} if bank_transaction.deposit > 0
) else 0,
})
company = frappe.get_cached_value("Account", company_account, "company") company = frappe.get_value("Account", company_account, "company")
journal_entry_dict = { journal_entry_dict = {
"voucher_type": entry_type, "voucher_type" : entry_type,
"company": company, "company" : company,
"posting_date": posting_date, "posting_date" : posting_date,
"cheque_date": reference_date, "cheque_date" : reference_date,
"cheque_no": reference_number, "cheque_no" : reference_number,
"mode_of_payment": mode_of_payment, "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.update(journal_entry_dict)
journal_entry.set("accounts", accounts) journal_entry.set("accounts", accounts)
if allow_edit: if allow_edit:
return journal_entry return journal_entry
@@ -191,53 +151,41 @@ def create_journal_entry_bts(
else: else:
paid_amount = bank_transaction.withdrawal paid_amount = bank_transaction.withdrawal
vouchers = json.dumps( vouchers = json.dumps([{
[{"payment_doctype": "Journal Entry", "payment_name": journal_entry.name, "amount": paid_amount}] "payment_doctype":"Journal Entry",
) "payment_name":journal_entry.name,
"amount":paid_amount}])
return reconcile_vouchers(bank_transaction.name, vouchers) return reconcile_vouchers(bank_transaction.name, vouchers)
@frappe.whitelist() @frappe.whitelist()
def create_payment_entry_bts( def create_payment_entry_bts( bank_transaction_name, reference_number=None, reference_date=None, party_type=None, party=None, posting_date=None,
bank_transaction_name, mode_of_payment=None, project=None, cost_center=None, allow_edit=None):
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 # Create a new payment entry based on the bank transaction
bank_transaction = frappe.db.get_values( bank_transaction = frappe.db.get_values(
"Bank Transaction", "Bank Transaction", bank_transaction_name,
bank_transaction_name, fieldname=["name", "unallocated_amount", "deposit", "bank_account"] ,
fieldname=["name", "unallocated_amount", "deposit", "bank_account"], as_dict=True
as_dict=True,
)[0] )[0]
paid_amount = bank_transaction.unallocated_amount paid_amount = bank_transaction.unallocated_amount
payment_type = "Receive" if bank_transaction.deposit > 0 else "Pay" payment_type = "Receive" if bank_transaction.deposit > 0 else "Pay"
company_account = frappe.get_cached_value( company_account = frappe.get_value("Bank Account", bank_transaction.bank_account, "account")
"Bank Account", bank_transaction.bank_account, "account" company = frappe.get_value("Account", company_account, "company")
)
company = frappe.get_cached_value("Account", company_account, "company")
payment_entry_dict = { payment_entry_dict = {
"company": company, "company" : company,
"payment_type": payment_type, "payment_type" : payment_type,
"reference_no": reference_number, "reference_no" : reference_number,
"reference_date": reference_date, "reference_date" : reference_date,
"party_type": party_type, "party_type" : party_type,
"party": party, "party" : party,
"posting_date": posting_date, "posting_date" : posting_date,
"paid_amount": paid_amount, "paid_amount": paid_amount,
"received_amount": paid_amount, "received_amount": paid_amount
} }
payment_entry = frappe.new_doc("Payment Entry") payment_entry = frappe.new_doc("Payment Entry")
payment_entry.update(payment_entry_dict) payment_entry.update(payment_entry_dict)
if mode_of_payment: if mode_of_payment:
@@ -259,111 +207,80 @@ def create_payment_entry_bts(
payment_entry.insert() payment_entry.insert()
payment_entry.submit() payment_entry.submit()
vouchers = json.dumps( vouchers = json.dumps([{
[{"payment_doctype": "Payment Entry", "payment_name": payment_entry.name, "amount": paid_amount}] "payment_doctype":"Payment Entry",
) "payment_name":payment_entry.name,
"amount":paid_amount}])
return reconcile_vouchers(bank_transaction.name, vouchers) return reconcile_vouchers(bank_transaction.name, vouchers)
@frappe.whitelist() @frappe.whitelist()
def reconcile_vouchers(bank_transaction_name, vouchers): def reconcile_vouchers(bank_transaction_name, vouchers):
# updated clear date of all the vouchers based on the bank transaction # updated clear date of all the vouchers based on the bank transaction
vouchers = json.loads(vouchers) vouchers = json.loads(vouchers)
transaction = frappe.get_doc("Bank Transaction", bank_transaction_name) transaction = frappe.get_doc("Bank Transaction", bank_transaction_name)
company_account = frappe.get_cached_value("Bank Account", transaction.bank_account, "account") company_account = frappe.db.get_value('Bank Account', transaction.bank_account, 'account')
if transaction.unallocated_amount == 0: if transaction.unallocated_amount == 0:
frappe.throw(_("This bank transaction is already fully reconciled")) frappe.throw(_("This bank transaction is already fully reconciled"))
total_amount = 0 total_amount = 0
for voucher in vouchers: for voucher in vouchers:
voucher["payment_entry"] = frappe.get_doc(voucher["payment_doctype"], voucher["payment_name"]) voucher['payment_entry'] = frappe.get_doc(voucher['payment_doctype'], voucher['payment_name'])
total_amount += get_paid_amount( total_amount += get_paid_amount(frappe._dict({
frappe._dict( 'payment_document': voucher['payment_doctype'],
{ 'payment_entry': voucher['payment_name'],
"payment_document": voucher["payment_doctype"], }), transaction.currency, company_account)
"payment_entry": voucher["payment_name"],
}
),
transaction.currency,
company_account,
)
if total_amount > transaction.unallocated_amount: if total_amount > transaction.unallocated_amount:
frappe.throw( 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")
"The sum total of amounts of all selected vouchers should be less than the unallocated amount of the bank transaction"
)
)
account = frappe.get_cached_value("Bank Account", transaction.bank_account, "account")
for voucher in vouchers: for voucher in vouchers:
gl_entry = frappe.db.get_value( 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 Entry", gl_amount, transaction_amount = (gl_entry.credit, transaction.deposit) if gl_entry.credit > 0 else (gl_entry.debit, transaction.withdrawal)
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 allocated_amount = gl_amount if gl_amount >= transaction_amount else transaction_amount
transaction.append( transaction.append("payment_entries", {
"payment_entries", "payment_document": voucher['payment_entry'].doctype,
{ "payment_entry": voucher['payment_entry'].name,
"payment_document": voucher["payment_entry"].doctype, "allocated_amount": allocated_amount
"payment_entry": voucher["payment_entry"].name, })
"allocated_amount": allocated_amount,
},
)
transaction.save() transaction.save()
transaction.update_allocations() transaction.update_allocations()
return frappe.get_doc("Bank Transaction", bank_transaction_name) return frappe.get_doc("Bank Transaction", bank_transaction_name)
@frappe.whitelist() @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 # get all matching payments for a bank transaction
transaction = frappe.get_doc("Bank Transaction", bank_transaction_name) transaction = frappe.get_doc("Bank Transaction", bank_transaction_name)
bank_account = frappe.db.get_values( bank_account = frappe.db.get_values(
"Bank Account", transaction.bank_account, ["account", "company"], as_dict=True "Bank Account",
)[0] transaction.bank_account,
["account", "company"],
as_dict=True)[0]
(account, company) = (bank_account.account, bank_account.company) (account, company) = (bank_account.account, bank_account.company)
matching = check_matching(account, company, transaction, document_types) matching = check_matching(account, company, transaction, document_types)
return matching return matching
def check_matching(bank_account, company, transaction, document_types): def check_matching(bank_account, company, transaction, document_types):
# combine all types of vouchers # combine all types of vouchers
subquery = get_queries(bank_account, company, transaction, document_types) subquery = get_queries(bank_account, company, transaction, document_types)
filters = { filters = {
"amount": transaction.unallocated_amount, "amount": transaction.unallocated_amount,
"payment_type": "Receive" if transaction.deposit > 0 else "Pay", "payment_type" : "Receive" if transaction.deposit > 0 else "Pay",
"reference_no": transaction.reference_number, "reference_no": transaction.reference_number,
"party_type": transaction.party_type, "party_type": transaction.party_type,
"party": transaction.party, "party": transaction.party,
"bank_account": bank_account, "bank_account": bank_account
} }
matching_vouchers = [] matching_vouchers = []
matching_vouchers.extend(get_loan_vouchers(bank_account, transaction, document_types, filters))
for query in subquery: for query in subquery:
matching_vouchers.extend( matching_vouchers.extend(
frappe.db.sql( frappe.db.sql(query, filters,)
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): def get_queries(bank_account, company, transaction, document_types):
# get queries to get matching vouchers # get queries to get matching vouchers
@@ -371,27 +288,6 @@ def get_queries(bank_account, company, transaction, document_types):
account_from_to = "paid_to" if transaction.deposit > 0 else "paid_from" account_from_to = "paid_to" if transaction.deposit > 0 else "paid_from"
queries = [] queries = []
# get matching queries from all the apps
for method_name in frappe.get_hooks("get_matching_queries"):
queries.extend(
frappe.get_attr(method_name)(
bank_account,
company,
transaction,
document_types,
amount_condition,
account_from_to,
)
or []
)
return queries
def get_matching_queries(
bank_account, company, transaction, document_types, amount_condition, account_from_to
):
queries = []
if "payment_entry" in document_types: if "payment_entry" in document_types:
pe_amount_matching = get_pe_matching_query(amount_condition, account_from_to, transaction) pe_amount_matching = get_pe_matching_query(amount_condition, account_from_to, transaction)
queries.extend([pe_amount_matching]) queries.extend([pe_amount_matching])
@@ -409,101 +305,12 @@ def get_matching_queries(
pi_amount_matching = get_pi_matching_query(amount_condition) pi_amount_matching = get_pi_matching_query(amount_condition)
queries.extend([pi_amount_matching]) queries.extend([pi_amount_matching])
if "expense_claim" in document_types:
ec_amount_matching = get_ec_matching_query(bank_account, company, amount_condition)
queries.extend([ec_amount_matching])
return queries return queries
def get_loan_vouchers(bank_account, transaction, document_types, filters):
vouchers = []
amount_condition = True if "exact_match" in document_types else False
if transaction.withdrawal > 0 and "loan_disbursement" in document_types:
vouchers.extend(get_ld_matching_query(bank_account, amount_condition, filters))
if transaction.deposit > 0 and "loan_repayment" in document_types:
vouchers.extend(get_lr_matching_query(bank_account, amount_condition, 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")
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,
)
.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"))
else:
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")
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,
)
.where(loan_repayment.docstatus == 1)
.where(loan_repayment.clearance_date.isnull())
.where(loan_repayment.payment_account == bank_account)
)
if frappe.db.has_column("Loan Repayment", "repay_from_salary"):
query = query.where((loan_repayment.repay_from_salary == 0))
if amount_condition:
query.where(loan_repayment.amount_paid == filters.get("amount"))
else:
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): def get_pe_matching_query(amount_condition, account_from_to, transaction):
# get matching payment entries query # get matching payment entries query
if transaction.deposit > 0: if transaction.deposit > 0:
@@ -541,6 +348,7 @@ def get_je_matching_query(amount_condition, transaction):
# We have mapping at the bank level # We have mapping at the bank level
# So one bank could have both types of bank accounts like asset and liability # So one bank could have both types of bank accounts like asset and liability
# So cr_or_dr should be judged only on basis of withdrawal and deposit and not account type # So cr_or_dr should be judged only on basis of withdrawal and deposit and not account type
company_account = frappe.get_value("Bank Account", transaction.bank_account, "account")
cr_or_dr = "credit" if transaction.withdrawal > 0 else "debit" cr_or_dr = "credit" if transaction.withdrawal > 0 else "debit"
return f""" return f"""
@@ -599,7 +407,6 @@ def get_si_matching_query(amount_condition):
AND si.docstatus = 1 AND si.docstatus = 1
""" """
def get_pi_matching_query(amount_condition): def get_pi_matching_query(amount_condition):
# get matching purchase invoice query # get matching purchase invoice query
return f""" return f"""
@@ -624,3 +431,32 @@ def get_pi_matching_query(amount_condition):
AND ifnull(clearance_date, '') = "" AND ifnull(clearance_date, '') = ""
AND cash_bank_account = %(bank_account)s 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) + '\' )'
company_currency = get_company_currency(company)
return f"""
SELECT
( CASE WHEN employee = %(party)s THEN 1 ELSE 0 END
+ 1 ) AS rank,
'Expense Claim' as doctype,
name,
total_sanctioned_amount as paid_amount,
'' as reference_no,
'' as reference_date,
employee as party,
'Employee' as party_type,
posting_date,
'{company_currency}' as currency
FROM
`tabExpense Claim`
WHERE
total_sanctioned_amount {amount_condition} %(amount)s
AND docstatus = 1
AND is_paid = 1
AND ifnull(clearance_date, '') = ""
AND mode_of_payment in {mode_of_payments}
"""

View File

@@ -100,7 +100,7 @@ frappe.ui.form.on("Bank Statement Import", {
if (frm.doc.status.includes("Success")) { if (frm.doc.status.includes("Success")) {
frm.add_custom_button( frm.add_custom_button(
__("Go to {0} List", [__(frm.doc.reference_doctype)]), __("Go to {0} List", [frm.doc.reference_doctype]),
() => frappe.set_route("List", frm.doc.reference_doctype) () => frappe.set_route("List", frm.doc.reference_doctype)
); );
} }
@@ -141,7 +141,7 @@ frappe.ui.form.on("Bank Statement Import", {
}, },
show_import_status(frm) { show_import_status(frm) {
let import_log = JSON.parse(frm.doc.statement_import_log || "[]"); let import_log = JSON.parse(frm.doc.import_log || "[]");
let successful_records = import_log.filter((log) => log.success); let successful_records = import_log.filter((log) => log.success);
let failed_records = import_log.filter((log) => !log.success); let failed_records = import_log.filter((log) => !log.success);
if (successful_records.length === 0) return; if (successful_records.length === 0) return;
@@ -200,7 +200,7 @@ frappe.ui.form.on("Bank Statement Import", {
}) })
.then((result) => { .then((result) => {
if (result.length > 0) { if (result.length > 0) {
frm.add_custom_button(__("Report Error"), () => { frm.add_custom_button("Report Error", () => {
let fake_xhr = { let fake_xhr = {
responseText: JSON.stringify({ responseText: JSON.stringify({
exc: result[0].error, exc: result[0].error,
@@ -309,7 +309,7 @@ frappe.ui.form.on("Bank Statement Import", {
// method: 'frappe.core.doctype.data_import.data_import.get_preview_from_template', // method: 'frappe.core.doctype.data_import.data_import.get_preview_from_template',
show_import_preview(frm, preview_data) { show_import_preview(frm, preview_data) {
let import_log = JSON.parse(frm.doc.statement_import_log || "[]"); let import_log = JSON.parse(frm.doc.import_log || "[]");
if ( if (
frm.import_preview && frm.import_preview &&
@@ -439,7 +439,7 @@ frappe.ui.form.on("Bank Statement Import", {
}, },
show_import_log(frm) { show_import_log(frm) {
let import_log = JSON.parse(frm.doc.statement_import_log || "[]"); let import_log = JSON.parse(frm.doc.import_log || "[]");
let logs = import_log; let logs = import_log;
frm.toggle_display("import_log", false); frm.toggle_display("import_log", false);
frm.toggle_display("import_log_section", logs.length > 0); frm.toggle_display("import_log_section", logs.length > 0);

View File

@@ -24,7 +24,7 @@
"section_import_preview", "section_import_preview",
"import_preview", "import_preview",
"import_log_section", "import_log_section",
"statement_import_log", "import_log",
"show_failed_logs", "show_failed_logs",
"import_log_preview", "import_log_preview",
"reference_doctype", "reference_doctype",
@@ -90,6 +90,12 @@
"options": "JSON", "options": "JSON",
"read_only": 1 "read_only": 1
}, },
{
"fieldname": "import_log",
"fieldtype": "Code",
"label": "Import Log",
"options": "JSON"
},
{ {
"fieldname": "import_log_section", "fieldname": "import_log_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
@@ -192,17 +198,11 @@
{ {
"fieldname": "column_break_4", "fieldname": "column_break_4",
"fieldtype": "Column Break" "fieldtype": "Column Break"
},
{
"fieldname": "statement_import_log",
"fieldtype": "Code",
"label": "Statement Import Log",
"options": "JSON"
} }
], ],
"hide_toolbar": 1, "hide_toolbar": 1,
"links": [], "links": [],
"modified": "2022-09-07 11:11:40.293317", "modified": "2021-05-12 14:17:37.777246",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Bank Statement Import", "name": "Bank Statement Import",

View File

@@ -18,7 +18,6 @@ from openpyxl.utils import get_column_letter
INVALID_VALUES = ("", None) INVALID_VALUES = ("", None)
class BankStatementImport(DataImport): class BankStatementImport(DataImport):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(BankStatementImport, self).__init__(*args, **kwargs) super(BankStatementImport, self).__init__(*args, **kwargs)
@@ -50,16 +49,20 @@ class BankStatementImport(DataImport):
self.import_file, self.google_sheets_url 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")) frappe.throw(_("Please add the Bank Account column"))
from frappe.utils.background_jobs import is_job_queued from frappe.core.page.background_jobs.background_jobs import get_info
from frappe.utils.scheduler import is_scheduler_inactive from frappe.utils.scheduler import is_scheduler_inactive
if is_scheduler_inactive() and not frappe.flags.in_test: 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")
)
if not is_job_queued(self.name): enqueued_jobs = [d.get("job_name") for d in get_info()]
if self.name not in enqueued_jobs:
enqueue( enqueue(
start_import, start_import,
queue="default", queue="default",
@@ -78,25 +81,21 @@ class BankStatementImport(DataImport):
return False return False
@frappe.whitelist() @frappe.whitelist()
def get_preview_from_template(data_import, import_file=None, google_sheets_url=None): 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( return frappe.get_doc("Bank Statement Import", data_import).get_preview_from_template(
import_file, google_sheets_url import_file, google_sheets_url
) )
@frappe.whitelist() @frappe.whitelist()
def form_start_import(data_import): def form_start_import(data_import):
return frappe.get_doc("Bank Statement Import", data_import).start_import() return frappe.get_doc("Bank Statement Import", data_import).start_import()
@frappe.whitelist() @frappe.whitelist()
def download_errored_template(data_import_name): def download_errored_template(data_import_name):
data_import = frappe.get_doc("Bank Statement Import", data_import_name) data_import = frappe.get_doc("Bank Statement Import", data_import_name)
data_import.export_errored_rows() data_import.export_errored_rows()
def parse_data_from_template(raw_data): def parse_data_from_template(raw_data):
data = [] data = []
@@ -109,10 +108,7 @@ def parse_data_from_template(raw_data):
return 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""" """This method runs in background job"""
update_mapping_db(bank, template_options) update_mapping_db(bank, template_options)
@@ -120,7 +116,7 @@ def start_import(
data_import = frappe.get_doc("Bank Statement Import", data_import) data_import = frappe.get_doc("Bank Statement Import", data_import)
file = import_file_path if import_file_path else google_sheets_url 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) data = parse_data_from_template(import_file.raw_data)
@@ -134,24 +130,22 @@ def start_import(
except Exception: except Exception:
frappe.db.rollback() frappe.db.rollback()
data_import.db_set("status", "Error") data_import.db_set("status", "Error")
data_import.log_error("Bank Statement Import failed") frappe.log_error(title=data_import.name)
finally: finally:
frappe.flags.in_import = False frappe.flags.in_import = False
frappe.publish_realtime("data_import_refresh", {"data_import": data_import.name}) frappe.publish_realtime("data_import_refresh", {"data_import": data_import.name})
def update_mapping_db(bank, template_options): def update_mapping_db(bank, template_options):
bank = frappe.get_doc("Bank", bank) bank = frappe.get_doc("Bank", bank)
for d in bank.bank_transaction_mapping: for d in bank.bank_transaction_mapping:
d.delete() d.delete()
for d in json.loads(template_options)["column_to_field_map"].items(): 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() bank.save()
def add_bank_account(data, bank_account): def add_bank_account(data, bank_account):
bank_account_loc = None bank_account_loc = None
if "Bank Account" not in data[0]: if "Bank Account" not in data[0]:
@@ -167,7 +161,6 @@ def add_bank_account(data, bank_account):
else: else:
row.append(bank_account) row.append(bank_account)
def write_files(import_file, data): def write_files(import_file, data):
full_file_path = import_file.file_doc.get_full_path() full_file_path = import_file.file_doc.get_full_path()
parts = import_file.file_doc.get_extension() parts = import_file.file_doc.get_extension()
@@ -175,12 +168,11 @@ def write_files(import_file, data):
extension = extension.lstrip(".") extension = extension.lstrip(".")
if extension == "csv": 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 = csv.writer(file)
writer.writerows(data) writer.writerows(data)
elif extension == "xlsx" or "xls": 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): def write_xlsx(data, sheet_name, wb=None, column_widths=None, file_path=None):
# from xlsx utils with changes # from xlsx utils with changes
@@ -195,19 +187,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 ws.column_dimensions[get_column_letter(i + 1)].width = column_width
row1 = ws.row_dimensions[1] row1 = ws.row_dimensions[1]
row1.font = Font(name="Calibri", bold=True) row1.font = Font(name='Calibri', bold=True)
for row in data: for row in data:
clean_row = [] clean_row = []
for item in 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) value = handle_html(item)
else: else:
value = item value = item
if isinstance(item, str) and next(ILLEGAL_CHARACTERS_RE.finditer(value), None): if isinstance(item, str) and next(ILLEGAL_CHARACTERS_RE.finditer(value), None):
# Remove illegal characters from the string # Remove illegal characters from the string
value = re.sub(ILLEGAL_CHARACTERS_RE, "", value) value = re.sub(ILLEGAL_CHARACTERS_RE, '', value)
clean_row.append(value) clean_row.append(value)
@@ -216,20 +208,19 @@ def write_xlsx(data, sheet_name, wb=None, column_widths=None, file_path=None):
wb.save(file_path) wb.save(file_path)
return True return True
@frappe.whitelist() @frappe.whitelist()
def upload_bank_statement(**args): def upload_bank_statement(**args):
args = frappe._dict(args) args = frappe._dict(args)
bsi = frappe.new_doc("Bank Statement Import") bsi = frappe.new_doc("Bank Statement Import")
if args.company: if args.company:
bsi.update( bsi.update({
{
"company": args.company, "company": args.company,
} })
)
if args.bank_account: if args.bank_account:
bsi.update({"bank_account": args.bank_account}) bsi.update({
"bank_account": args.bank_account
})
return bsi return bsi

View File

@@ -3,21 +3,28 @@
frappe.ui.form.on("Bank Transaction", { frappe.ui.form.on("Bank Transaction", {
onload(frm) { onload(frm) {
frm.set_query("payment_document", "payment_entries", function() { frm.set_query("payment_document", "payment_entries", function () {
const payment_doctypes = frm.events.get_payment_doctypes(frm);
return { return {
filters: { filters: {
name: ["in", payment_doctypes], name: [
"in",
[
"Payment Entry",
"Journal Entry",
"Sales Invoice",
"Purchase Invoice",
"Expense Claim",
],
],
}, },
}; };
}); });
}, },
bank_account: function (frm) {
bank_account: function(frm) {
set_bank_statement_filter(frm); set_bank_statement_filter(frm);
}, },
setup: function(frm) { setup: function (frm) {
frm.set_query("party_type", function () { frm.set_query("party_type", function () {
return { return {
filters: { filters: {
@@ -26,16 +33,6 @@ frappe.ui.form.on("Bank Transaction", {
}; };
}); });
}, },
get_payment_doctypes: function() {
// get payment doctypes from all the apps
return [
"Payment Entry",
"Journal Entry",
"Sales Invoice",
"Purchase Invoice",
];
}
}); });
frappe.ui.form.on("Bank Transaction Payments", { frappe.ui.form.on("Bank Transaction Payments", {

View File

@@ -134,8 +134,7 @@
{ {
"fieldname": "allocated_amount", "fieldname": "allocated_amount",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Allocated Amount", "label": "Allocated Amount"
"options": "currency"
}, },
{ {
"fieldname": "amended_from", "fieldname": "amended_from",
@@ -153,8 +152,7 @@
{ {
"fieldname": "unallocated_amount", "fieldname": "unallocated_amount",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Unallocated Amount", "label": "Unallocated Amount"
"options": "currency"
}, },
{ {
"fieldname": "party_section", "fieldname": "party_section",
@@ -194,11 +192,10 @@
], ],
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2022-03-21 19:05:04.208222", "modified": "2021-04-14 17:31:58.963529",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Bank Transaction", "name": "Bank Transaction",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
@@ -245,7 +242,6 @@
], ],
"sort_field": "date", "sort_field": "date",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"title_field": "bank_account", "title_field": "bank_account",
"track_changes": 1 "track_changes": 1
} }

View File

@@ -29,26 +29,17 @@ class BankTransaction(StatusUpdater):
def update_allocations(self): def update_allocations(self):
if self.payment_entries: if self.payment_entries:
allocated_amount = reduce( allocated_amount = reduce(lambda x, y: flt(x) + flt(y), [x.allocated_amount for x in self.payment_entries])
lambda x, y: flt(x) + flt(y), [x.allocated_amount for x in self.payment_entries]
)
else: else:
allocated_amount = 0 allocated_amount = 0
if allocated_amount: if allocated_amount:
frappe.db.set_value(self.doctype, self.name, "allocated_amount", flt(allocated_amount)) frappe.db.set_value(self.doctype, self.name, "allocated_amount", flt(allocated_amount))
frappe.db.set_value( frappe.db.set_value(self.doctype, self.name, "unallocated_amount", abs(flt(self.withdrawal) - flt(self.deposit)) - flt(allocated_amount))
self.doctype,
self.name,
"unallocated_amount",
abs(flt(self.withdrawal) - flt(self.deposit)) - flt(allocated_amount),
)
else: else:
frappe.db.set_value(self.doctype, self.name, "allocated_amount", 0) frappe.db.set_value(self.doctype, self.name, "allocated_amount", 0)
frappe.db.set_value( frappe.db.set_value(self.doctype, self.name, "unallocated_amount", abs(flt(self.withdrawal) - flt(self.deposit)))
self.doctype, self.name, "unallocated_amount", abs(flt(self.withdrawal) - flt(self.deposit))
)
amount = self.deposit or self.withdrawal amount = self.deposit or self.withdrawal
if amount == self.allocated_amount: if amount == self.allocated_amount:
@@ -58,54 +49,46 @@ class BankTransaction(StatusUpdater):
def clear_linked_payment_entries(self, for_cancel=False): def clear_linked_payment_entries(self, for_cancel=False):
for payment_entry in self.payment_entries: for payment_entry in self.payment_entries:
if payment_entry.payment_document == "Sales Invoice": if payment_entry.payment_document in ["Payment Entry", "Journal Entry", "Purchase Invoice", "Expense Claim"]:
self.clear_sales_invoice(payment_entry, for_cancel=for_cancel)
elif payment_entry.payment_document in get_doctypes_for_bank_reconciliation():
self.clear_simple_entry(payment_entry, for_cancel=for_cancel) self.clear_simple_entry(payment_entry, for_cancel=for_cancel)
elif payment_entry.payment_document == "Sales Invoice":
self.clear_sales_invoice(payment_entry, for_cancel=for_cancel)
def clear_simple_entry(self, payment_entry, for_cancel=False): def clear_simple_entry(self, payment_entry, for_cancel=False):
if payment_entry.payment_document == "Payment Entry": if payment_entry.payment_document == "Payment Entry":
if ( if frappe.db.get_value("Payment Entry", payment_entry.payment_entry, "payment_type") == "Internal Transfer":
frappe.db.get_value("Payment Entry", payment_entry.payment_entry, "payment_type")
== "Internal Transfer"
):
if len(get_reconciled_bank_transactions(payment_entry)) < 2: if len(get_reconciled_bank_transactions(payment_entry)) < 2:
return return
clearance_date = self.date if not for_cancel else None clearance_date = self.date if not for_cancel else None
frappe.db.set_value( 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): def clear_sales_invoice(self, payment_entry, for_cancel=False):
clearance_date = self.date if not for_cancel else None clearance_date = self.date if not for_cancel else None
frappe.db.set_value( frappe.db.set_value(
"Sales Invoice Payment", "Sales Invoice Payment",
dict(parenttype=payment_entry.payment_document, parent=payment_entry.payment_entry), dict(
"clearance_date", parenttype=payment_entry.payment_document,
clearance_date, parent=payment_entry.payment_entry
) ),
"clearance_date", clearance_date)
@frappe.whitelist()
def get_doctypes_for_bank_reconciliation():
"""Get Bank Reconciliation doctypes from all the apps"""
return frappe.get_hooks("bank_reconciliation_doctypes")
def get_reconciled_bank_transactions(payment_entry): def get_reconciled_bank_transactions(payment_entry):
reconciled_bank_transactions = frappe.get_all( reconciled_bank_transactions = frappe.get_all(
"Bank Transaction Payments", 'Bank Transaction Payments',
filters={"payment_entry": payment_entry.payment_entry}, filters = {
fields=["parent"], 'payment_entry': payment_entry.payment_entry
},
fields = ['parent']
) )
return reconciled_bank_transactions return reconciled_bank_transactions
def get_total_allocated_amount(payment_entry): def get_total_allocated_amount(payment_entry):
return frappe.db.sql( return frappe.db.sql("""
"""
SELECT SELECT
SUM(btp.allocated_amount) as allocated_amount, SUM(btp.allocated_amount) as allocated_amount,
bt.name bt.name
@@ -118,73 +101,36 @@ def get_total_allocated_amount(payment_entry):
AND AND
btp.payment_entry = %s btp.payment_entry = %s
AND AND
bt.docstatus = 1""", bt.docstatus = 1""", (payment_entry.payment_document, payment_entry.payment_entry), as_dict=True)
(payment_entry.payment_document, payment_entry.payment_entry),
as_dict=True,
)
def get_paid_amount(payment_entry, currency, bank_account): def get_paid_amount(payment_entry, currency, bank_account):
if payment_entry.payment_document in ["Payment Entry", "Sales Invoice", "Purchase Invoice"]: if payment_entry.payment_document in ["Payment Entry", "Sales Invoice", "Purchase Invoice"]:
paid_amount_field = "paid_amount" 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) 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")
if doc.payment_type == "Receive": return frappe.db.get_value(payment_entry.payment_document,
paid_amount_field = ( payment_entry.payment_entry, 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": elif payment_entry.payment_document == "Journal Entry":
return frappe.db.get_value( return frappe.db.get_value('Journal Entry Account', {'parent': payment_entry.payment_entry, 'account': bank_account}, "sum(credit_in_account_currency)")
"Journal Entry Account",
{"parent": payment_entry.payment_entry, "account": bank_account},
"sum(credit_in_account_currency)",
)
elif payment_entry.payment_document == "Expense Claim": elif payment_entry.payment_document == "Expense Claim":
return frappe.db.get_value( return frappe.db.get_value(payment_entry.payment_document, payment_entry.payment_entry, "total_amount_reimbursed")
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"
)
elif payment_entry.payment_document == "Loan Repayment":
return frappe.db.get_value(
payment_entry.payment_document, payment_entry.payment_entry, "amount_paid"
)
else: else:
frappe.throw( frappe.throw("Please reconcile {0}: {1} manually".format(payment_entry.payment_document, payment_entry.payment_entry))
"Please reconcile {0}: {1} manually".format(
payment_entry.payment_document, payment_entry.payment_entry
)
)
@frappe.whitelist() @frappe.whitelist()
def unclear_reference_payment(doctype, docname): def unclear_reference_payment(doctype, docname):
if frappe.db.exists(doctype, docname): if frappe.db.exists(doctype, docname):
doc = frappe.get_doc(doctype, docname) doc = frappe.get_doc(doctype, docname)
if doctype == "Sales Invoice": if doctype == "Sales Invoice":
frappe.db.set_value( frappe.db.set_value("Sales Invoice Payment", dict(parenttype=doc.payment_document,
"Sales Invoice Payment", parent=doc.payment_entry), "clearance_date", None)
dict(parenttype=doc.payment_document, parent=doc.payment_entry),
"clearance_date",
None,
)
else: else:
frappe.db.set_value(doc.payment_document, doc.payment_entry, "clearance_date", None) frappe.db.set_value(doc.payment_document, doc.payment_entry, "clearance_date", None)

View File

@@ -18,14 +18,12 @@ def upload_bank_statement():
fcontent = frappe.local.uploaded_file fcontent = frappe.local.uploaded_file
fname = frappe.local.uploaded_filename 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 from frappe.utils.csvutils import read_csv_content
rows = read_csv_content(fcontent, False) 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 from frappe.utils.xlsxutils import read_xlsx_file_from_attached_file
rows = read_xlsx_file_from_attached_file(fcontent=fcontent) rows = read_xlsx_file_from_attached_file(fcontent=fcontent)
columns = rows[0] columns = rows[0]
@@ -45,10 +43,12 @@ def create_bank_entries(columns, data, bank_account):
continue continue
fields = {} fields = {}
for key, value in header_map.items(): for key, value in header_map.items():
fields.update({key: d[int(value) - 1]}) fields.update({key: d[int(value)-1]})
try: try:
bank_transaction = frappe.get_doc({"doctype": "Bank Transaction"}) bank_transaction = frappe.get_doc({
"doctype": "Bank Transaction"
})
bank_transaction.update(fields) bank_transaction.update(fields)
bank_transaction.date = getdate(parse_date(bank_transaction.date)) bank_transaction.date = getdate(parse_date(bank_transaction.date))
bank_transaction.bank_account = bank_account bank_transaction.bank_account = bank_account
@@ -56,12 +56,11 @@ def create_bank_entries(columns, data, bank_account):
bank_transaction.submit() bank_transaction.submit()
success += 1 success += 1
except Exception: except Exception:
bank_transaction.log_error("Bank entry creation failed") frappe.log_error(frappe.get_traceback())
errors += 1 errors += 1
return {"success": success, "errors": errors} return {"success": success, "errors": errors}
def get_header_mapping(columns, bank_account): def get_header_mapping(columns, bank_account):
mapping = get_bank_mapping(bank_account) mapping = get_bank_mapping(bank_account)
@@ -72,11 +71,10 @@ def get_header_mapping(columns, bank_account):
return header_map return header_map
def get_bank_mapping(bank_account): def get_bank_mapping(bank_account):
bank_name = frappe.get_cached_value("Bank Account", bank_account, "bank") bank_name = frappe.db.get_value("Bank Account", bank_account, "bank")
bank = frappe.get_doc("Bank", bank_name) 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 return mapping

View File

@@ -5,7 +5,6 @@ import json
import unittest import unittest
import frappe import frappe
from frappe.tests.utils import FrappeTestCase
from erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool import ( from erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool import (
get_linked_payments, get_linked_payments,
@@ -18,52 +17,45 @@ from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sal
test_dependencies = ["Item", "Cost Center"] test_dependencies = ["Item", "Cost Center"]
class TestBankTransaction(unittest.TestCase):
class TestBankTransaction(FrappeTestCase): @classmethod
def setUp(self): def setUpClass(cls):
for dt in [
"Loan Repayment",
"Bank Transaction",
"Payment Entry",
"Payment Entry Reference",
"POS Profile",
]:
frappe.db.delete(dt)
make_pos_profile() make_pos_profile()
add_transactions() add_transactions()
add_vouchers() add_vouchers()
@classmethod
def tearDownClass(cls):
for bt in frappe.get_all("Bank Transaction"):
doc = frappe.get_doc("Bank Transaction", bt.name)
if doc.docstatus == 1:
doc.cancel()
doc.delete()
# Delete directly in DB to avoid validation errors for countries not allowing deletion
frappe.db.sql("""delete from `tabPayment Entry Reference`""")
frappe.db.sql("""delete from `tabPayment Entry`""")
# Delete POS Profile
frappe.db.sql("delete from `tabPOS Profile`")
# This test checks if ERPNext is able to provide a linked payment for a bank transaction based on the amount of the bank transaction. # 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): def test_linked_payments(self):
bank_transaction = frappe.get_doc( bank_transaction = frappe.get_doc("Bank Transaction", dict(description="Re 95282925234 FE/000002917 AT171513000281183046 Conrad Electronic"))
"Bank Transaction", linked_payments = get_linked_payments(bank_transaction.name, ['payment_entry', 'exact_match'])
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") 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 # This test validates a simple reconciliation leading to the clearance of the bank transaction and the payment
def test_reconcile(self): def test_reconcile(self):
bank_transaction = frappe.get_doc( bank_transaction = frappe.get_doc("Bank Transaction", dict(description="1512567 BG/000003025 OPSKATTUZWXXX AT776000000098709849 Herr G"))
"Bank Transaction",
dict(description="1512567 BG/000003025 OPSKATTUZWXXX AT776000000098709849 Herr G"),
)
payment = frappe.get_doc("Payment Entry", dict(party="Mr G", paid_amount=1700)) payment = frappe.get_doc("Payment Entry", dict(party="Mr G", paid_amount=1700))
vouchers = json.dumps( vouchers = json.dumps([{
[ "payment_doctype":"Payment Entry",
{ "payment_name":payment.name,
"payment_doctype": "Payment Entry", "amount":bank_transaction.unallocated_amount}])
"payment_name": payment.name,
"amount": bank_transaction.unallocated_amount,
}
]
)
reconcile_vouchers(bank_transaction.name, vouchers) reconcile_vouchers(bank_transaction.name, vouchers)
unallocated_amount = frappe.db.get_value( unallocated_amount = frappe.db.get_value("Bank Transaction", bank_transaction.name, "unallocated_amount")
"Bank Transaction", bank_transaction.name, "unallocated_amount"
)
self.assertTrue(unallocated_amount == 0) self.assertTrue(unallocated_amount == 0)
clearance_date = frappe.db.get_value("Payment Entry", payment.name, "clearance_date") clearance_date = frappe.db.get_value("Payment Entry", payment.name, "clearance_date")
@@ -77,206 +69,122 @@ class TestBankTransaction(FrappeTestCase):
# Check if ERPNext can correctly filter a linked payments based on the debit/credit amount # Check if ERPNext can correctly filter a linked payments based on the debit/credit amount
def test_debit_credit_output(self): def test_debit_credit_output(self):
bank_transaction = frappe.get_doc( bank_transaction = frappe.get_doc("Bank Transaction", dict(description="Auszahlung Karte MC/000002916 AUTOMAT 698769 K002 27.10. 14:07"))
"Bank Transaction", linked_payments = get_linked_payments(bank_transaction.name, ['payment_entry', 'exact_match'])
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]) self.assertTrue(linked_payments[0][3])
# Check error if already reconciled # Check error if already reconciled
def test_already_reconciled(self): def test_already_reconciled(self):
bank_transaction = frappe.get_doc( bank_transaction = frappe.get_doc("Bank Transaction", dict(description="1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G"))
"Bank Transaction",
dict(description="1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G"),
)
payment = frappe.get_doc("Payment Entry", dict(party="Mr G", paid_amount=1200)) payment = frappe.get_doc("Payment Entry", dict(party="Mr G", paid_amount=1200))
vouchers = json.dumps( vouchers = json.dumps([{
[ "payment_doctype":"Payment Entry",
{ "payment_name":payment.name,
"payment_doctype": "Payment Entry", "amount":bank_transaction.unallocated_amount}])
"payment_name": payment.name,
"amount": bank_transaction.unallocated_amount,
}
]
)
reconcile_vouchers(bank_transaction.name, vouchers) reconcile_vouchers(bank_transaction.name, vouchers)
bank_transaction = frappe.get_doc( bank_transaction = frappe.get_doc("Bank Transaction", dict(description="1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G"))
"Bank Transaction",
dict(description="1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G"),
)
payment = frappe.get_doc("Payment Entry", dict(party="Mr G", paid_amount=1200)) payment = frappe.get_doc("Payment Entry", dict(party="Mr G", paid_amount=1200))
vouchers = json.dumps( vouchers = json.dumps([{
[ "payment_doctype":"Payment Entry",
{ "payment_name":payment.name,
"payment_doctype": "Payment Entry", "amount":bank_transaction.unallocated_amount}])
"payment_name": payment.name, self.assertRaises(frappe.ValidationError, reconcile_vouchers, bank_transaction_name=bank_transaction.name, vouchers=vouchers)
"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 # Raise an error if debitor transaction vs debitor payment
def test_clear_sales_invoice(self): def test_clear_sales_invoice(self):
bank_transaction = frappe.get_doc( bank_transaction = frappe.get_doc("Bank Transaction", dict(description="I2015000011 VD/000002514 ATWWXXX AT4701345000003510057 Bio"))
"Bank Transaction",
dict(description="I2015000011 VD/000002514 ATWWXXX AT4701345000003510057 Bio"),
)
payment = frappe.get_doc("Sales Invoice", dict(customer="Fayva", status=["=", "Paid"])) payment = frappe.get_doc("Sales Invoice", dict(customer="Fayva", status=["=", "Paid"]))
vouchers = json.dumps( vouchers = json.dumps([{
[ "payment_doctype":"Sales Invoice",
{ "payment_name":payment.name,
"payment_doctype": "Sales Invoice", "amount":bank_transaction.unallocated_amount}])
"payment_name": payment.name,
"amount": bank_transaction.unallocated_amount,
}
]
)
reconcile_vouchers(bank_transaction.name, vouchers=vouchers) reconcile_vouchers(bank_transaction.name, vouchers=vouchers)
self.assertEqual( self.assertEqual(frappe.db.get_value("Bank Transaction", bank_transaction.name, "unallocated_amount"), 0)
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.assertTrue(
frappe.db.get_value("Sales Invoice Payment", dict(parent=payment.name), "clearance_date")
is not None
)
def test_matching_loan_repayment(self):
from erpnext.loan_management.doctype.loan.test_loan import create_loan_accounts
create_loan_accounts()
bank_account = frappe.get_doc(
{
"doctype": "Bank Account",
"account_name": "Payment Account",
"bank": "Citi Bank",
"account": "Payment Account - _TC",
}
).insert(ignore_if_duplicate=True)
bank_transaction = frappe.get_doc(
{
"doctype": "Bank Transaction",
"description": "Loan Repayment - OPSKATTUZWXXX AT776000000098709837 Herr G",
"date": "2018-10-27",
"deposit": 500,
"currency": "INR",
"bank_account": bank_account.name,
}
).submit()
repayment_entry = create_loan_and_repayment()
linked_payments = get_linked_payments(bank_transaction.name, ["loan_repayment", "exact_match"])
self.assertEqual(linked_payments[0][2], repayment_entry.name)
def create_bank_account(bank_name="Citi Bank", account_name="_Test Bank - _TC"): def create_bank_account(bank_name="Citi Bank", account_name="_Test Bank - _TC"):
try: try:
frappe.get_doc( frappe.get_doc({
{
"doctype": "Bank", "doctype": "Bank",
"bank_name": bank_name, "bank_name":bank_name,
} }).insert()
).insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError: except frappe.DuplicateEntryError:
pass pass
try: try:
frappe.get_doc( frappe.get_doc({
{
"doctype": "Bank Account", "doctype": "Bank Account",
"account_name": "Checking Account", "account_name":"Checking Account",
"bank": bank_name, "bank": bank_name,
"account": account_name, "account": account_name
} }).insert()
).insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError: except frappe.DuplicateEntryError:
pass pass
def add_transactions(): def add_transactions():
create_bank_account() create_bank_account()
doc = frappe.get_doc( doc = frappe.get_doc({
{
"doctype": "Bank Transaction", "doctype": "Bank Transaction",
"description": "1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G", "description":"1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G",
"date": "2018-10-23", "date": "2018-10-23",
"deposit": 1200, "deposit": 1200,
"currency": "INR", "currency": "INR",
"bank_account": "Checking Account - Citi Bank", "bank_account": "Checking Account - Citi Bank"
} }).insert()
).insert()
doc.submit() doc.submit()
doc = frappe.get_doc( doc = frappe.get_doc({
{
"doctype": "Bank Transaction", "doctype": "Bank Transaction",
"description": "1512567 BG/000003025 OPSKATTUZWXXX AT776000000098709849 Herr G", "description":"1512567 BG/000003025 OPSKATTUZWXXX AT776000000098709849 Herr G",
"date": "2018-10-23", "date": "2018-10-23",
"deposit": 1700, "deposit": 1700,
"currency": "INR", "currency": "INR",
"bank_account": "Checking Account - Citi Bank", "bank_account": "Checking Account - Citi Bank"
} }).insert()
).insert()
doc.submit() doc.submit()
doc = frappe.get_doc( doc = frappe.get_doc({
{
"doctype": "Bank Transaction", "doctype": "Bank Transaction",
"description": "Re 95282925234 FE/000002917 AT171513000281183046 Conrad Electronic", "description":"Re 95282925234 FE/000002917 AT171513000281183046 Conrad Electronic",
"date": "2018-10-26", "date": "2018-10-26",
"withdrawal": 690, "withdrawal": 690,
"currency": "INR", "currency": "INR",
"bank_account": "Checking Account - Citi Bank", "bank_account": "Checking Account - Citi Bank"
} }).insert()
).insert()
doc.submit() doc.submit()
doc = frappe.get_doc( doc = frappe.get_doc({
{
"doctype": "Bank Transaction", "doctype": "Bank Transaction",
"description": "Auszahlung Karte MC/000002916 AUTOMAT 698769 K002 27.10. 14:07", "description":"Auszahlung Karte MC/000002916 AUTOMAT 698769 K002 27.10. 14:07",
"date": "2018-10-27", "date": "2018-10-27",
"deposit": 3900, "deposit": 3900,
"currency": "INR", "currency": "INR",
"bank_account": "Checking Account - Citi Bank", "bank_account": "Checking Account - Citi Bank"
} }).insert()
).insert()
doc.submit() doc.submit()
doc = frappe.get_doc( doc = frappe.get_doc({
{
"doctype": "Bank Transaction", "doctype": "Bank Transaction",
"description": "I2015000011 VD/000002514 ATWWXXX AT4701345000003510057 Bio", "description":"I2015000011 VD/000002514 ATWWXXX AT4701345000003510057 Bio",
"date": "2018-10-27", "date": "2018-10-27",
"withdrawal": 109080, "withdrawal": 109080,
"currency": "INR", "currency": "INR",
"bank_account": "Checking Account - Citi Bank", "bank_account": "Checking Account - Citi Bank"
} }).insert()
).insert()
doc.submit() doc.submit()
def add_vouchers(): def add_vouchers():
try: try:
frappe.get_doc( frappe.get_doc({
{
"doctype": "Supplier", "doctype": "Supplier",
"supplier_group": "All Supplier Groups", "supplier_group":"All Supplier Groups",
"supplier_type": "Company", "supplier_type": "Company",
"supplier_name": "Conrad Electronic", "supplier_name": "Conrad Electronic"
} }).insert()
).insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError: except frappe.DuplicateEntryError:
pass pass
@@ -290,14 +198,12 @@ def add_vouchers():
pe.submit() pe.submit()
try: try:
frappe.get_doc( frappe.get_doc({
{
"doctype": "Supplier", "doctype": "Supplier",
"supplier_group": "All Supplier Groups", "supplier_group":"All Supplier Groups",
"supplier_type": "Company", "supplier_type": "Company",
"supplier_name": "Mr G", "supplier_name": "Mr G"
} }).insert()
).insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError: except frappe.DuplicateEntryError:
pass pass
@@ -316,30 +222,26 @@ def add_vouchers():
pe.submit() pe.submit()
try: try:
frappe.get_doc( frappe.get_doc({
{
"doctype": "Supplier", "doctype": "Supplier",
"supplier_group": "All Supplier Groups", "supplier_group":"All Supplier Groups",
"supplier_type": "Company", "supplier_type": "Company",
"supplier_name": "Poore Simon's", "supplier_name": "Poore Simon's"
} }).insert()
).insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError: except frappe.DuplicateEntryError:
pass pass
try: try:
frappe.get_doc( frappe.get_doc({
{
"doctype": "Customer", "doctype": "Customer",
"customer_group": "All Customer Groups", "customer_group":"All Customer Groups",
"customer_type": "Company", "customer_type": "Company",
"customer_name": "Poore Simon's", "customer_name": "Poore Simon's"
} }).insert()
).insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError: except frappe.DuplicateEntryError:
pass 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.cash_bank_account = "_Test Bank - _TC"
pi.insert() pi.insert()
pi.submit() pi.submit()
@@ -359,87 +261,33 @@ def add_vouchers():
pe.submit() pe.submit()
try: try:
frappe.get_doc( frappe.get_doc({
{
"doctype": "Customer", "doctype": "Customer",
"customer_group": "All Customer Groups", "customer_group":"All Customer Groups",
"customer_type": "Company", "customer_type": "Company",
"customer_name": "Fayva", "customer_name": "Fayva"
} }).insert()
).insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError: except frappe.DuplicateEntryError:
pass 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( if not frappe.db.get_value('Mode of Payment Account', {'company': "_Test Company", 'parent': "Cash"}):
"Mode of Payment Account", {"company": "_Test Company", "parent": "Cash"} mode_of_payment.append("accounts", {
): "company": "_Test Company",
mode_of_payment.append( "default_account": "_Test Bank - _TC"
"accounts", {"company": "_Test Company", "default_account": "_Test Bank - _TC"} })
)
mode_of_payment.save() mode_of_payment.save()
si = create_sales_invoice(customer="Fayva", qty=1, rate=109080, do_not_save=1) si = create_sales_invoice(customer="Fayva", qty=1, rate=109080, do_not_save=1)
si.is_pos = 1 si.is_pos = 1
si.append( si.append("payments", {
"payments", {"mode_of_payment": "Cash", "account": "_Test Bank - _TC", "amount": 109080} "mode_of_payment": "Cash",
) "account": "_Test Bank - _TC",
"amount": 109080
})
si.insert() si.insert()
si.submit() si.submit()
def create_loan_and_repayment():
from erpnext.loan_management.doctype.loan.test_loan import (
create_loan,
create_loan_type,
create_repayment_entry,
make_loan_disbursement_entry,
)
from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import (
process_loan_interest_accrual_for_term_loans,
)
from erpnext.setup.doctype.employee.test_employee import make_employee
create_loan_type(
"Personal Loan",
500000,
8.4,
is_term_loan=1,
mode_of_payment="Cash",
disbursement_account="Disbursement Account - _TC",
payment_account="Payment Account - _TC",
loan_account="Loan Account - _TC",
interest_income_account="Interest Income Account - _TC",
penalty_income_account="Penalty Income Account - _TC",
)
applicant = make_employee("test_bank_reco@loan.com", company="_Test Company")
loan = create_loan(applicant, "Personal Loan", 5000, "Repay Over Number of Periods", 20)
loan = frappe.get_doc(
{
"doctype": "Loan",
"applicant_type": "Employee",
"company": "_Test Company",
"applicant": applicant,
"loan_type": "Personal Loan",
"loan_amount": 5000,
"repayment_method": "Repay Fixed Amount per Period",
"monthly_repayment_amount": 500,
"repayment_start_date": "2018-09-27",
"is_term_loan": 1,
"posting_date": "2018-09-27",
}
).insert()
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date="2018-09-27")
process_loan_interest_accrual_for_term_loans(posting_date="2018-10-27")
repayment_entry = create_repayment_entry(
loan.name,
applicant,
"2018-10-27",
500,
)
repayment_entry.submit()
return repayment_entry

View File

@@ -1,7 +1,6 @@
{ {
"actions": [], "actions": [],
"allow_import": 1, "allow_import": 1,
"autoname": "naming_series:",
"creation": "2016-05-16 11:42:29.632528", "creation": "2016-05-16 11:42:29.632528",
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 1, "editable_grid": 1,
@@ -10,7 +9,6 @@
"budget_against", "budget_against",
"company", "company",
"cost_center", "cost_center",
"naming_series",
"project", "project",
"fiscal_year", "fiscal_year",
"column_break_3", "column_break_3",
@@ -192,26 +190,15 @@
"label": "Budget Accounts", "label": "Budget Accounts",
"options": "Budget Account", "options": "Budget Account",
"reqd": 1 "reqd": 1
},
{
"fieldname": "naming_series",
"fieldtype": "Data",
"hidden": 1,
"label": "Series",
"no_copy": 1,
"print_hide": 1,
"read_only": 1,
"set_only_once": 1
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2022-10-10 22:14:36.361509", "modified": "2020-10-06 15:13:54.055854",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Budget", "name": "Budget",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
@@ -233,6 +220,5 @@
], ],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@@ -5,6 +5,7 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.model.naming import make_autoname
from frappe.utils import add_months, flt, fmt_money, get_last_day, getdate from frappe.utils import add_months, flt, fmt_money, get_last_day, getdate
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
@@ -13,15 +14,14 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
from erpnext.accounts.utils import get_fiscal_year from erpnext.accounts.utils import get_fiscal_year
class BudgetError(frappe.ValidationError): class BudgetError(frappe.ValidationError): pass
pass class DuplicateBudgetError(frappe.ValidationError): pass
class DuplicateBudgetError(frappe.ValidationError):
pass
class Budget(Document): class Budget(Document):
def autoname(self):
self.name = make_autoname(self.get(frappe.scrub(self.budget_against))
+ "/" + self.fiscal_year + "/.###")
def validate(self): def validate(self):
if not self.get(frappe.scrub(self.budget_against)): if not self.get(frappe.scrub(self.budget_against)):
frappe.throw(_("{0} is mandatory").format(self.budget_against)) frappe.throw(_("{0} is mandatory").format(self.budget_against))
@@ -35,44 +35,34 @@ class Budget(Document):
budget_against = self.get(budget_against_field) budget_against = self.get(budget_against_field)
accounts = [d.account for d in self.accounts] or [] accounts = [d.account for d in self.accounts] or []
existing_budget = frappe.db.sql( existing_budget = frappe.db.sql("""
"""
select select
b.name, ba.account from `tabBudget` b, `tabBudget Account` ba b.name, ba.account from `tabBudget` b, `tabBudget Account` ba
where where
ba.parent = b.name and b.docstatus < 2 and b.company = %s and %s=%s and 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) """ b.fiscal_year=%s and b.name != %s and ba.account in (%s) """
% ("%s", budget_against_field, "%s", "%s", "%s", ",".join(["%s"] * len(accounts))), % ('%s', budget_against_field, '%s', '%s', '%s', ','.join(['%s'] * len(accounts))),
(self.company, budget_against, self.fiscal_year, self.name) + tuple(accounts), (self.company, budget_against, self.fiscal_year, self.name) + tuple(accounts), as_dict=1)
as_dict=1,
)
for d in existing_budget: for d in existing_budget:
frappe.throw( 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)
"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): def validate_accounts(self):
account_list = [] account_list = []
for d in self.get("accounts"): for d in self.get('accounts'):
if d.account: if d.account:
account_details = frappe.get_cached_value( account_details = frappe.db.get_value("Account", d.account,
"Account", d.account, ["is_group", "company", "report_type"], as_dict=1 ["is_group", "company", "report_type"], as_dict=1)
)
if account_details.is_group: if account_details.is_group:
frappe.throw(_("Budget cannot be assigned against Group Account {0}").format(d.account)) frappe.throw(_("Budget cannot be assigned against Group Account {0}").format(d.account))
elif account_details.company != self.company: 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": elif account_details.report_type != "Profit and Loss":
frappe.throw( frappe.throw(_("Budget cannot be assigned against {0}, as it's not an Income or Expense account")
_("Budget cannot be assigned against {0}, as it's not an Income or Expense account").format( .format(d.account))
d.account
)
)
if d.account in account_list: if d.account in account_list:
frappe.throw(_("Account {0} has been entered multiple times").format(d.account)) frappe.throw(_("Account {0} has been entered multiple times").format(d.account))
@@ -80,69 +70,51 @@ class Budget(Document):
account_list.append(d.account) account_list.append(d.account)
def set_null_value(self): def set_null_value(self):
if self.budget_against == "Cost Center": if self.budget_against == 'Cost Center':
self.project = None self.project = None
else: else:
self.cost_center = None self.cost_center = None
def validate_applicable_for(self): def validate_applicable_for(self):
if self.applicable_on_material_request and not ( if (self.applicable_on_material_request
self.applicable_on_purchase_order and self.applicable_on_booking_actual_expenses 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"))
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")) frappe.throw(_("Please enable Applicable on Booking Actual Expenses"))
elif not ( elif not(self.applicable_on_material_request
self.applicable_on_material_request or self.applicable_on_purchase_order or self.applicable_on_booking_actual_expenses):
or self.applicable_on_purchase_order
or self.applicable_on_booking_actual_expenses
):
self.applicable_on_booking_actual_expenses = 1 self.applicable_on_booking_actual_expenses = 1
def before_naming(self): def validate_expense_against_budget(args):
self.naming_series = f"{{{frappe.scrub(self.budget_against)}}}./.{self.fiscal_year}/.###"
def validate_expense_against_budget(args, expense_amount=0):
args = frappe._dict(args) args = frappe._dict(args)
if args.get("company") and not args.fiscal_year: if args.get('company') and not args.fiscal_year:
args.fiscal_year = get_fiscal_year(args.get("posting_date"), company=args.get("company"))[0] args.fiscal_year = get_fiscal_year(args.get('posting_date'), company=args.get('company'))[0]
frappe.flags.exception_approver_role = frappe.get_cached_value( frappe.flags.exception_approver_role = frappe.get_cached_value('Company',
"Company", args.get("company"), "exception_budget_approver_role" args.get('company'), 'exception_budget_approver_role')
)
if not args.account: if not args.account:
args.account = args.get("expense_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) args.cost_center, args.account = get_item_details(args)
if not args.account: if not args.account:
return return
for budget_against in ["project", "cost_center"] + get_accounting_dimensions(): for budget_against in ['project', 'cost_center'] + get_accounting_dimensions():
if ( if (args.get(budget_against) and args.account
args.get(budget_against) and frappe.db.get_value("Account", {"name": args.account, "root_type": "Expense"})):
and args.account
and frappe.db.get_value("Account", {"name": args.account, "root_type": "Expense"})
):
doctype = frappe.unscrub(budget_against) 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"]) lft, rgt = frappe.db.get_value(doctype, args.get(budget_against), ["lft", "rgt"])
condition = """and exists(select name from `tab%s` condition = """and exists(select name from `tab%s`
where lft<=%s and rgt>=%s and name=b.%s)""" % ( where lft<=%s and rgt>=%s and name=b.%s)""" % (doctype, lft, rgt, budget_against) #nosec
doctype,
lft,
rgt,
budget_against,
) # nosec
args.is_tree = True args.is_tree = True
else: else:
condition = "and b.%s=%s" % (budget_against, frappe.db.escape(args.get(budget_against))) condition = "and b.%s=%s" % (budget_against, frappe.db.escape(args.get(budget_against)))
@@ -151,8 +123,7 @@ def validate_expense_against_budget(args, expense_amount=0):
args.budget_against_field = budget_against args.budget_against_field = budget_against
args.budget_against_doctype = doctype args.budget_against_doctype = doctype
budget_records = frappe.db.sql( budget_records = frappe.db.sql("""
"""
select select
b.{budget_against_field} as budget_against, ba.budget_amount, b.monthly_distribution, b.{budget_against_field} as budget_against, ba.budget_amount, b.monthly_distribution,
ifnull(b.applicable_on_material_request, 0) as for_material_request, ifnull(b.applicable_on_material_request, 0) as for_material_request,
@@ -167,136 +138,100 @@ def validate_expense_against_budget(args, expense_amount=0):
b.name=ba.parent and b.fiscal_year=%s b.name=ba.parent and b.fiscal_year=%s
and ba.account=%s and b.docstatus=1 and ba.account=%s and b.docstatus=1
{condition} {condition}
""".format( """.format(condition=condition, budget_against_field=budget_against), (args.fiscal_year, args.account), as_dict=True) #nosec
condition=condition, budget_against_field=budget_against
),
(args.fiscal_year, args.account),
as_dict=True,
) # nosec
if budget_records: if budget_records:
validate_budget_records(args, budget_records, expense_amount) validate_budget_records(args, budget_records)
def validate_budget_records(args, budget_records):
def validate_budget_records(args, budget_records, expense_amount):
for budget in budget_records: for budget in budget_records:
if flt(budget.budget_amount): if flt(budget.budget_amount):
amount = expense_amount or get_amount(args, budget) amount = get_amount(args, budget)
yearly_action, monthly_action = get_actions(args, budget) yearly_action, monthly_action = get_actions(args, budget)
if monthly_action in ["Stop", "Warn"]: if monthly_action in ["Stop", "Warn"]:
budget_amount = get_accumulated_monthly_budget( budget_amount = get_accumulated_monthly_budget(budget.monthly_distribution,
budget.monthly_distribution, args.posting_date, args.fiscal_year, budget.budget_amount args.posting_date, args.fiscal_year, budget.budget_amount)
)
args["month_end_date"] = get_last_day(args.posting_date) args["month_end_date"] = get_last_day(args.posting_date)
compare_expense_with_budget( compare_expense_with_budget(args, budget_amount,
args, budget_amount, _("Accumulated Monthly"), monthly_action, budget.budget_against, 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): def compare_expense_with_budget(args, budget_amount, action_for, action, budget_against, amount=0):
actual_expense = amount or get_actual_expense(args) actual_expense = amount or get_actual_expense(args)
if actual_expense > budget_amount: if actual_expense > budget_amount:
diff = 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( msg = _("{0} Budget for Account {1} against {2} {3} is {4}. It will exceed by {5}").format(
_(action_for), _(action_for), frappe.bold(args.account), args.budget_against_field,
frappe.bold(args.account),
args.budget_against_field,
frappe.bold(budget_against), frappe.bold(budget_against),
frappe.bold(fmt_money(budget_amount, currency=currency)), frappe.bold(fmt_money(budget_amount, currency=currency)),
frappe.bold(fmt_money(diff, currency=currency)), frappe.bold(fmt_money(diff, currency=currency)))
)
if ( if (frappe.flags.exception_approver_role
frappe.flags.exception_approver_role and frappe.flags.exception_approver_role in frappe.get_roles(frappe.session.user)):
and frappe.flags.exception_approver_role in frappe.get_roles(frappe.session.user)
):
action = "Warn" action = "Warn"
if action == "Stop": if action=="Stop":
frappe.throw(msg, BudgetError) frappe.throw(msg, BudgetError)
else: else:
frappe.msgprint(msg, indicator="orange") frappe.msgprint(msg, indicator='orange')
def get_actions(args, budget): def get_actions(args, budget):
yearly_action = budget.action_if_annual_budget_exceeded yearly_action = budget.action_if_annual_budget_exceeded
monthly_action = budget.action_if_accumulated_monthly_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 yearly_action = budget.action_if_annual_budget_exceeded_on_mr
monthly_action = budget.action_if_accumulated_monthly_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 yearly_action = budget.action_if_annual_budget_exceeded_on_po
monthly_action = budget.action_if_accumulated_monthly_budget_exceeded_on_po monthly_action = budget.action_if_accumulated_monthly_budget_exceeded_on_po
return yearly_action, monthly_action return yearly_action, monthly_action
def get_amount(args, budget): def get_amount(args, budget):
amount = 0 amount = 0
if args.get("doctype") == "Material Request" and budget.for_material_request: if args.get('doctype') == 'Material Request' and budget.for_material_request:
amount = ( amount = (get_requested_amount(args, budget)
get_requested_amount(args, budget) + get_ordered_amount(args, budget) + get_actual_expense(args) + 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) amount = get_ordered_amount(args, budget) + get_actual_expense(args)
return amount return amount
def get_requested_amount(args, budget): def get_requested_amount(args, budget):
item_code = args.get("item_code") item_code = args.get('item_code')
condition = get_other_condition(args, budget, "Material Request") condition = get_other_condition(args, budget, 'Material Request')
data = frappe.db.sql( data = frappe.db.sql(""" select ifnull((sum(child.stock_qty - child.ordered_qty) * rate), 0) as amount
""" 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 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 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( parent.material_request_type = 'Purchase' and parent.status != 'Stopped'""".format(condition), item_code, as_list=1)
condition
),
item_code,
as_list=1,
)
return data[0][0] if data else 0 return data[0][0] if data else 0
def get_ordered_amount(args, budget): def get_ordered_amount(args, budget):
item_code = args.get("item_code") item_code = args.get('item_code')
condition = get_other_condition(args, budget, "Purchase Order") condition = get_other_condition(args, budget, 'Purchase Order')
data = frappe.db.sql( data = frappe.db.sql(""" select ifnull(sum(child.amount - child.billed_amt), 0) as amount
""" select ifnull(sum(child.amount - child.billed_amt), 0) as amount
from `tabPurchase Order Item` child, `tabPurchase Order` parent where 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 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( and parent.status != 'Closed' and {0}""".format(condition), item_code, as_list=1)
condition
),
item_code,
as_list=1,
)
return data[0][0] if data else 0 return data[0][0] if data else 0
def get_other_condition(args, budget, for_doc): def get_other_condition(args, budget, for_doc):
condition = "expense_account = '%s'" % (args.expense_account) condition = "expense_account = '%s'" % (args.expense_account)
budget_against_field = args.get("budget_against_field") budget_against_field = args.get("budget_against_field")
@@ -304,51 +239,41 @@ def get_other_condition(args, budget, for_doc):
if budget_against_field and args.get(budget_against_field): if budget_against_field and args.get(budget_against_field):
condition += " and child.%s = '%s'" % (budget_against_field, args.get(budget_against_field)) condition += " and child.%s = '%s'" % (budget_against_field, args.get(budget_against_field))
if args.get("fiscal_year"): if args.get('fiscal_year'):
date_field = "schedule_date" if for_doc == "Material Request" else "transaction_date" date_field = 'schedule_date' if for_doc == 'Material Request' else 'transaction_date'
start_date, end_date = frappe.get_cached_value( start_date, end_date = frappe.db.get_value('Fiscal Year', args.get('fiscal_year'),
"Fiscal Year", args.get("fiscal_year"), ["year_start_date", "year_end_date"] ['year_start_date', 'year_end_date'])
)
condition += """ and parent.%s condition += """ and parent.%s
between '%s' and '%s' """ % ( between '%s' and '%s' """ %(date_field, start_date, end_date)
date_field,
start_date,
end_date,
)
return condition return condition
def get_actual_expense(args): def get_actual_expense(args):
if not args.budget_against_doctype: if not args.budget_against_doctype:
args.budget_against_doctype = frappe.unscrub(args.budget_against_field) args.budget_against_doctype = frappe.unscrub(args.budget_against_field)
budget_against_field = args.get("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 "" condition1 = " and gle.posting_date <= %(month_end_date)s" \
if args.get("month_end_date") else ""
if args.is_tree: if args.is_tree:
lft_rgt = frappe.db.get_value( lft_rgt = frappe.db.get_value(args.budget_against_doctype,
args.budget_against_doctype, args.get(budget_against_field), ["lft", "rgt"], as_dict=1 args.get(budget_against_field), ["lft", "rgt"], as_dict=1)
)
args.update(lft_rgt) args.update(lft_rgt)
condition2 = """and exists(select name from `tab{doctype}` condition2 = """and exists(select name from `tab{doctype}`
where lft>=%(lft)s and rgt<=%(rgt)s where lft>=%(lft)s and rgt<=%(rgt)s
and name=gle.{budget_against_field})""".format( and name=gle.{budget_against_field})""".format(doctype=args.budget_against_doctype, #nosec
doctype=args.budget_against_doctype, budget_against_field=budget_against_field # nosec budget_against_field=budget_against_field)
)
else: else:
condition2 = """and exists(select name from `tab{doctype}` condition2 = """and exists(select name from `tab{doctype}`
where name=gle.{budget_against} and where name=gle.{budget_against} and
gle.{budget_against} = %({budget_against})s)""".format( gle.{budget_against} = %({budget_against})s)""".format(doctype=args.budget_against_doctype,
doctype=args.budget_against_doctype, budget_against=budget_against_field budget_against = budget_against_field)
)
amount = flt( amount = flt(frappe.db.sql("""
frappe.db.sql(
"""
select sum(gle.debit) - sum(gle.credit) select sum(gle.debit) - sum(gle.credit)
from `tabGL Entry` gle from `tabGL Entry` gle
where gle.account=%(account)s where gle.account=%(account)s
@@ -357,59 +282,46 @@ def get_actual_expense(args):
and gle.company=%(company)s and gle.company=%(company)s
and gle.docstatus=1 and gle.docstatus=1
{condition2} {condition2}
""".format( """.format(condition1=condition1, condition2=condition2), (args))[0][0]) #nosec
condition1=condition1, condition2=condition2
),
(args),
)[0][0]
) # nosec
return amount return amount
def get_accumulated_monthly_budget(monthly_distribution, posting_date, fiscal_year, annual_budget): def get_accumulated_monthly_budget(monthly_distribution, posting_date, fiscal_year, annual_budget):
distribution = {} distribution = {}
if monthly_distribution: if monthly_distribution:
for d in frappe.db.sql( for d in frappe.db.sql("""select mdp.month, mdp.percentage_allocation
"""select mdp.month, mdp.percentage_allocation
from `tabMonthly Distribution Percentage` mdp, `tabMonthly Distribution` md from `tabMonthly Distribution Percentage` mdp, `tabMonthly Distribution` md
where mdp.parent=md.name and md.fiscal_year=%s""", where mdp.parent=md.name and md.fiscal_year=%s""", fiscal_year, as_dict=1):
fiscal_year,
as_dict=1,
):
distribution.setdefault(d.month, d.percentage_allocation) distribution.setdefault(d.month, d.percentage_allocation)
dt = frappe.get_cached_value("Fiscal Year", fiscal_year, "year_start_date") dt = frappe.db.get_value("Fiscal Year", fiscal_year, "year_start_date")
accumulated_percentage = 0.0 accumulated_percentage = 0.0
while dt <= getdate(posting_date): while(dt <= getdate(posting_date)):
if monthly_distribution: if monthly_distribution:
accumulated_percentage += distribution.get(getdate(dt).strftime("%B"), 0) accumulated_percentage += distribution.get(getdate(dt).strftime("%B"), 0)
else: else:
accumulated_percentage += 100.0 / 12 accumulated_percentage += 100.0/12
dt = add_months(dt, 1) dt = add_months(dt, 1)
return annual_budget * accumulated_percentage / 100 return annual_budget * accumulated_percentage / 100
def get_item_details(args): def get_item_details(args):
cost_center, expense_account = None, None cost_center, expense_account = None, None
if not args.get("company"): if not args.get('company'):
return cost_center, expense_account return cost_center, expense_account
if args.item_code: if args.item_code:
item_defaults = frappe.db.get_value( item_defaults = frappe.db.get_value('Item Default',
"Item Default", {'parent': args.item_code, 'company': args.get('company')},
{"parent": args.item_code, "company": args.get("company")}, ['buying_cost_center', 'expense_account'])
["buying_cost_center", "expense_account"],
)
if item_defaults: if item_defaults:
cost_center, expense_account = item_defaults cost_center, expense_account = item_defaults
if not (cost_center and expense_account): 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) data = get_expense_cost_center(doctype, args)
if not cost_center and data: if not cost_center and data:
@@ -423,15 +335,11 @@ def get_item_details(args):
return cost_center, expense_account return cost_center, expense_account
def get_expense_cost_center(doctype, args): def get_expense_cost_center(doctype, args):
if doctype == "Item Group": if doctype == 'Item Group':
return frappe.db.get_value( return frappe.db.get_value('Item Default',
"Item Default", {'parent': args.get(frappe.scrub(doctype)), 'company': args.get('company')},
{"parent": args.get(frappe.scrub(doctype)), "company": args.get("company")}, ['buying_cost_center', 'expense_account'])
["buying_cost_center", "expense_account"],
)
else: else:
return frappe.db.get_value( return frappe.db.get_value(doctype, args.get(frappe.scrub(doctype)),\
doctype, args.get(frappe.scrub(doctype)), ["cost_center", "default_expense_account"] ['cost_center', 'default_expense_account'])
)

View File

@@ -11,8 +11,7 @@ from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journ
from erpnext.accounts.utils import get_fiscal_year from erpnext.accounts.utils import get_fiscal_year
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order 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): class TestBudget(unittest.TestCase):
def test_monthly_budget_crossed_ignore(self): def test_monthly_budget_crossed_ignore(self):
@@ -20,18 +19,11 @@ class TestBudget(unittest.TestCase):
budget = make_budget(budget_against="Cost Center") budget = make_budget(budget_against="Cost Center")
jv = make_journal_entry( jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
"_Test Account Cost for Goods Sold - _TC", "_Test Bank - _TC", 40000, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True)
"_Test Bank - _TC",
40000,
"_Test Cost Center - _TC",
posting_date=nowdate(),
submit=True,
)
self.assertTrue( self.assertTrue(frappe.db.get_value("GL Entry",
frappe.db.get_value("GL Entry", {"voucher_type": "Journal Entry", "voucher_no": jv.name}) {"voucher_type": "Journal Entry", "voucher_no": jv.name}))
)
budget.cancel() budget.cancel()
jv.cancel() jv.cancel()
@@ -41,17 +33,10 @@ class TestBudget(unittest.TestCase):
budget = make_budget(budget_against="Cost Center") budget = make_budget(budget_against="Cost Center")
frappe.db.set_value( frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
"Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop"
)
jv = make_journal_entry( jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
"_Test Account Cost for Goods Sold - _TC", "_Test Bank - _TC", 40000, "_Test Cost Center - _TC", posting_date=nowdate())
"_Test Bank - _TC",
40000,
"_Test Cost Center - _TC",
posting_date=nowdate(),
)
self.assertRaises(BudgetError, jv.submit) self.assertRaises(BudgetError, jv.submit)
@@ -63,65 +48,49 @@ class TestBudget(unittest.TestCase):
budget = make_budget(budget_against="Cost Center") budget = make_budget(budget_against="Cost Center")
frappe.db.set_value( frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
"Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop"
)
jv = make_journal_entry( jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
"_Test Account Cost for Goods Sold - _TC", "_Test Bank - _TC", 40000, "_Test Cost Center - _TC", posting_date=nowdate())
"_Test Bank - _TC",
40000,
"_Test Cost Center - _TC",
posting_date=nowdate(),
)
self.assertRaises(BudgetError, jv.submit) 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() 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() 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.load_from_db()
budget.cancel() budget.cancel()
def test_monthly_budget_crossed_for_mr(self): def test_monthly_budget_crossed_for_mr(self):
budget = make_budget( budget = make_budget(applicable_on_material_request=1,
applicable_on_material_request=1, applicable_on_purchase_order=1, action_if_accumulated_monthly_budget_exceeded_on_mr="Stop",
applicable_on_purchase_order=1, budget_against="Cost Center")
action_if_accumulated_monthly_budget_exceeded_on_mr="Stop",
budget_against="Cost Center",
)
fiscal_year = get_fiscal_year(nowdate())[0] fiscal_year = get_fiscal_year(nowdate())[0]
frappe.db.set_value( frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
"Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop"
)
frappe.db.set_value("Budget", budget.name, "fiscal_year", fiscal_year) frappe.db.set_value("Budget", budget.name, "fiscal_year", fiscal_year)
mr = frappe.get_doc( mr = frappe.get_doc({
{
"doctype": "Material Request", "doctype": "Material Request",
"material_request_type": "Purchase", "material_request_type": "Purchase",
"transaction_date": nowdate(), "transaction_date": nowdate(),
"company": budget.company, "company": budget.company,
"items": [ "items": [{
{ 'item_code': '_Test Item',
"item_code": "_Test Item", 'qty': 1,
"qty": 1, 'uom': "_Test UOM",
"uom": "_Test UOM", 'warehouse': '_Test Warehouse - _TC',
"warehouse": "_Test Warehouse - _TC", 'schedule_date': nowdate(),
"schedule_date": nowdate(), 'rate': 100000,
"rate": 100000, 'expense_account': '_Test Account Cost for Goods Sold - _TC',
"expense_account": "_Test Account Cost for Goods Sold - _TC", 'cost_center': '_Test Cost Center - _TC'
"cost_center": "_Test Cost Center - _TC", }]
} })
],
}
)
mr.set_missing_values() mr.set_missing_values()
@@ -131,16 +100,11 @@ class TestBudget(unittest.TestCase):
budget.cancel() budget.cancel()
def test_monthly_budget_crossed_for_po(self): def test_monthly_budget_crossed_for_po(self):
budget = make_budget( budget = make_budget(applicable_on_purchase_order=1,
applicable_on_purchase_order=1, action_if_accumulated_monthly_budget_exceeded_on_po="Stop", budget_against="Cost Center")
action_if_accumulated_monthly_budget_exceeded_on_po="Stop",
budget_against="Cost Center",
)
fiscal_year = get_fiscal_year(nowdate())[0] fiscal_year = get_fiscal_year(nowdate())[0]
frappe.db.set_value( frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
"Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop"
)
frappe.db.set_value("Budget", budget.name, "fiscal_year", fiscal_year) frappe.db.set_value("Budget", budget.name, "fiscal_year", fiscal_year)
po = create_purchase_order(transaction_date=nowdate(), do_not_submit=True) po = create_purchase_order(transaction_date=nowdate(), do_not_submit=True)
@@ -158,20 +122,12 @@ class TestBudget(unittest.TestCase):
budget = make_budget(budget_against="Project") budget = make_budget(budget_against="Project")
frappe.db.set_value( frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
"Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop"
)
project = frappe.get_value("Project", {"project_name": "_Test Project"}) project = frappe.get_value("Project", {"project_name": "_Test Project"})
jv = make_journal_entry( jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
"_Test Account Cost for Goods Sold - _TC", "_Test Bank - _TC", 40000, "_Test Cost Center - _TC", project=project, posting_date=nowdate())
"_Test Bank - _TC",
40000,
"_Test Cost Center - _TC",
project=project,
posting_date=nowdate(),
)
self.assertRaises(BudgetError, jv.submit) self.assertRaises(BudgetError, jv.submit)
@@ -183,13 +139,8 @@ class TestBudget(unittest.TestCase):
budget = make_budget(budget_against="Cost Center") budget = make_budget(budget_against="Cost Center")
jv = make_journal_entry( jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
"_Test Account Cost for Goods Sold - _TC", "_Test Bank - _TC", 250000, "_Test Cost Center - _TC", posting_date=nowdate())
"_Test Bank - _TC",
250000,
"_Test Cost Center - _TC",
posting_date=nowdate(),
)
self.assertRaises(BudgetError, jv.submit) self.assertRaises(BudgetError, jv.submit)
@@ -202,14 +153,9 @@ class TestBudget(unittest.TestCase):
project = frappe.get_value("Project", {"project_name": "_Test Project"}) project = frappe.get_value("Project", {"project_name": "_Test Project"})
jv = make_journal_entry( jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
"_Test Account Cost for Goods Sold - _TC", "_Test Bank - _TC", 250000, "_Test Cost Center - _TC",
"_Test Bank - _TC", project=project, posting_date=nowdate())
250000,
"_Test Cost Center - _TC",
project=project,
posting_date=nowdate(),
)
self.assertRaises(BudgetError, jv.submit) self.assertRaises(BudgetError, jv.submit)
@@ -223,23 +169,14 @@ class TestBudget(unittest.TestCase):
if month > 9: if month > 9:
month = 9 month = 9
for i in range(month + 1): for i in range(month+1):
jv = make_journal_entry( jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
"_Test Account Cost for Goods Sold - _TC", "_Test Bank - _TC", 20000, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True)
"_Test Bank - _TC",
20000,
"_Test Cost Center - _TC",
posting_date=nowdate(),
submit=True,
)
self.assertTrue( self.assertTrue(frappe.db.get_value("GL Entry",
frappe.db.get_value("GL Entry", {"voucher_type": "Journal Entry", "voucher_no": jv.name}) {"voucher_type": "Journal Entry", "voucher_no": jv.name}))
)
frappe.db.set_value( frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
"Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop"
)
self.assertRaises(BudgetError, jv.cancel) self.assertRaises(BudgetError, jv.cancel)
@@ -256,23 +193,14 @@ class TestBudget(unittest.TestCase):
project = frappe.get_value("Project", {"project_name": "_Test Project"}) project = frappe.get_value("Project", {"project_name": "_Test Project"})
for i in range(month + 1): for i in range(month + 1):
jv = make_journal_entry( jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
"_Test Account Cost for Goods Sold - _TC", "_Test Bank - _TC", 20000, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True,
"_Test Bank - _TC", project=project)
20000,
"_Test Cost Center - _TC",
posting_date=nowdate(),
submit=True,
project=project,
)
self.assertTrue( self.assertTrue(frappe.db.get_value("GL Entry",
frappe.db.get_value("GL Entry", {"voucher_type": "Journal Entry", "voucher_no": jv.name}) {"voucher_type": "Journal Entry", "voucher_no": jv.name}))
)
frappe.db.set_value( frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
"Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop"
)
self.assertRaises(BudgetError, jv.cancel) self.assertRaises(BudgetError, jv.cancel)
@@ -284,17 +212,10 @@ class TestBudget(unittest.TestCase):
set_total_expense_zero(nowdate(), "cost_center", "_Test Cost Center 2 - _TC") set_total_expense_zero(nowdate(), "cost_center", "_Test Cost Center 2 - _TC")
budget = make_budget(budget_against="Cost Center", cost_center="_Test Company - _TC") budget = make_budget(budget_against="Cost Center", cost_center="_Test Company - _TC")
frappe.db.set_value( frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
"Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop"
)
jv = make_journal_entry( jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
"_Test Account Cost for Goods Sold - _TC", "_Test Bank - _TC", 40000, "_Test Cost Center 2 - _TC", posting_date=nowdate())
"_Test Bank - _TC",
40000,
"_Test Cost Center 2 - _TC",
posting_date=nowdate(),
)
self.assertRaises(BudgetError, jv.submit) self.assertRaises(BudgetError, jv.submit)
@@ -305,28 +226,19 @@ class TestBudget(unittest.TestCase):
cost_center = "_Test Cost Center 3 - _TC" cost_center = "_Test Cost Center 3 - _TC"
if not frappe.db.exists("Cost Center", cost_center): if not frappe.db.exists("Cost Center", cost_center):
frappe.get_doc( frappe.get_doc({
{ 'doctype': 'Cost Center',
"doctype": "Cost Center", 'cost_center_name': '_Test Cost Center 3',
"cost_center_name": "_Test Cost Center 3", 'parent_cost_center': "_Test Company - _TC",
"parent_cost_center": "_Test Company - _TC", 'company': '_Test Company',
"company": "_Test Company", 'is_group': 0
"is_group": 0, }).insert(ignore_permissions=True)
}
).insert(ignore_permissions=True)
budget = make_budget(budget_against="Cost Center", cost_center=cost_center) budget = make_budget(budget_against="Cost Center", cost_center=cost_center)
frappe.db.set_value( frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
"Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop"
)
jv = make_journal_entry( jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
"_Test Account Cost for Goods Sold - _TC", "_Test Bank - _TC", 40000, cost_center, posting_date=nowdate())
"_Test Bank - _TC",
40000,
cost_center,
posting_date=nowdate(),
)
self.assertRaises(BudgetError, jv.submit) self.assertRaises(BudgetError, jv.submit)
@@ -334,39 +246,6 @@ class TestBudget(unittest.TestCase):
budget.cancel() budget.cancel()
jv.cancel() jv.cancel()
def test_monthly_budget_against_main_cost_center(self):
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
from erpnext.accounts.doctype.cost_center_allocation.test_cost_center_allocation import (
create_cost_center_allocation,
)
cost_centers = [
"Main Budget Cost Center 1",
"Sub Budget Cost Center 1",
"Sub Budget Cost Center 2",
]
for cc in cost_centers:
create_cost_center(cost_center_name=cc, company="_Test Company")
create_cost_center_allocation(
"_Test Company",
"Main Budget Cost Center 1 - _TC",
{"Sub Budget Cost Center 1 - _TC": 60, "Sub Budget Cost Center 2 - _TC": 40},
)
make_budget(budget_against="Cost Center", cost_center="Main Budget Cost Center 1 - _TC")
jv = make_journal_entry(
"_Test Account Cost for Goods Sold - _TC",
"_Test Bank - _TC",
400000,
"Main Budget Cost Center 1 - _TC",
posting_date=nowdate(),
)
self.assertRaises(BudgetError, jv.submit)
def set_total_expense_zero(posting_date, budget_against_field=None, budget_against_CC=None): def set_total_expense_zero(posting_date, budget_against_field=None, budget_against_CC=None):
if budget_against_field == "project": if budget_against_field == "project":
@@ -376,16 +255,14 @@ def set_total_expense_zero(posting_date, budget_against_field=None, budget_again
fiscal_year = get_fiscal_year(nowdate())[0] fiscal_year = get_fiscal_year(nowdate())[0]
args = frappe._dict( args = frappe._dict({
{
"account": "_Test Account Cost for Goods Sold - _TC", "account": "_Test Account Cost for Goods Sold - _TC",
"cost_center": "_Test Cost Center - _TC", "cost_center": "_Test Cost Center - _TC",
"monthly_end_date": posting_date, "monthly_end_date": posting_date,
"company": "_Test Company", "company": "_Test Company",
"fiscal_year": fiscal_year, "fiscal_year": fiscal_year,
"budget_against_field": budget_against_field, "budget_against_field": budget_against_field,
} })
)
if not args.get(budget_against_field): if not args.get(budget_against_field):
args[budget_against_field] = budget_against args[budget_against_field] = budget_against
@@ -394,42 +271,26 @@ def set_total_expense_zero(posting_date, budget_against_field=None, budget_again
if existing_expense: if existing_expense:
if budget_against_field == "cost_center": if budget_against_field == "cost_center":
make_journal_entry( make_journal_entry("_Test Account Cost for Goods Sold - _TC",
"_Test Account Cost for Goods Sold - _TC", "_Test Bank - _TC", -existing_expense, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True)
"_Test Bank - _TC",
-existing_expense,
"_Test Cost Center - _TC",
posting_date=nowdate(),
submit=True,
)
elif budget_against_field == "project": elif budget_against_field == "project":
make_journal_entry( make_journal_entry("_Test Account Cost for Goods Sold - _TC",
"_Test Account Cost for Goods Sold - _TC", "_Test Bank - _TC", -existing_expense, "_Test Cost Center - _TC", submit=True, project=budget_against, posting_date=nowdate())
"_Test Bank - _TC",
-existing_expense,
"_Test Cost Center - _TC",
submit=True,
project=budget_against,
posting_date=nowdate(),
)
def make_budget(**args): def make_budget(**args):
args = frappe._dict(args) args = frappe._dict(args)
budget_against = args.budget_against budget_against=args.budget_against
cost_center = args.cost_center cost_center=args.cost_center
fiscal_year = get_fiscal_year(nowdate())[0] fiscal_year = get_fiscal_year(nowdate())[0]
if budget_against == "Project": if budget_against == "Project":
project_name = "{0}%".format("_Test Project/" + fiscal_year) 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: else:
cost_center_name = "{0}%".format(cost_center or "_Test Cost Center - _TC/" + fiscal_year) cost_center_name = "{0}%".format(cost_center or "_Test Cost Center - _TC/" + fiscal_year)
budget_list = frappe.get_all( budget_list = frappe.get_all("Budget", fields=["name"], filters = {"name": ("like", cost_center_name)})
"Budget", fields=["name"], filters={"name": ("like", cost_center_name)}
)
for d in budget_list: for d in budget_list:
frappe.db.sql("delete from `tabBudget` where name = %(name)s", d) frappe.db.sql("delete from `tabBudget` where name = %(name)s", d)
frappe.db.sql("delete from `tabBudget Account` where parent = %(name)s", d) frappe.db.sql("delete from `tabBudget Account` where parent = %(name)s", d)
@@ -439,7 +300,7 @@ def make_budget(**args):
if budget_against == "Project": if budget_against == "Project":
budget.project = frappe.get_value("Project", {"project_name": "_Test Project"}) budget.project = frappe.get_value("Project", {"project_name": "_Test Project"})
else: 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 = frappe.get_doc("Monthly Distribution", "_Test Distribution")
monthly_distribution.fiscal_year = fiscal_year monthly_distribution.fiscal_year = fiscal_year
@@ -451,27 +312,20 @@ def make_budget(**args):
budget.action_if_annual_budget_exceeded = "Stop" budget.action_if_annual_budget_exceeded = "Stop"
budget.action_if_accumulated_monthly_budget_exceeded = "Ignore" budget.action_if_accumulated_monthly_budget_exceeded = "Ignore"
budget.budget_against = budget_against budget.budget_against = budget_against
budget.append( budget.append("accounts", {
"accounts", {"account": "_Test Account Cost for Goods Sold - _TC", "budget_amount": 200000} "account": "_Test Account Cost for Goods Sold - _TC",
) "budget_amount": 200000
})
if args.applicable_on_material_request: if args.applicable_on_material_request:
budget.applicable_on_material_request = 1 budget.applicable_on_material_request = 1
budget.action_if_annual_budget_exceeded_on_mr = ( budget.action_if_annual_budget_exceeded_on_mr = args.action_if_annual_budget_exceeded_on_mr or 'Warn'
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_accumulated_monthly_budget_exceeded_on_mr = (
args.action_if_accumulated_monthly_budget_exceeded_on_mr or "Warn"
)
if args.applicable_on_purchase_order: if args.applicable_on_purchase_order:
budget.applicable_on_purchase_order = 1 budget.applicable_on_purchase_order = 1
budget.action_if_annual_budget_exceeded_on_po = ( budget.action_if_annual_budget_exceeded_on_po = args.action_if_annual_budget_exceeded_on_po or 'Warn'
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_accumulated_monthly_budget_exceeded_on_po = (
args.action_if_accumulated_monthly_budget_exceeded_on_po or "Warn"
)
budget.insert() budget.insert()
budget.submit() budget.submit()

View File

@@ -0,0 +1 @@
C Form (India specific only) - Will be deprecated.

View File

@@ -0,0 +1,43 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
//c-form js file
// -----------------------------
frappe.ui.form.on('C-Form', {
setup(frm) {
frm.fields_dict.invoices.grid.get_field("invoice_no").get_query = function(doc) {
return {
filters: {
"docstatus": 1,
"customer": doc.customer,
"company": doc.company,
"c_form_applicable": 'Yes',
"c_form_no": ''
}
};
}
frm.fields_dict.state.get_query = function() {
return {
filters: {
country: "India"
}
};
}
}
});
frappe.ui.form.on('C-Form Invoice Detail', {
invoice_no(frm, cdt, cdn) {
let d = frappe.get_doc(cdt, cdn);
if (d.invoice_no) {
frm.call('get_invoice_details', {
invoice_no: d.invoice_no
}).then(r => {
frappe.model.set_value(cdt, cdn, r.message);
});
}
}
});

View File

@@ -0,0 +1,511 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 1,
"allow_rename": 0,
"autoname": "naming_series:",
"beta": 0,
"creation": "2013-03-07 11:55:06",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"editable_grid": 0,
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "",
"fieldname": "naming_series",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Series",
"length": 0,
"no_copy": 0,
"options": "ACC-CF-.YYYY.-",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 1,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "c_form_no",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "C-Form No",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "received_date",
"fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Received Date",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "customer",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Customer",
"length": 0,
"no_copy": 0,
"options": "Customer",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break1",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "50%",
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0,
"width": "50%"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "company",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 1,
"label": "Company",
"length": 0,
"no_copy": 0,
"options": "Company",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "quarter",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 1,
"label": "Quarter",
"length": 0,
"no_copy": 0,
"options": "\nI\nII\nIII\nIV",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "total_amount",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Total Amount",
"length": 0,
"no_copy": 0,
"options": "Company:company:default_currency",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "state",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "State",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_break0",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "invoices",
"fieldtype": "Table",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Invoices",
"length": 0,
"no_copy": 0,
"options": "C-Form Invoice Detail",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "total_invoiced_amount",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Total Invoiced Amount",
"length": 0,
"no_copy": 0,
"options": "Company:company:default_currency",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "amended_from",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 1,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Amended From",
"length": 0,
"no_copy": 1,
"options": "C-Form",
"permlevel": 0,
"print_hide": 1,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"icon": "fa fa-file-text",
"idx": 1,
"image_view": 0,
"in_create": 0,
"is_submittable": 1,
"issingle": 0,
"istable": 0,
"max_attachments": 3,
"modified": "2018-08-21 14:44:30.558767",
"modified_by": "Administrator",
"module": "Accounts",
"name": "C-Form",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 0,
"email": 1,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts User",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
},
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 0,
"email": 1,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
},
{
"amend": 0,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 0,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 1,
"print": 0,
"read": 1,
"report": 1,
"role": "All",
"set_user_permissions": 0,
"share": 0,
"submit": 0,
"write": 0
}
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_order": "DESC",
"timeline_field": "customer",
"track_changes": 0,
"track_seen": 0,
"track_views": 0
}

View File

@@ -0,0 +1,72 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import flt
class CForm(Document):
def validate(self):
"""Validate invoice that c-form is applicable
and no other c-form is received for that"""
for d in self.get('invoices'):
if d.invoice_no:
inv = frappe.db.sql("""select c_form_applicable, c_form_no from
`tabSales Invoice` where name = %s and docstatus = 1""", d.invoice_no)
if inv and inv[0][0] != 'Yes':
frappe.throw(_("C-form is not applicable for Invoice: {0}").format(d.invoice_no))
elif inv and inv[0][1] and inv[0][1] != self.name:
frappe.throw(_("""Invoice {0} is tagged in another C-form: {1}.
If you want to change C-form no for this invoice,
please remove invoice no from the previous c-form and then try again"""\
.format(d.invoice_no, inv[0][1])))
elif not inv:
frappe.throw(_("Row {0}: Invoice {1} is invalid, it might be cancelled / does not exist. \
Please enter a valid Invoice".format(d.idx, d.invoice_no)))
def on_update(self):
""" Update C-Form No on invoices"""
self.set_total_invoiced_amount()
def on_submit(self):
self.set_cform_in_sales_invoices()
def before_cancel(self):
# remove cform reference
frappe.db.sql("""update `tabSales Invoice` set c_form_no=null where c_form_no=%s""", self.name)
def set_cform_in_sales_invoices(self):
inv = [d.invoice_no for d in self.get('invoices')]
if inv:
frappe.db.sql("""update `tabSales Invoice` set c_form_no=%s, modified=%s where name in (%s)""" %
('%s', '%s', ', '.join(['%s'] * len(inv))), tuple([self.name, self.modified] + inv))
frappe.db.sql("""update `tabSales Invoice` set c_form_no = null, modified = %s
where name not in (%s) and ifnull(c_form_no, '') = %s""" %
('%s', ', '.join(['%s']*len(inv)), '%s'), tuple([self.modified] + inv + [self.name]))
else:
frappe.throw(_("Please enter atleast 1 invoice in the table"))
def set_total_invoiced_amount(self):
total = sum(flt(d.grand_total) for d in self.get('invoices'))
frappe.db.set(self, 'total_invoiced_amount', total)
@frappe.whitelist()
def get_invoice_details(self, invoice_no):
""" Pull details from invoices for referrence """
if invoice_no:
inv = frappe.db.get_value("Sales Invoice", invoice_no,
["posting_date", "territory", "base_net_total", "base_grand_total"], as_dict=True)
return {
'invoice_date' : inv.posting_date,
'territory' : inv.territory,
'net_total' : inv.base_net_total,
'grand_total' : inv.base_grand_total
}

View File

@@ -0,0 +1,9 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
# test_records = frappe.get_test_records('C-Form')
class TestCForm(unittest.TestCase):
pass

View File

@@ -0,0 +1 @@
Invoice detail for parent C-Form.

View File

@@ -0,0 +1,168 @@
{
"allow_copy": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2013-02-22 01:27:38",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"editable_grid": 1,
"fields": [
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "invoice_no",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Invoice No",
"length": 0,
"no_copy": 0,
"options": "Sales Invoice",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "160px",
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0,
"width": "160px"
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "invoice_date",
"fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Invoice Date",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "120px",
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0,
"width": "120px"
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"description": "",
"fieldname": "territory",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Territory",
"length": 0,
"no_copy": 0,
"options": "Territory",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "120px",
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0,
"width": "120px"
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "net_total",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Net Total",
"length": 0,
"no_copy": 0,
"options": "Company:company:default_currency",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "120px",
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0,
"width": "120px"
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "grand_total",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Grand Total",
"length": 0,
"no_copy": 0,
"options": "Company:company:default_currency",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "120px",
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0,
"width": "120px"
}
],
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 1,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2016-07-11 03:27:58.768719",
"modified_by": "Administrator",
"module": "Accounts",
"name": "C-Form Invoice Detail",
"owner": "Administrator",
"permissions": [],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"track_seen": 0
}

View File

@@ -0,0 +1,9 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
from frappe.model.document import Document
class CFormInvoiceDetail(Document):
pass

View File

@@ -1,25 +1,25 @@
DEFAULT_MAPPERS = [ DEFAULT_MAPPERS = [
{ {
"doctype": "Cash Flow Mapper", 'doctype': 'Cash Flow Mapper',
"section_footer": "Net cash generated by operating activities", 'section_footer': 'Net cash generated by operating activities',
"section_header": "Cash flows from operating activities", 'section_header': 'Cash flows from operating activities',
"section_leader": "Adjustments for", 'section_leader': 'Adjustments for',
"section_name": "Operating Activities", 'section_name': 'Operating Activities',
"position": 0, 'position': 0,
"section_subtotal": "Cash generated from operations", 'section_subtotal': 'Cash generated from operations',
}, },
{ {
"doctype": "Cash Flow Mapper", 'doctype': 'Cash Flow Mapper',
"position": 1, 'position': 1,
"section_footer": "Net cash used in investing activities", 'section_footer': 'Net cash used in investing activities',
"section_header": "Cash flows from investing activities", 'section_header': 'Cash flows from investing activities',
"section_name": "Investing Activities", 'section_name': 'Investing Activities'
}, },
{ {
"doctype": "Cash Flow Mapper", 'doctype': 'Cash Flow Mapper',
"position": 2, 'position': 2,
"section_footer": "Net cash used in financing activites", 'section_footer': 'Net cash used in financing activites',
"section_header": "Cash flows from financing activities", 'section_header': 'Cash flows from financing activities',
"section_name": "Financing Activities", 'section_name': 'Financing Activities',
}, }
] ]

View File

@@ -3,7 +3,6 @@
import frappe import frappe
from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
@@ -12,11 +11,9 @@ class CashFlowMapping(Document):
self.validate_checked_options() self.validate_checked_options()
def validate_checked_options(self): def validate_checked_options(self):
checked_fields = [ checked_fields = [d for d in self.meta.fields if d.fieldtype == 'Check' and self.get(d.fieldname) == 1]
d for d in self.meta.fields if d.fieldtype == "Check" and self.get(d.fieldname) == 1
]
if len(checked_fields) > 1: if len(checked_fields) > 1:
frappe.throw( frappe.throw(
_("You can only select a maximum of one option from the list of check boxes."), frappe._('You can only select a maximum of one option from the list of check boxes.'),
title=_("Error"), title='Error'
) )

View File

@@ -9,16 +9,19 @@ import frappe
class TestCashFlowMapping(unittest.TestCase): class TestCashFlowMapping(unittest.TestCase):
def setUp(self): def setUp(self):
if frappe.db.exists("Cash Flow Mapping", "Test Mapping"): 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): 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): def test_multiple_selections_not_allowed(self):
doc = frappe.new_doc("Cash Flow Mapping") doc = frappe.new_doc('Cash Flow Mapping')
doc.mapping_name = "Test Mapping" doc.mapping_name = 'Test Mapping'
doc.label = "Test label" doc.label = 'Test label'
doc.append("accounts", {"account": "Accounts Receivable - _TC"}) doc.append(
'accounts',
{'account': 'Accounts Receivable - _TC'}
)
doc.is_working_capital = 1 doc.is_working_capital = 1
doc.is_finance_cost = 1 doc.is_finance_cost = 1

Some files were not shown because too many files have changed in this diff Show More